@stevederico/dotbot 0.18.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,8 @@
1
+ 0.19
2
+
3
+ Add test suite (node:test)
4
+ Zero test dependencies
5
+
1
6
  0.18
2
7
 
3
8
  Add --verbose flag
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.18-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.18 — 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.18';
53
+ const VERSION = '0.19';
54
54
  const DEFAULT_PORT = 3000;
55
55
  const DEFAULT_DB = './dotbot.db';
56
56
 
package/dotbot.db CHANGED
Binary file
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.18.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",
@@ -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
+ });