@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.
- package/.claude/settings.local.json +7 -0
- package/CHANGELOG.md +11 -0
- package/README.md +2 -2
- package/bin/dotbot.js +17 -5
- package/dotbot.db +0 -0
- package/package.json +7 -1
- package/test/events.test.js +75 -0
- package/test/normalize.test.js +79 -0
- package/test/storage.test.js +93 -0
- package/test/tools.test.js +43 -0
package/CHANGELOG.md
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|