@stevederico/dotbot 0.17.0 → 0.19.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(grep -r \"grok-3\" /Users/sd/Desktop/projects/dotbot --include=\"*.js\" --include=\"*.md\" 2>/dev/null | grep -v node_modules)"
5
+ ]
6
+ }
7
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.19
2
+
3
+ Add test suite (node:test)
4
+ Zero test dependencies
5
+
6
+ 0.18
7
+
8
+ Add --verbose flag
9
+ Hide init logs by default
10
+ Add publishConfig for npm
11
+
1
12
  0.17
2
13
 
3
14
  Default model grok-4-1-fast-reasoning
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  <img src="https://img.shields.io/github/stars/stevederico/dotbot?style=social" alt="GitHub stars">
13
13
  </a>
14
14
  <a href="https://github.com/stevederico/dotbot">
15
- <img src="https://img.shields.io/badge/version-0.17-green" alt="version">
15
+ <img src="https://img.shields.io/badge/version-0.19-green" alt="version">
16
16
  </a>
17
17
  <img src="https://img.shields.io/badge/LOC-11k-orange" alt="Lines of Code">
18
18
  </p>
@@ -147,7 +147,7 @@ for await (const event of agent.chat({
147
147
  ## CLI Reference
148
148
 
149
149
  ```
150
- dotbot v0.17 — AI agent CLI
150
+ dotbot v0.19 — AI agent CLI
151
151
 
152
152
  Usage:
153
153
  dotbot "message" Send a message (default)
package/bin/dotbot.js CHANGED
@@ -50,7 +50,7 @@ async function loadModules() {
50
50
  agentLoop = mod.agentLoop;
51
51
  }
52
52
 
53
- const VERSION = '0.17';
53
+ const VERSION = '0.19';
54
54
  const DEFAULT_PORT = 3000;
55
55
  const DEFAULT_DB = './dotbot.db';
56
56
 
@@ -71,6 +71,7 @@ Options:
71
71
  --model, -m Model name (default: grok-4-1-fast-reasoning)
72
72
  --db SQLite database path (default: ${DEFAULT_DB})
73
73
  --port Server port for 'serve' command (default: ${DEFAULT_PORT})
74
+ --verbose Show initialization logs
74
75
  --help, -h Show this help
75
76
  --version, -v Show version
76
77
 
@@ -98,6 +99,7 @@ function parseCliArgs() {
98
99
  options: {
99
100
  help: { type: 'boolean', short: 'h', default: false },
100
101
  version: { type: 'boolean', short: 'v', default: false },
102
+ verbose: { type: 'boolean', default: false },
101
103
  provider: { type: 'string', short: 'p', default: 'xai' },
102
104
  model: { type: 'string', short: 'm', default: 'grok-4-1-fast-reasoning' },
103
105
  db: { type: 'string', default: DEFAULT_DB },
@@ -149,11 +151,18 @@ async function getProviderConfig(providerId) {
149
151
  * Initialize stores.
150
152
  *
151
153
  * @param {string} dbPath - Path to SQLite database
154
+ * @param {boolean} verbose - Show initialization logs
152
155
  * @returns {Promise<Object>} Initialized stores
153
156
  */
154
- async function initStores(dbPath) {
157
+ async function initStores(dbPath, verbose = false) {
155
158
  await loadModules();
156
159
 
160
+ // Suppress init logs unless verbose
161
+ const originalLog = console.log;
162
+ if (!verbose) {
163
+ console.log = () => {};
164
+ }
165
+
157
166
  const sessionStore = new stores.SQLiteSessionStore();
158
167
  await sessionStore.init(dbPath, {
159
168
  prefsFetcher: async () => ({ agentName: 'Dotbot', agentPersonality: '' }),
@@ -174,6 +183,9 @@ async function initStores(dbPath) {
174
183
  const eventStore = new stores.SQLiteEventStore();
175
184
  await eventStore.init(dbPath);
176
185
 
186
+ // Restore console.log
187
+ console.log = originalLog;
188
+
177
189
  return { sessionStore, cronStore, taskStore, triggerStore, memoryStore, eventStore };
178
190
  }
179
191
 
@@ -184,7 +196,7 @@ async function initStores(dbPath) {
184
196
  * @param {Object} options - CLI options
185
197
  */
186
198
  async function runChat(message, options) {
187
- const storesObj = await initStores(options.db);
199
+ const storesObj = await initStores(options.db, options.verbose);
188
200
  const provider = await getProviderConfig(options.provider);
189
201
 
190
202
  const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
@@ -238,7 +250,7 @@ async function runChat(message, options) {
238
250
  * @param {Object} options - CLI options
239
251
  */
240
252
  async function runRepl(options) {
241
- const storesObj = await initStores(options.db);
253
+ const storesObj = await initStores(options.db, options.verbose);
242
254
  const provider = await getProviderConfig(options.provider);
243
255
 
244
256
  const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
@@ -338,7 +350,7 @@ async function runRepl(options) {
338
350
  */
339
351
  async function runServer(options) {
340
352
  const port = parseInt(options.port, 10);
341
- const storesObj = await initStores(options.db);
353
+ const storesObj = await initStores(options.db, options.verbose);
342
354
 
343
355
  const server = createServer(async (req, res) => {
344
356
  // CORS headers
package/dotbot.db CHANGED
Binary file
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.17.0",
3
+ "version": "0.19.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",
7
7
  "bin": {
8
8
  "dotbot": "./bin/dotbot.js"
9
9
  },
10
+ "scripts": {
11
+ "test": "node --test test/*.test.js"
12
+ },
10
13
  "exports": {
11
14
  ".": "./index.js",
12
15
  "./core/*": "./core/*.js",
@@ -38,5 +41,8 @@
38
41
  "repository": {
39
42
  "type": "git",
40
43
  "url": "https://github.com/stevederico/dotbot.git"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
41
47
  }
42
48
  }
@@ -0,0 +1,75 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { validateEvent } from '../core/events.js';
4
+
5
+ describe('validateEvent', () => {
6
+ test('rejects non-object events', () => {
7
+ assert.throws(() => validateEvent(null), /must be an object/);
8
+ assert.throws(() => validateEvent('string'), /must be an object/);
9
+ });
10
+
11
+ test('rejects events without type', () => {
12
+ assert.throws(() => validateEvent({}), /must have a type/);
13
+ });
14
+
15
+ test('validates text_delta event', () => {
16
+ assert.ok(validateEvent({ type: 'text_delta', text: 'hello' }));
17
+ assert.throws(
18
+ () => validateEvent({ type: 'text_delta', text: 123 }),
19
+ /must have text string/
20
+ );
21
+ });
22
+
23
+ test('validates thinking event', () => {
24
+ assert.ok(validateEvent({ type: 'thinking', text: 'reasoning', hasNativeThinking: true }));
25
+ assert.throws(
26
+ () => validateEvent({ type: 'thinking', text: 'hi' }),
27
+ /must have hasNativeThinking/
28
+ );
29
+ });
30
+
31
+ test('validates tool_start event', () => {
32
+ assert.ok(validateEvent({ type: 'tool_start', name: 'web_search', input: { query: 'test' } }));
33
+ assert.throws(
34
+ () => validateEvent({ type: 'tool_start', name: 'test' }),
35
+ /must have input object/
36
+ );
37
+ });
38
+
39
+ test('validates tool_result event', () => {
40
+ assert.ok(validateEvent({
41
+ type: 'tool_result',
42
+ name: 'web_search',
43
+ input: { query: 'test' },
44
+ result: 'found it'
45
+ }));
46
+ assert.throws(
47
+ () => validateEvent({ type: 'tool_result', name: 'test', input: {} }),
48
+ /must have result string/
49
+ );
50
+ });
51
+
52
+ test('validates tool_error event', () => {
53
+ assert.ok(validateEvent({ type: 'tool_error', name: 'test', error: 'failed' }));
54
+ assert.throws(
55
+ () => validateEvent({ type: 'tool_error', name: 'test' }),
56
+ /must have error string/
57
+ );
58
+ });
59
+
60
+ test('validates done event', () => {
61
+ assert.ok(validateEvent({ type: 'done', content: 'finished' }));
62
+ assert.throws(
63
+ () => validateEvent({ type: 'done' }),
64
+ /must have content string/
65
+ );
66
+ });
67
+
68
+ test('validates error event', () => {
69
+ assert.ok(validateEvent({ type: 'error', error: 'something broke' }));
70
+ assert.throws(
71
+ () => validateEvent({ type: 'error' }),
72
+ /must have error string/
73
+ );
74
+ });
75
+ });
@@ -0,0 +1,79 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { toStandardFormat, toProviderFormat } from '../core/normalize.js';
4
+
5
+ describe('toStandardFormat', () => {
6
+ test('normalizes simple user message', () => {
7
+ const messages = [{ role: 'user', content: 'hello' }];
8
+ const result = toStandardFormat(messages);
9
+ assert.strictEqual(result.length, 1);
10
+ assert.strictEqual(result[0].role, 'user');
11
+ assert.strictEqual(result[0].content, 'hello');
12
+ });
13
+
14
+ test('normalizes system message', () => {
15
+ const messages = [{ role: 'system', content: 'You are helpful' }];
16
+ const result = toStandardFormat(messages);
17
+ assert.strictEqual(result[0].role, 'system');
18
+ assert.strictEqual(result[0].content, 'You are helpful');
19
+ });
20
+
21
+ test('normalizes assistant message with text', () => {
22
+ const messages = [{ role: 'assistant', content: 'Hi there' }];
23
+ const result = toStandardFormat(messages);
24
+ assert.strictEqual(result[0].role, 'assistant');
25
+ assert.strictEqual(result[0].content, 'Hi there');
26
+ });
27
+
28
+ test('handles Anthropic tool_use blocks', () => {
29
+ const messages = [{
30
+ role: 'assistant',
31
+ content: [
32
+ { type: 'text', text: 'Let me search' },
33
+ { type: 'tool_use', id: 'tool1', name: 'web_search', input: { query: 'test' } }
34
+ ]
35
+ }];
36
+ const result = toStandardFormat(messages);
37
+ assert.strictEqual(result[0].role, 'assistant');
38
+ assert.strictEqual(result[0].content, 'Let me search');
39
+ assert.ok(result[0].toolCalls);
40
+ assert.strictEqual(result[0].toolCalls[0].name, 'web_search');
41
+ });
42
+
43
+ test('skips tool-result-only messages', () => {
44
+ const messages = [
45
+ { role: 'user', content: 'hello' },
46
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: 'result' }] }
47
+ ];
48
+ const result = toStandardFormat(messages);
49
+ assert.strictEqual(result.length, 1);
50
+ assert.strictEqual(result[0].content, 'hello');
51
+ });
52
+ });
53
+
54
+ describe('toProviderFormat', () => {
55
+ test('converts standard user message to Anthropic', () => {
56
+ const messages = [{ role: 'user', content: 'hello' }];
57
+ const result = toProviderFormat(messages, 'anthropic');
58
+ assert.strictEqual(result[0].role, 'user');
59
+ assert.strictEqual(result[0].content, 'hello');
60
+ });
61
+
62
+ test('converts standard user message to OpenAI', () => {
63
+ const messages = [{ role: 'user', content: 'hello' }];
64
+ const result = toProviderFormat(messages, 'openai');
65
+ assert.strictEqual(result[0].role, 'user');
66
+ assert.strictEqual(result[0].content, 'hello');
67
+ });
68
+
69
+ test('converts assistant with toolCalls to Anthropic format', () => {
70
+ const messages = [{
71
+ role: 'assistant',
72
+ content: 'Searching...',
73
+ toolCalls: [{ id: 't1', name: 'web_search', input: { q: 'test' }, result: 'found', status: 'done' }]
74
+ }];
75
+ const result = toProviderFormat(messages, 'anthropic');
76
+ assert.strictEqual(result[0].role, 'assistant');
77
+ assert.ok(Array.isArray(result[0].content));
78
+ });
79
+ });
@@ -0,0 +1,93 @@
1
+ import { test, describe, before, after } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { unlink } from 'node:fs/promises';
4
+ import { SQLiteSessionStore } from '../storage/SQLiteAdapter.js';
5
+ import { SQLiteMemoryStore } from '../storage/SQLiteMemoryAdapter.js';
6
+
7
+ const TEST_DB = './test/test.db';
8
+
9
+ describe('SQLiteSessionStore', () => {
10
+ let store;
11
+
12
+ before(async () => {
13
+ store = new SQLiteSessionStore();
14
+ await store.init(TEST_DB);
15
+ });
16
+
17
+ after(async () => {
18
+ if (store?.db) store.db.close();
19
+ await unlink(TEST_DB).catch(() => {});
20
+ });
21
+
22
+ test('creates a session', async () => {
23
+ const session = await store.createSession('user1', 'grok-4-1-fast-reasoning', 'xai');
24
+ assert.ok(session.id);
25
+ assert.strictEqual(session.owner, 'user1');
26
+ assert.strictEqual(session.model, 'grok-4-1-fast-reasoning');
27
+ assert.strictEqual(session.provider, 'xai');
28
+ });
29
+
30
+ test('retrieves session by id', async () => {
31
+ const created = await store.createSession('user2', 'gpt-4o', 'openai');
32
+ const fetched = await store.getSessionInternal(created.id);
33
+ assert.strictEqual(fetched.id, created.id);
34
+ assert.strictEqual(fetched.owner, 'user2');
35
+ });
36
+
37
+ test('lists sessions for user', async () => {
38
+ await store.createSession('user3', 'grok-4-1-fast-reasoning', 'xai');
39
+ await store.createSession('user3', 'grok-4-1-fast-reasoning', 'xai');
40
+ const sessions = await store.listSessions('user3');
41
+ assert.ok(sessions.length >= 2);
42
+ });
43
+
44
+ test('adds message to session', async () => {
45
+ const session = await store.createSession('user4', 'grok-4-1-fast-reasoning', 'xai');
46
+ await store.addMessage(session.id, { role: 'user', content: 'hello' });
47
+ const fetched = await store.getSessionInternal(session.id);
48
+ const userMsg = fetched.messages.find(m => m.role === 'user' && m.content === 'hello');
49
+ assert.ok(userMsg);
50
+ });
51
+
52
+ test('deletes session', async () => {
53
+ const session = await store.createSession('user5', 'grok-4-1-fast-reasoning', 'xai');
54
+ await store.deleteSession(session.id, 'user5');
55
+ const fetched = await store.getSessionInternal(session.id);
56
+ assert.strictEqual(fetched, null);
57
+ });
58
+ });
59
+
60
+ describe('SQLiteMemoryStore', () => {
61
+ let store;
62
+
63
+ before(async () => {
64
+ store = new SQLiteMemoryStore();
65
+ await store.init(TEST_DB);
66
+ });
67
+
68
+ after(async () => {
69
+ if (store?.db) store.db.close();
70
+ await unlink(TEST_DB).catch(() => {});
71
+ });
72
+
73
+ test('saves and retrieves memory', async () => {
74
+ await store.writeMemory('memuser1', 'favorite_color', 'blue');
75
+ const memories = await store.getAllMemories('memuser1');
76
+ const found = memories.find(m => m.key === 'favorite_color');
77
+ assert.ok(found);
78
+ assert.strictEqual(found.value, 'blue');
79
+ });
80
+
81
+ test('reads memory by key', async () => {
82
+ await store.writeMemory('memuser2', 'pet', 'dog named Max');
83
+ const result = await store.readMemory('memuser2', 'pet');
84
+ assert.strictEqual(result.value, 'dog named Max');
85
+ });
86
+
87
+ test('deletes memory', async () => {
88
+ await store.writeMemory('memuser3', 'temp', 'delete me');
89
+ await store.deleteMemory('memuser3', 'temp');
90
+ const result = await store.readMemory('memuser3', 'temp');
91
+ assert.strictEqual(result, null);
92
+ });
93
+ });
@@ -0,0 +1,43 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { coreTools } from '../tools/index.js';
4
+
5
+ describe('coreTools', () => {
6
+ test('exports an array of tools', () => {
7
+ assert.ok(Array.isArray(coreTools));
8
+ assert.ok(coreTools.length > 0);
9
+ });
10
+
11
+ test('all tools have required properties', () => {
12
+ for (const tool of coreTools) {
13
+ assert.ok(tool.name, `Tool missing name`);
14
+ assert.ok(tool.description, `Tool ${tool.name} missing description`);
15
+ assert.ok(tool.parameters, `Tool ${tool.name} missing parameters`);
16
+ assert.strictEqual(tool.parameters.type, 'object', `Tool ${tool.name} parameters.type must be object`);
17
+ }
18
+ });
19
+
20
+ test('tool names are unique', () => {
21
+ const names = coreTools.map(t => t.name);
22
+ const unique = new Set(names);
23
+ assert.strictEqual(names.length, unique.size, 'Duplicate tool names found');
24
+ });
25
+
26
+ test('tool names are snake_case', () => {
27
+ for (const tool of coreTools) {
28
+ assert.ok(/^[a-z][a-z0-9_]*$/.test(tool.name), `Tool ${tool.name} should be snake_case`);
29
+ }
30
+ });
31
+
32
+ test('contains expected core tools', () => {
33
+ const names = coreTools.map(t => t.name);
34
+ assert.ok(names.includes('memory_save'), 'Missing memory_save');
35
+ assert.ok(names.includes('web_search'), 'Missing web_search');
36
+ assert.ok(names.includes('file_read'), 'Missing file_read');
37
+ assert.ok(names.includes('weather_get'), 'Missing weather_get');
38
+ });
39
+
40
+ test('has at least 40 tools', () => {
41
+ assert.ok(coreTools.length >= 40, `Expected 40+ tools, got ${coreTools.length}`);
42
+ });
43
+ });