@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 +10 -0
- package/README.md +64 -12
- package/bin/dotbot.js +54 -92
- package/core/agent.js +1 -1
- package/core/cdp.js +5 -58
- package/index.js +0 -7
- package/package.json +1 -1
- package/storage/SQLiteCronAdapter.js +8 -92
- package/storage/index.js +0 -3
- package/tools/appgen.js +1 -10
- package/tools/browser.js +0 -15
- package/tools/code.js +0 -28
- package/tools/images.js +0 -10
- package/tools/index.js +2 -4
- package/tools/jobs.js +0 -2
- package/tools/tasks.js +0 -2
- package/tools/web.js +0 -36
- package/examples/sqlite-session-example.js +0 -69
- package/observer/index.js +0 -164
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.
|
|
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** |
|
|
30
|
+
| **Lines of Code** | **~11k** | 22k | 1M+ |
|
|
31
31
|
| **Tools** | **53** | ~10 | ~50 |
|
|
32
|
-
| **Dependencies** |
|
|
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.
|
|
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
|
-
| **
|
|
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
|
-
│
|
|
351
|
-
|
|
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
|
-
*
|
|
607
|
+
* Stream events from an agentLoop iterable to stdout.
|
|
608
|
+
* Handles thinking markers, text deltas, tool status, and errors.
|
|
608
609
|
*
|
|
609
|
-
* @param {
|
|
610
|
-
* @
|
|
610
|
+
* @param {AsyncIterable<Object>} events - Async iterable of agentLoop events
|
|
611
|
+
* @returns {Promise<string>} Accumulated assistant text content
|
|
611
612
|
*/
|
|
612
|
-
async function
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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
|
|
464
|
-
const now = Date.now();
|
|
465
|
-
const id = crypto.randomUUID();
|
|
396
|
+
const result = await this.ensureHeartbeat(userId);
|
|
466
397
|
|
|
467
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
|
16
|
+
import { taskTools } from './tasks.js';
|
|
17
17
|
import { triggerTools } from './triggers.js';
|
|
18
|
-
import { jobTools
|
|
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
package/tools/tasks.js
CHANGED
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
|
-
];
|