@stevederico/dotbot 0.18.0 → 0.20.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.
@@ -1,9 +1,44 @@
1
1
  import crypto from 'crypto';
2
2
  import { DatabaseSync } from 'node:sqlite';
3
3
  import { SessionStore } from './SessionStore.js';
4
- import { defaultSystemPrompt } from './MongoAdapter.js';
5
4
  import { toStandardFormat } from '../core/normalize.js';
6
5
 
6
+ /**
7
+ * Default system prompt builder for the agent.
8
+ *
9
+ * @param {Object} options - Prompt options
10
+ * @param {string} options.agentName - Agent display name
11
+ * @param {string} options.agentPersonality - Personality description
12
+ * @returns {string} System prompt
13
+ */
14
+ export function defaultSystemPrompt({ agentName = 'Dottie', agentPersonality = '' } = {}) {
15
+ const now = new Date().toISOString();
16
+ return `You are a helpful personal AI assistant called ${agentName}.${agentPersonality ? `\nYour personality and tone: ${agentPersonality}. Embody this in all responses.` : ''}
17
+ You have access to tools for searching the web, reading/writing files, fetching URLs, running code, long-term memory, and scheduled tasks.
18
+ The current date and time is ${now}.
19
+
20
+ Use tools when they would help answer the user's question — don't guess when you can look things up.
21
+ Keep responses concise and useful. When you use a tool, explain what you found.
22
+
23
+ Memory guidelines:
24
+ - When the user shares personal info (name, preferences, projects, goals), save it with memory_save.
25
+ - When the user references past conversations or asks "do you remember", search with memory_search.
26
+ - When the user asks to forget something, use memory_search to find the key, then memory_delete to remove it.
27
+ - Be selective — only save things worth recalling in future conversations.
28
+ - Don't announce every memory save unless the user would want to know.
29
+
30
+ Scheduling guidelines:
31
+ - When the user asks for a reminder, periodic check, or recurring job, use schedule_job.
32
+ - Write the prompt as if the user is asking you to do something when the job fires.
33
+ - For recurring jobs, suggest a reasonable interval if the user doesn't specify one.
34
+
35
+ Follow-up suggestions:
36
+ - At the end of every response, suggest one natural follow-up question the user might ask next.
37
+ - Format: <followup>Your suggested question here</followup>
38
+ - Keep it short, specific to the conversation context, and genuinely useful.
39
+ - Do not include the followup tag when using tools or in error responses.`;
40
+ }
41
+
7
42
  /**
8
43
  * SQLite-backed SessionStore implementation
9
44
  *
package/storage/index.js CHANGED
@@ -1,19 +1,14 @@
1
1
  export { SessionStore } from './SessionStore.js';
2
- export { MongoSessionStore, defaultSystemPrompt } from './MongoAdapter.js';
3
- export { SQLiteSessionStore } from './SQLiteAdapter.js';
2
+ export { SQLiteSessionStore, defaultSystemPrompt } from './SQLiteAdapter.js';
4
3
  export { MemorySessionStore } from './MemoryStore.js';
5
4
  export { CronStore } from './CronStore.js';
6
- export { MongoCronStore, parseInterval, HEARTBEAT_INTERVAL_MS, HEARTBEAT_PROMPT } from './MongoCronAdapter.js';
7
- export { SQLiteCronStore } from './SQLiteCronAdapter.js';
5
+ export { SQLiteCronStore, parseInterval, HEARTBEAT_INTERVAL_MS, HEARTBEAT_PROMPT } from './SQLiteCronAdapter.js';
8
6
  export { TaskStore } from './TaskStore.js';
9
- export { MongoTaskStore } from './MongoTaskAdapter.js';
10
7
  export { SQLiteTaskStore } from './SQLiteTaskAdapter.js';
11
8
  // Backwards compatibility aliases
12
9
  export { TaskStore as GoalStore } from './TaskStore.js';
13
- export { MongoTaskStore as MongoGoalStore } from './MongoTaskAdapter.js';
14
10
  export { SQLiteTaskStore as SQLiteGoalStore } from './SQLiteTaskAdapter.js';
15
11
  export { TriggerStore } from './TriggerStore.js';
16
- export { MongoTriggerStore } from './MongoTriggerAdapter.js';
17
12
  export { SQLiteTriggerStore } from './SQLiteTriggerAdapter.js';
18
13
  export { SQLiteMemoryStore } from './SQLiteMemoryAdapter.js';
19
14
  export { EventStore } from './EventStore.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
+ });