@stevederico/dotbot 0.28.0 → 0.29.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/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ 0.29
2
+
3
+ Extract shared streamEvents
4
+ Remove dead databaseManager logging
5
+ Remove dead CDP methods
6
+ Remove dead compat aliases
7
+ Remove dead observer module
8
+ Consolidate cron row mapping
9
+ Update README sandbox docs
10
+
1
11
  0.28
2
12
 
3
13
  Add --sandbox mode
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/github/stars/stevederico/dotbot?style=social" alt="GitHub stars">
14
14
  </a>
15
15
  <a href="https://github.com/stevederico/dotbot">
16
- <img src="https://img.shields.io/badge/version-0.25-green" alt="version">
16
+ <img src="https://img.shields.io/badge/version-0.28-green" alt="version">
17
17
  </a>
18
18
  <img src="https://img.shields.io/badge/LOC-11k-orange" alt="Lines of Code">
19
19
  </p>
@@ -27,9 +27,10 @@
27
27
 
28
28
  | | dotbot | nanobot | OpenClaw |
29
29
  |---|:---:|:---:|:---:|
30
- | **Lines of Code** | **11k** | 22k | 1M+ |
30
+ | **Lines of Code** | **~11k** | 22k | 1M+ |
31
31
  | **Tools** | **53** | ~10 | ~50 |
32
- | **Dependencies** | Minimal | Heavy | Heavy |
32
+ | **Dependencies** | **0** | Heavy | Heavy |
33
+ | **Sandbox Mode** | **Built-in** | No | Requires NemoClaw |
33
34
 
34
35
  Everything you need for AI agents. Nothing you don't. No bloated abstractions. No dependency hell. Just a clean, focused agent that works.
35
36
 
@@ -43,7 +44,9 @@ A **streaming AI agent** with tool execution, autonomous tasks, and scheduled jo
43
44
  ```bash
44
45
  dotbot "What's the weather in San Francisco?"
45
46
  dotbot # Interactive mode
47
+ dotbot --sandbox # Sandbox mode (restricted tools)
46
48
  dotbot serve --port 3000
49
+ dotbot models # List available models
47
50
  dotbot tools # List all 53 tools
48
51
  ```
49
52
 
@@ -80,6 +83,49 @@ dotbot stats
80
83
  dotbot memory
81
84
  ```
82
85
 
86
+ ### Sandbox Mode
87
+
88
+ Run dotbot with restricted tool access — deny-by-default.
89
+
90
+ ```bash
91
+ # Full lockdown — safe tools only (memory, search, weather, tasks)
92
+ dotbot --sandbox "What is 2+2?"
93
+
94
+ # Allow specific domains for web_fetch and browser_navigate
95
+ dotbot --sandbox --allow github
96
+ dotbot --sandbox --allow github --allow slack
97
+
98
+ # Allow specific tool groups
99
+ dotbot --sandbox --allow messages
100
+ dotbot --sandbox --allow images
101
+
102
+ # Mix domains and tool groups
103
+ dotbot --sandbox --allow github --allow messages --allow npm
104
+
105
+ # Custom domain
106
+ dotbot --sandbox --allow api.mycompany.com
107
+
108
+ # Persistent config in ~/.dotbotrc
109
+ # { "sandbox": true, "sandboxAllow": ["github", "slack", "messages"] }
110
+ ```
111
+
112
+ **What's blocked by default:**
113
+
114
+ | Category | Tools | How to unlock |
115
+ |----------|-------|---------------|
116
+ | Filesystem writes | `file_write`, `file_delete`, `file_move`, `folder_create` | Cannot unlock |
117
+ | Arbitrary HTTP | `web_fetch` | `--allow <domain>` |
118
+ | Browser | `browser_navigate` | `--allow <domain>` |
119
+ | Code execution | `run_code` | Always allowed (Node.js permission model) |
120
+ | Messaging | `message_*` | `--allow messages` |
121
+ | Images | `image_*` | `--allow images` |
122
+ | Notifications | `notify_user` | `--allow notifications` |
123
+ | App generation | `app_generate`, `app_validate` | Cannot unlock |
124
+
125
+ **What's always allowed:** `memory_*`, `web_search`, `grokipedia_search`, `file_read`, `file_list`, `weather_get`, `event_*`, `task_*`, `trigger_*`, `schedule_job`, `list_jobs`, `toggle_job`, `cancel_job`
126
+
127
+ **Domain presets:** `github`, `slack`, `discord`, `npm`, `pypi`, `jira`, `huggingface`, `docker`, `telegram`
128
+
83
129
  ### Library Usage
84
130
 
85
131
  ```bash
@@ -139,9 +185,13 @@ for await (const event of agent.chat({
139
185
  - **Cerebras** — ultra-fast inference
140
186
  - **Ollama** — local models, no API cost
141
187
 
188
+ ### 🔒 **Sandbox Mode**
189
+ - **Deny-by-default** tool access — no files, code, browser, or messaging
190
+ - **Domain allowlists** — `--allow github`, `--allow slack`
191
+ - **Preset-based** tool unlocking — `--allow messages`, `--allow images`
192
+
142
193
  ### 💾 **Pluggable Storage**
143
194
  - **SQLite** — zero dependencies with Node.js 22.5+
144
- - **MongoDB** — scalable with full-text search
145
195
  - **Memory** — in-memory for testing
146
196
 
147
197
  ### 📊 **Full Audit Trail**
@@ -154,7 +204,7 @@ for await (const event of agent.chat({
154
204
  ## CLI Reference
155
205
 
156
206
  ```
157
- dotbot v0.25 — AI agent CLI
207
+ dotbot v0.28 — AI agent CLI
158
208
 
159
209
  Usage:
160
210
  dotbot "message" One-shot query
@@ -164,6 +214,7 @@ Usage:
164
214
  echo "msg" | dotbot Pipe input from stdin
165
215
 
166
216
  Commands:
217
+ models List available models from provider
167
218
  doctor Check environment and configuration
168
219
  tools List all available tools
169
220
  stats Show database statistics
@@ -182,6 +233,8 @@ Options:
182
233
  --model, -m Model name (default: grok-4-1-fast-reasoning)
183
234
  --system, -s Custom system prompt (prepended to default)
184
235
  --session Resume a specific session by ID
236
+ --sandbox Restrict tools to safe subset (deny-by-default)
237
+ --allow Allow domain/preset in sandbox (github, slack, messages, etc.)
185
238
  --db SQLite database path (default: ./dotbot.db)
186
239
  --port Server port for 'serve' command
187
240
  --openai Enable OpenAI-compatible API endpoints
@@ -197,7 +250,7 @@ Environment Variables:
197
250
  OLLAMA_BASE_URL Base URL for Ollama (default: http://localhost:11434)
198
251
 
199
252
  Config File:
200
- ~/.dotbotrc JSON config for defaults (provider, model, db)
253
+ ~/.dotbotrc JSON config for defaults (provider, model, db, sandbox)
201
254
  ```
202
255
 
203
256
  <br />
@@ -323,9 +376,8 @@ await agent.chat({
323
376
  | Technology | Purpose |
324
377
  |------------|---------|
325
378
  | **Node.js 22.5+** | Runtime with built-in SQLite |
326
- | **Playwright** | Browser automation |
379
+ | **Chrome DevTools Protocol** | Browser automation (zero deps) |
327
380
  | **SQLite** | Default storage (zero deps) |
328
- | **MongoDB** | Scalable storage option |
329
381
 
330
382
  <br />
331
383
 
@@ -334,12 +386,13 @@ await agent.chat({
334
386
  ```
335
387
  dotbot/
336
388
  ├── bin/
337
- │ └── dotbot.js # CLI entry point
389
+ │ └── dotbot.js # CLI entry point (REPL, server, sandbox mode)
338
390
  ├── core/
339
391
  │ ├── agent.js # Streaming agent loop
340
392
  │ ├── events.js # SSE event schemas
341
393
  │ ├── compaction.js # Context window management
342
394
  │ ├── normalize.js # Message format conversion
395
+ │ ├── failover.js # Cross-provider failover
343
396
  │ ├── cron_handler.js # Scheduled job execution
344
397
  │ └── trigger_handler.js # Event-driven triggers
345
398
  ├── storage/
@@ -347,9 +400,8 @@ dotbot/
347
400
  │ ├── TaskStore.js # Task interface
348
401
  │ ├── CronStore.js # Job scheduling interface
349
402
  │ ├── TriggerStore.js # Trigger interface
350
- ├── SQLite*.js # SQLite adapters
351
- │ └── Mongo*.js # MongoDB adapters
352
- ├── tools/ # 47 built-in tools
403
+ └── SQLite*.js # SQLite adapters
404
+ ├── tools/ # 53 built-in tools
353
405
  │ ├── memory.js
354
406
  │ ├── web.js
355
407
  │ ├── browser.js
package/bin/dotbot.js CHANGED
@@ -604,50 +604,21 @@ async function initStores(dbPath, verbose = false, customSystemPrompt = '') {
604
604
  }
605
605
 
606
606
  /**
607
- * Run a single chat message and stream output.
607
+ * Stream events from an agentLoop iterable to stdout.
608
+ * Handles thinking markers, text deltas, tool status, and errors.
608
609
  *
609
- * @param {string} message - User message
610
- * @param {Object} options - CLI options
610
+ * @param {AsyncIterable<Object>} events - Async iterable of agentLoop events
611
+ * @returns {Promise<string>} Accumulated assistant text content
611
612
  */
612
- async function runChat(message, options) {
613
- const storesObj = await initStores(options.db, options.verbose, options.system);
614
- const provider = await getProviderConfig(options.provider);
615
-
616
- let session;
617
- let messages;
618
-
619
- if (options.session) {
620
- session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
621
- if (!session) {
622
- console.error(`Error: Session not found: ${options.session}`);
623
- process.exit(1);
624
- }
625
- messages = [...(session.messages || []), { role: 'user', content: message }];
626
- } else {
627
- session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
628
- messages = [{ role: 'user', content: message }];
629
- }
630
-
631
- const context = {
632
- userID: 'cli-user',
633
- sessionId: session.id,
634
- providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
635
- ...storesObj,
636
- };
637
-
613
+ async function streamEvents(events) {
638
614
  let hasThinkingText = false;
639
615
  let thinkingDone = false;
616
+ let assistantContent = '';
640
617
 
641
618
  process.stdout.write('Thinking');
642
619
  startSpinner();
643
620
 
644
- for await (const event of agentLoop({
645
- model: options.model,
646
- messages,
647
- tools: getActiveTools(options.sandbox, options.sandboxAllow),
648
- provider,
649
- context,
650
- })) {
621
+ for await (const event of events) {
651
622
  switch (event.type) {
652
623
  case 'thinking':
653
624
  if (event.text) {
@@ -669,6 +640,7 @@ async function runChat(message, options) {
669
640
  thinkingDone = true;
670
641
  }
671
642
  process.stdout.write(event.text);
643
+ assistantContent += event.text;
672
644
  break;
673
645
  case 'tool_start':
674
646
  if (!thinkingDone) {
@@ -689,11 +661,55 @@ async function runChat(message, options) {
689
661
  stopSpinner('error');
690
662
  break;
691
663
  case 'error':
664
+ stopSpinner();
692
665
  console.error(`\nError: ${event.error}`);
693
666
  break;
694
667
  }
695
668
  }
696
669
 
670
+ return assistantContent;
671
+ }
672
+
673
+ /**
674
+ * Run a single chat message and stream output.
675
+ *
676
+ * @param {string} message - User message
677
+ * @param {Object} options - CLI options
678
+ */
679
+ async function runChat(message, options) {
680
+ const storesObj = await initStores(options.db, options.verbose, options.system);
681
+ const provider = await getProviderConfig(options.provider);
682
+
683
+ let session;
684
+ let messages;
685
+
686
+ if (options.session) {
687
+ session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
688
+ if (!session) {
689
+ console.error(`Error: Session not found: ${options.session}`);
690
+ process.exit(1);
691
+ }
692
+ messages = [...(session.messages || []), { role: 'user', content: message }];
693
+ } else {
694
+ session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
695
+ messages = [{ role: 'user', content: message }];
696
+ }
697
+
698
+ const context = {
699
+ userID: 'cli-user',
700
+ sessionId: session.id,
701
+ providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
702
+ ...storesObj,
703
+ };
704
+
705
+ await streamEvents(agentLoop({
706
+ model: options.model,
707
+ messages,
708
+ tools: getActiveTools(options.sandbox, options.sandboxAllow),
709
+ provider,
710
+ context,
711
+ }));
712
+
697
713
  process.stdout.write('\n\n');
698
714
  process.exit(0);
699
715
  }
@@ -864,68 +880,14 @@ async function runRepl(options) {
864
880
  const handleMessage = async (text) => {
865
881
  messages.push({ role: 'user', content: text });
866
882
 
867
- let hasThinkingText = false;
868
- let thinkingDone = false;
869
- let assistantContent = '';
870
-
871
- process.stdout.write('Thinking');
872
- startSpinner();
873
-
874
883
  try {
875
- for await (const event of agentLoop({
884
+ const assistantContent = await streamEvents(agentLoop({
876
885
  model: options.model,
877
886
  messages: [...messages],
878
887
  tools: getActiveTools(options.sandbox, options.sandboxAllow),
879
888
  provider,
880
889
  context,
881
- })) {
882
- switch (event.type) {
883
- case 'thinking':
884
- if (event.text) {
885
- if (!hasThinkingText) {
886
- stopSpinner('');
887
- process.stdout.write('\n');
888
- hasThinkingText = true;
889
- }
890
- process.stdout.write(event.text);
891
- }
892
- break;
893
- case 'text_delta':
894
- if (!thinkingDone) {
895
- if (hasThinkingText) {
896
- process.stdout.write('\n...done thinking.\n\n');
897
- } else {
898
- stopSpinner('');
899
- }
900
- thinkingDone = true;
901
- }
902
- process.stdout.write(event.text);
903
- assistantContent += event.text;
904
- break;
905
- case 'tool_start':
906
- if (!thinkingDone) {
907
- if (hasThinkingText) {
908
- process.stdout.write('\n...done thinking.\n\n');
909
- } else {
910
- stopSpinner('');
911
- }
912
- thinkingDone = true;
913
- }
914
- process.stdout.write(`[${event.name}] `);
915
- startSpinner();
916
- break;
917
- case 'tool_result':
918
- stopSpinner('done');
919
- break;
920
- case 'tool_error':
921
- stopSpinner('error');
922
- break;
923
- case 'error':
924
- stopSpinner();
925
- console.error(`\nError: ${event.error}`);
926
- break;
927
- }
928
- }
890
+ }));
929
891
 
930
892
  if (assistantContent) {
931
893
  messages.push({ role: 'assistant', content: assistantContent });
package/core/agent.js CHANGED
@@ -31,7 +31,7 @@ const OLLAMA_BASE = "http://localhost:11434";
31
31
  * @param {Array} options.tools - Tool definitions from tools.js
32
32
  * @param {AbortSignal} [options.signal] - Optional abort signal
33
33
  * @param {Object} [options.provider] - Provider config from AI_PROVIDERS. Defaults to Ollama.
34
- * @param {Object} [options.context] - Execution context passed to tool execute functions (e.g. databaseManager, dbConfig, userID).
34
+ * @param {Object} [options.context] - Execution context passed to tool execute functions (e.g. providers, userID).
35
35
  * @yields {Object} Stream events for the frontend
36
36
  */
37
37
  export async function* agentLoop({ model, messages, tools, signal, provider, context, maxTurns }) {
package/core/cdp.js CHANGED
@@ -184,35 +184,22 @@ export class CDPClient {
184
184
  return result.result?.value;
185
185
  }
186
186
 
187
- /**
188
- * Get the page title.
189
- * @returns {Promise<string>}
190
- */
187
+ /** Get the page title. */
191
188
  async getTitle() {
192
189
  return this.evaluate('document.title');
193
190
  }
194
191
 
195
- /**
196
- * Get the current URL.
197
- * @returns {Promise<string>}
198
- */
192
+ /** Get the current URL. */
199
193
  async getUrl() {
200
194
  return this.evaluate('window.location.href');
201
195
  }
202
196
 
203
- /**
204
- * Get text content of the page body.
205
- * @returns {Promise<string>}
206
- */
197
+ /** Get text content of the page body. */
207
198
  async getBodyText() {
208
199
  return this.evaluate('document.body?.innerText || ""');
209
200
  }
210
201
 
211
- /**
212
- * Get text content of an element by CSS selector.
213
- * @param {string} selector - CSS selector
214
- * @returns {Promise<string>}
215
- */
202
+ /** Get text content of an element by CSS selector. */
216
203
  async getText(selector) {
217
204
  const escaped = selector.replace(/"/g, '\\"');
218
205
  return this.evaluate(`document.querySelector("${escaped}")?.innerText || ""`);
@@ -308,26 +295,6 @@ export class CDPClient {
308
295
  });
309
296
  }
310
297
 
311
- /**
312
- * Click an element by CSS selector.
313
- * @param {string} selector - CSS selector
314
- */
315
- async clickSelector(selector) {
316
- const el = await this.querySelector(selector);
317
- if (!el) throw new Error(`Element not found: ${selector}`);
318
- await this.click(el.x, el.y);
319
- }
320
-
321
- /**
322
- * Click an element by visible text.
323
- * @param {string} text - Text content to find
324
- */
325
- async clickText(text) {
326
- const el = await this.getByText(text);
327
- if (!el) throw new Error(`Element with text "${text}" not found`);
328
- await this.click(el.x, el.y);
329
- }
330
-
331
298
  /**
332
299
  * Type text character by character.
333
300
  * @param {string} text - Text to type
@@ -453,9 +420,7 @@ export class CDPClient {
453
420
  });
454
421
  }
455
422
 
456
- /**
457
- * Close the connection.
458
- */
423
+ /** Close the CDP connection. */
459
424
  close() {
460
425
  if (this.ws) {
461
426
  this.ws.close();
@@ -490,24 +455,6 @@ export class CDPClient {
490
455
  throw lastError;
491
456
  }
492
457
 
493
- /**
494
- * Wait for an element to appear in the DOM.
495
- * @param {string} selector - CSS selector
496
- * @param {Object} options - Wait options
497
- * @param {number} options.timeout - Timeout in ms (default: 5000)
498
- * @param {number} options.interval - Poll interval in ms (default: 100)
499
- * @returns {Promise<{x: number, y: number, nodeId: number}>} Element info
500
- */
501
- async waitForSelector(selector, { timeout = 5000, interval = 100 } = {}) {
502
- const start = Date.now();
503
- while (Date.now() - start < timeout) {
504
- const el = await this.querySelector(selector);
505
- if (el) return el;
506
- await new Promise(r => setTimeout(r, interval));
507
- }
508
- throw new Error(`Timeout waiting for selector: ${selector}`);
509
- }
510
-
511
458
  /**
512
459
  * Wait for network to be idle (no requests for a period).
513
460
  * @param {Object} options - Wait options
package/index.js CHANGED
@@ -18,10 +18,8 @@ import {
18
18
  notifyTools,
19
19
  createBrowserTools,
20
20
  taskTools,
21
- goalTools,
22
21
  triggerTools,
23
22
  jobTools,
24
- cronTools,
25
23
  eventTools,
26
24
  appgenTools,
27
25
  } from './tools/index.js';
@@ -40,9 +38,6 @@ export {
40
38
  runWithConcurrency,
41
39
  TaskStore,
42
40
  SQLiteTaskStore,
43
- // Backwards compatibility aliases
44
- GoalStore,
45
- SQLiteGoalStore,
46
41
  TriggerStore,
47
42
  SQLiteTriggerStore,
48
43
  SQLiteMemoryStore,
@@ -65,10 +60,8 @@ export {
65
60
  browserTools,
66
61
  createBrowserTools,
67
62
  taskTools,
68
- goalTools, // backwards compatibility alias
69
63
  triggerTools,
70
64
  jobTools,
71
- cronTools, // backwards compatibility alias
72
65
  eventTools,
73
66
  appgenTools,
74
67
  } from './tools/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",
@@ -220,16 +220,7 @@ export class SQLiteCronStore extends CronStore {
220
220
  "SELECT * FROM cron_tasks WHERE session_id = ? AND name != 'heartbeat' ORDER BY next_run_at ASC"
221
221
  ).all(sessionId || 'default');
222
222
 
223
- return rows.map(r => ({
224
- id: r.id,
225
- name: r.name,
226
- prompt: r.prompt,
227
- nextRunAt: new Date(r.next_run_at),
228
- recurring: !!r.recurring,
229
- intervalMs: r.interval_ms,
230
- enabled: !!r.enabled,
231
- lastRunAt: r.last_run_at ? new Date(r.last_run_at) : null,
232
- }));
223
+ return rows.map(r => this._rowToTask(r));
233
224
  }
234
225
 
235
226
  /**
@@ -257,18 +248,7 @@ export class SQLiteCronStore extends CronStore {
257
248
 
258
249
  const rows = this.db.prepare(query).all(...params);
259
250
 
260
- return rows.map(r => ({
261
- id: r.id,
262
- name: r.name,
263
- prompt: r.prompt,
264
- sessionId: r.session_id,
265
- nextRunAt: new Date(r.next_run_at),
266
- recurring: !!r.recurring,
267
- intervalMs: r.interval_ms,
268
- enabled: !!r.enabled,
269
- lastRunAt: r.last_run_at ? new Date(r.last_run_at) : null,
270
- createdAt: new Date(r.created_at),
271
- }));
251
+ return rows.map(r => this._rowToTask(r));
272
252
  }
273
253
 
274
254
  /**
@@ -374,53 +354,6 @@ export class SQLiteCronStore extends CronStore {
374
354
  return null;
375
355
  }
376
356
 
377
- /**
378
- * Ensure a Morning Brief job exists for the user (disabled by default).
379
- * Creates a daily recurring job at 8:00 AM if not present.
380
- *
381
- * @param {string} userId - User ID
382
- * @returns {Promise<Object|null>} Created task or null if already exists
383
- */
384
- async ensureMorningBrief(userId) {
385
- if (!this.db || !userId) return null;
386
-
387
- // Check if Morning Brief already exists for this user
388
- const existing = this.db.prepare(
389
- `SELECT id FROM cron_tasks WHERE user_id = ? AND name = 'Morning Brief' LIMIT 1`
390
- ).get(userId);
391
- if (existing) return null;
392
-
393
- const DAY_MS = 24 * 60 * 60 * 1000;
394
- const MORNING_BRIEF_PROMPT = `Good morning! Give me a brief summary to start my day:
395
- 1. What's on my calendar today?
396
- 2. Any important reminders or tasks due?
397
- 3. A quick weather update for my location.
398
- Keep it concise and actionable.`;
399
-
400
- // Calculate next 8:00 AM
401
- const now = new Date();
402
- const today8AM = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0, 0);
403
- const nextRun = now.getTime() < today8AM.getTime()
404
- ? today8AM.getTime()
405
- : today8AM.getTime() + DAY_MS;
406
-
407
- const id = crypto.randomUUID();
408
- const nowMs = Date.now();
409
-
410
- const result = this.db.prepare(`
411
- INSERT OR IGNORE INTO cron_tasks (id, name, prompt, session_id, user_id, next_run_at, interval_ms, recurring, enabled, created_at, last_run_at)
412
- VALUES (?, 'Morning Brief', ?, 'default', ?, ?, ?, 1, 0, ?, NULL)
413
- `).run(id, MORNING_BRIEF_PROMPT, userId, nextRun, DAY_MS, nowMs);
414
-
415
- if (result.changes > 0) {
416
- const runTime = new Date(nextRun);
417
- console.log(`[cron] created Morning Brief for user ${userId}, next run at ${runTime.toLocaleTimeString()} (disabled by default)`);
418
- return { id };
419
- }
420
-
421
- return null;
422
- }
423
-
424
357
  /**
425
358
  * Get heartbeat status for a user
426
359
  *
@@ -455,35 +388,18 @@ Keep it concise and actionable.`;
455
388
  async resetHeartbeat(userId) {
456
389
  if (!this.db || !userId) return null;
457
390
 
458
- const deleted = this.db.prepare(
391
+ this.db.prepare(
459
392
  "DELETE FROM cron_tasks WHERE user_id = ? AND name = 'heartbeat'"
460
393
  ).run(userId);
461
394
  console.log(`[cron] deleted existing heartbeat(s) for user ${userId}`);
462
395
 
463
- const jitter = Math.floor(Math.random() * HEARTBEAT_INTERVAL_MS);
464
- const now = Date.now();
465
- const id = crypto.randomUUID();
396
+ const result = await this.ensureHeartbeat(userId);
466
397
 
467
- this.db.prepare(`
468
- INSERT INTO cron_tasks (id, name, prompt, session_id, user_id, next_run_at, interval_ms, recurring, enabled, created_at, last_run_at)
469
- VALUES (?, 'heartbeat', ?, 'default', ?, ?, ?, 1, 1, ?, NULL)
470
- `).run(id, HEARTBEAT_PROMPT, userId, now + jitter, HEARTBEAT_INTERVAL_MS, now);
471
-
472
- console.log(`[cron] created new heartbeat for user ${userId}, first run in ${Math.round(jitter / 60000)}m`);
398
+ if (!result) return null;
473
399
 
474
- return {
475
- id,
476
- name: 'heartbeat',
477
- prompt: HEARTBEAT_PROMPT,
478
- userId,
479
- sessionId: 'default',
480
- nextRunAt: new Date(now + jitter),
481
- intervalMs: HEARTBEAT_INTERVAL_MS,
482
- recurring: true,
483
- enabled: true,
484
- createdAt: new Date(now),
485
- lastRunAt: null,
486
- };
400
+ // Return the full task object for the newly created heartbeat
401
+ const row = this.db.prepare('SELECT * FROM cron_tasks WHERE id = ?').get(result.id);
402
+ return row ? this._rowToTask(row) : null;
487
403
  }
488
404
 
489
405
  /**
package/storage/index.js CHANGED
@@ -5,9 +5,6 @@ export { CronStore } from './CronStore.js';
5
5
  export { SQLiteCronStore, parseInterval, HEARTBEAT_INTERVAL_MS, HEARTBEAT_PROMPT } from './SQLiteCronAdapter.js';
6
6
  export { TaskStore } from './TaskStore.js';
7
7
  export { SQLiteTaskStore } from './SQLiteTaskAdapter.js';
8
- // Backwards compatibility aliases
9
- export { TaskStore as GoalStore } from './TaskStore.js';
10
- export { SQLiteTaskStore as SQLiteGoalStore } from './SQLiteTaskAdapter.js';
11
8
  export { TriggerStore } from './TriggerStore.js';
12
9
  export { SQLiteTriggerStore } from './SQLiteTriggerAdapter.js';
13
10
  export { SQLiteMemoryStore } from './SQLiteMemoryAdapter.js';
package/tools/appgen.js CHANGED
@@ -57,14 +57,7 @@ export function cleanGeneratedCode(code) {
57
57
  if (!code) return { code: '', windowSize: { width: 800, height: 650 } };
58
58
 
59
59
  let cleanCode = code
60
- // Remove markdown code blocks
61
- .replace(/```javascript/gi, '')
62
- .replace(/```jsx/gi, '')
63
- .replace(/```js/gi, '')
64
- .replace(/```react/gi, '')
65
- .replace(/```typescript/gi, '')
66
- .replace(/```tsx/gi, '')
67
- .replace(/```/g, '')
60
+ .replace(/```(?:javascript|jsx|js|react|typescript|tsx)?/gi, '')
68
61
  // Remove HTML document wrappers
69
62
  .replace(/<html[^>]*>[\s\S]*<\/html>/gi, '')
70
63
  .replace(/<head[^>]*>[\s\S]*<\/head>/gi, '')
@@ -307,5 +300,3 @@ export const appgenTools = [
307
300
  }
308
301
  }
309
302
  ];
310
-
311
- export default appgenTools;
package/tools/browser.js CHANGED
@@ -616,21 +616,6 @@ export function createBrowserTools(screenshotUrlPattern = (filename) => `/api/ag
616
616
  pageSummary += `\n\nPage structure:\n${trimmed}`;
617
617
  }
618
618
 
619
- // Log to activity so Photos app can list the screenshot
620
- if (context?.databaseManager) {
621
- try {
622
- await context.databaseManager.logAgentActivity(
623
- context.dbConfig.dbType,
624
- context.dbConfig.db,
625
- context.dbConfig.connectionString,
626
- context.userID,
627
- { type: 'image_generation', prompt: `Screenshot: ${title}`, url: screenshotUrl, source: 'browser' }
628
- );
629
- } catch {
630
- /* best effort */
631
- }
632
- }
633
-
634
619
  // Return image JSON so frontend renders the screenshot inline
635
620
  return JSON.stringify({ type: 'image', url: screenshotUrl, prompt: pageSummary });
636
621
  } catch (err) {
package/tools/code.js CHANGED
@@ -52,20 +52,6 @@ export const codeTools = [
52
52
 
53
53
  await unlink(tmpFile).catch(() => {});
54
54
 
55
- if (context?.databaseManager) {
56
- try {
57
- await context.databaseManager.logAgentActivity(
58
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
59
- context.userID, {
60
- type: 'code_execution',
61
- code: input.code.slice(0, 500),
62
- output: (stdout || stderr || '').slice(0, 500),
63
- success: !stderr
64
- }
65
- );
66
- } catch (e) { /* best effort */ }
67
- }
68
-
69
55
  if (stderr) {
70
56
  return `Stderr:\n${stderr}\n\nStdout:\n${stdout}`;
71
57
  }
@@ -74,20 +60,6 @@ export const codeTools = [
74
60
  } catch (err) {
75
61
  await unlink(tmpFile).catch(() => {});
76
62
 
77
- if (context?.databaseManager) {
78
- try {
79
- await context.databaseManager.logAgentActivity(
80
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
81
- context.userID, {
82
- type: 'code_execution',
83
- code: input.code.slice(0, 500),
84
- output: (err.stderr || err.message || '').slice(0, 500),
85
- success: false
86
- }
87
- );
88
- } catch (e) { /* best effort */ }
89
- }
90
-
91
63
  if (err.killed) {
92
64
  return "Error: code execution timed out (10s limit)";
93
65
  }
package/tools/images.js CHANGED
@@ -176,16 +176,6 @@ export const imageTools = [
176
176
  return result.error;
177
177
  }
178
178
 
179
- // Log to activity for persistence
180
- if (context?.databaseManager) {
181
- try {
182
- await context.databaseManager.logAgentActivity(
183
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
184
- context.userID, { type: 'image_generation', prompt: input.prompt, url: result.url, source: 'agent' }
185
- );
186
- } catch (e) { /* best effort */ }
187
- }
188
-
189
179
  return JSON.stringify({ type: 'image', url: result.url, prompt: input.prompt });
190
180
  },
191
181
  },
package/tools/index.js CHANGED
@@ -13,9 +13,9 @@ import { imageTools } from './images.js';
13
13
  import { weatherTools } from './weather.js';
14
14
  import { notifyTools } from './notify.js';
15
15
  import { browserTools, createBrowserTools } from './browser.js';
16
- import { taskTools, goalTools } from './tasks.js';
16
+ import { taskTools } from './tasks.js';
17
17
  import { triggerTools } from './triggers.js';
18
- import { jobTools, cronTools } from './jobs.js';
18
+ import { jobTools } from './jobs.js';
19
19
  import { eventTools } from './events.js';
20
20
  import { appgenTools } from './appgen.js';
21
21
 
@@ -88,10 +88,8 @@ export {
88
88
  browserTools,
89
89
  createBrowserTools,
90
90
  taskTools,
91
- goalTools, // backwards compatibility alias
92
91
  triggerTools,
93
92
  jobTools,
94
- cronTools, // backwards compatibility alias
95
93
  eventTools,
96
94
  appgenTools,
97
95
  };
package/tools/jobs.js CHANGED
@@ -155,5 +155,3 @@ export const jobTools = [
155
155
  },
156
156
  ];
157
157
 
158
- // Backwards compatibility alias
159
- export const cronTools = jobTools;
package/tools/tasks.js CHANGED
@@ -400,5 +400,3 @@ export const taskTools = [
400
400
  },
401
401
  ];
402
402
 
403
- // Backwards compatibility: export goalTools as alias
404
- export const goalTools = taskTools;
package/tools/web.js CHANGED
@@ -70,15 +70,6 @@ export const webTools = [
70
70
  result += "\n\nSources:\n" + [...citations].slice(0, 5).map((url, i) => `${i + 1}. ${url}`).join("\n");
71
71
  }
72
72
 
73
- if (context?.databaseManager) {
74
- try {
75
- await context.databaseManager.logAgentActivity(
76
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
77
- context.userID, { type: 'web_search', query: input.query, provider: 'grok', resultPreview: result.slice(0, 300) }
78
- );
79
- } catch (e) { /* best effort */ }
80
- }
81
-
82
73
  return result || "No results found.";
83
74
  } else {
84
75
  const errText = await res.text();
@@ -120,15 +111,6 @@ export const webTools = [
120
111
 
121
112
  const result = parts.join("\n\n");
122
113
 
123
- if (context?.databaseManager) {
124
- try {
125
- await context.databaseManager.logAgentActivity(
126
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
127
- context.userID, { type: 'web_search', query: input.query, provider: 'duckduckgo', resultPreview: parts.slice(0, 2).join('\n').slice(0, 300) }
128
- );
129
- } catch (e) { /* best effort */ }
130
- }
131
-
132
114
  return result;
133
115
  },
134
116
  },
@@ -176,15 +158,6 @@ export const webTools = [
176
158
  text = text.slice(0, maxChars) + `\n\n... [truncated, ${text.length} chars total]`;
177
159
  }
178
160
 
179
- if (context?.databaseManager) {
180
- try {
181
- await context.databaseManager.logAgentActivity(
182
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
183
- context.userID, { type: 'grokipedia_search', query: input.query, url }
184
- );
185
- } catch (e) { /* best effort */ }
186
- }
187
-
188
161
  return text;
189
162
  } catch (err) {
190
163
  return `Error looking up Grokipedia: ${err.message}`;
@@ -260,15 +233,6 @@ export const webTools = [
260
233
  .trim();
261
234
  }
262
235
 
263
- if (context?.databaseManager) {
264
- try {
265
- await context.databaseManager.logAgentActivity(
266
- context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
267
- context.userID, { type: 'web_fetch', url: input.url, status: res.status }
268
- );
269
- } catch (e) { /* best effort */ }
270
- }
271
-
272
236
  const maxChars = 8000;
273
237
  if (text.length > maxChars) {
274
238
  return text.slice(0, maxChars) + `\n\n... [truncated, ${text.length} chars total]`;
@@ -1,69 +0,0 @@
1
- /**
2
- * SQLiteSessionStore Usage Example
3
- *
4
- * Demonstrates how to use SQLite as a session storage backend
5
- * for the @dottie/agent library. Requires Node.js 22.5+.
6
- */
7
-
8
- import { createAgent, SQLiteSessionStore, coreTools } from '@dottie/agent';
9
-
10
- // Initialize SQLite session store
11
- const sessionStore = new SQLiteSessionStore();
12
- await sessionStore.init('./sessions.db', {
13
- // Optional: Fetch user preferences from your database
14
- prefsFetcher: async (userId) => {
15
- // Example: fetch from a user database
16
- return {
17
- agentName: 'Dottie',
18
- agentPersonality: 'helpful and concise',
19
- };
20
- },
21
- // Optional: Ensure user heartbeat for cron tasks
22
- heartbeatEnsurer: async (userId) => {
23
- // Example: update last_seen timestamp in user database
24
- console.log(`User ${userId} active`);
25
- return null;
26
- },
27
- });
28
-
29
- // Create agent with SQLite session storage
30
- const agent = createAgent({
31
- sessionStore,
32
- providers: {
33
- anthropic: { apiKey: process.env.ANTHROPIC_API_KEY },
34
- },
35
- tools: coreTools,
36
- });
37
-
38
- // Example 1: Create a new session
39
- const session = await agent.createSession('user-123', 'claude-sonnet-4-5', 'anthropic');
40
- console.log('Created session:', session.id);
41
-
42
- // Example 2: Chat with the agent
43
- for await (const event of agent.chat({
44
- sessionId: session.id,
45
- message: 'What can you help me with?',
46
- provider: 'anthropic',
47
- model: 'claude-sonnet-4-5',
48
- })) {
49
- if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
50
- process.stdout.write(event.delta.text);
51
- }
52
- }
53
- console.log('\n');
54
-
55
- // Example 3: List all sessions for a user
56
- const sessions = await sessionStore.listSessions('user-123');
57
- console.log('User sessions:', sessions);
58
-
59
- // Example 4: Get or create default session
60
- const defaultSession = await sessionStore.getOrCreateDefaultSession('user-123');
61
- console.log('Default session:', defaultSession.id);
62
-
63
- // Example 5: Clear session history
64
- await sessionStore.clearSession(session.id);
65
- console.log('Session cleared');
66
-
67
- // Example 6: Delete a session
68
- await sessionStore.deleteSession(session.id, 'user-123');
69
- console.log('Session deleted');
package/observer/index.js DELETED
@@ -1,164 +0,0 @@
1
- /**
2
- * Browser Observer — in-memory snapshot store and agent tool.
3
- *
4
- * The frontend pushes structured browser-state snapshots via POST /api/agent/observer.
5
- * The agent reads the latest snapshot via the `browser_observe` tool to understand
6
- * what the user is currently doing in the browser.
7
- */
8
-
9
- const SNAPSHOT_TTL_MS = 5 * 60 * 1000; // 5 minutes
10
-
11
- /** @type {Map<string, Object>} userID → { ...snapshot, receivedAt } */
12
- const snapshots = new Map();
13
-
14
- /**
15
- * Store the latest browser snapshot for a user.
16
- *
17
- * @param {string} userID - Authenticated user ID
18
- * @param {Object} snapshot - Structured browser state from the frontend
19
- */
20
- export function storeSnapshot(userID, snapshot) {
21
- snapshots.set(userID, { ...snapshot, receivedAt: Date.now() });
22
- }
23
-
24
- /**
25
- * Retrieve the latest snapshot for a user, or null if stale/missing.
26
- *
27
- * @param {string} userID - Authenticated user ID
28
- * @returns {Object|null} Snapshot with receivedAt, or null
29
- */
30
- export function getSnapshot(userID) {
31
- const entry = snapshots.get(userID);
32
- if (!entry) return null;
33
- if (Date.now() - entry.receivedAt > SNAPSHOT_TTL_MS) {
34
- snapshots.delete(userID);
35
- return null;
36
- }
37
- return entry;
38
- }
39
-
40
- /**
41
- * Remove a user's snapshot (cleanup on logout, etc.).
42
- *
43
- * @param {string} userID - Authenticated user ID
44
- */
45
- export function clearSnapshot(userID) {
46
- snapshots.delete(userID);
47
- }
48
-
49
- /**
50
- * Format a snapshot into plain-text for LLM consumption.
51
- *
52
- * @param {Object} snap - Snapshot object from the store
53
- * @param {boolean} includeActions - Whether to include recent actions
54
- * @returns {string} Human-readable state description
55
- */
56
- function formatSnapshot(snap, includeActions = true) {
57
- const ageSec = Math.round((Date.now() - snap.timestamp) / 1000);
58
- const lines = [];
59
-
60
- lines.push(`Browser state (${ageSec}s ago):`);
61
- lines.push('');
62
-
63
- // Windows
64
- if (snap.windows && snap.windows.length > 0) {
65
- lines.push(`Open apps (${snap.windowCount || snap.windows.length}):`);
66
- for (const w of snap.windows) {
67
- const focus = w.isFocused ? ' [focused]' : '';
68
- lines.push(` - ${w.app}${w.title ? ': ' + w.title : ''}${focus}`);
69
- }
70
- } else {
71
- lines.push('No apps open.');
72
- }
73
-
74
- if (snap.focusedApp) {
75
- lines.push(`Focused: ${snap.focusedApp}`);
76
- }
77
-
78
- // Docked panel
79
- if (snap.isDottieDocked) {
80
- lines.push('DotBot panel: docked (sidebar)');
81
- }
82
-
83
- // Input bar
84
- if (snap.isInputElevated) {
85
- lines.push(`Input bar: elevated${snap.inputValue ? ' — "' + snap.inputValue + '"' : ''}`);
86
- }
87
-
88
- // Voice
89
- if (snap.voiceState && snap.voiceState !== 'idle') {
90
- lines.push(`Voice: ${snap.voiceState}`);
91
- }
92
-
93
- // Streaming
94
- if (snap.isStreaming) {
95
- lines.push('Agent: streaming response');
96
- }
97
-
98
- // Last tool call
99
- if (snap.lastToolCall) {
100
- const tc = snap.lastToolCall;
101
- const tcAge = Math.round((Date.now() - tc.timestamp) / 1000);
102
- lines.push(`Last tool: ${tc.name} (${tc.status}, ${tcAge}s ago)`);
103
- }
104
-
105
- // Messages
106
- if (snap.messageCount > 0) {
107
- lines.push(`Messages in session: ${snap.messageCount}`);
108
- }
109
-
110
- // Provider/model
111
- if (snap.currentProvider || snap.currentModel) {
112
- lines.push(`Model: ${snap.currentProvider || '?'}/${snap.currentModel || '?'}`);
113
- }
114
-
115
- // Layout + dock
116
- lines.push(`Layout: ${snap.layoutMode || 'desktop'}`);
117
- if (snap.dockApps && snap.dockApps.length > 0) {
118
- lines.push(`Dock: ${snap.dockApps.join(', ')}`);
119
- }
120
-
121
- // Viewport
122
- if (snap.viewport) {
123
- lines.push(`Viewport: ${snap.viewport.width}x${snap.viewport.height}`);
124
- }
125
-
126
- // Recent actions
127
- if (includeActions && snap.recentActions && snap.recentActions.length > 0) {
128
- lines.push('');
129
- lines.push('Recent actions:');
130
- for (const a of snap.recentActions) {
131
- const aAge = Math.round((Date.now() - a.timestamp) / 1000);
132
- const detail = a.app ? ` (${a.app})` : a.tool ? ` (${a.tool})` : '';
133
- lines.push(` - ${a.action}${detail} — ${aAge}s ago`);
134
- }
135
- }
136
-
137
- return lines.join('\n');
138
- }
139
-
140
- /** Agent tool definitions for the browser observer. */
141
- export const observerTools = [
142
- {
143
- name: 'browser_observe',
144
- description:
145
- "See what the user is currently doing in Dottie OS — open apps, focused window, voice state, recent actions. " +
146
- "Call this when you need context about the user's current activity, or when they reference 'this', 'what I'm looking at', or 'current'.",
147
- parameters: {
148
- type: 'object',
149
- properties: {
150
- include_actions: {
151
- type: 'boolean',
152
- description: 'Include recent user actions (default true)',
153
- },
154
- },
155
- },
156
- execute: async (input, signal, context) => {
157
- if (!context?.userID) return 'Error: user context not available';
158
- const snap = getSnapshot(context.userID);
159
- if (!snap) return 'No browser state available. The user may have the tab in the background or just opened the page.';
160
- const includeActions = input.include_actions !== false;
161
- return formatSnapshot(snap, includeActions);
162
- },
163
- },
164
- ];