@stevederico/dotbot 0.16.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +380 -0
  3. package/bin/dotbot.js +461 -0
  4. package/core/agent.js +779 -0
  5. package/core/compaction.js +261 -0
  6. package/core/cron_handler.js +262 -0
  7. package/core/events.js +229 -0
  8. package/core/failover.js +193 -0
  9. package/core/gptoss_tool_parser.js +173 -0
  10. package/core/init.js +154 -0
  11. package/core/normalize.js +324 -0
  12. package/core/trigger_handler.js +148 -0
  13. package/docs/core.md +103 -0
  14. package/docs/protected-files.md +59 -0
  15. package/examples/sqlite-session-example.js +69 -0
  16. package/index.js +341 -0
  17. package/observer/index.js +164 -0
  18. package/package.json +42 -0
  19. package/storage/CronStore.js +145 -0
  20. package/storage/EventStore.js +71 -0
  21. package/storage/MemoryStore.js +175 -0
  22. package/storage/MongoAdapter.js +291 -0
  23. package/storage/MongoCronAdapter.js +347 -0
  24. package/storage/MongoTaskAdapter.js +242 -0
  25. package/storage/MongoTriggerAdapter.js +158 -0
  26. package/storage/SQLiteAdapter.js +382 -0
  27. package/storage/SQLiteCronAdapter.js +562 -0
  28. package/storage/SQLiteEventStore.js +300 -0
  29. package/storage/SQLiteMemoryAdapter.js +240 -0
  30. package/storage/SQLiteTaskAdapter.js +419 -0
  31. package/storage/SQLiteTriggerAdapter.js +262 -0
  32. package/storage/SessionStore.js +149 -0
  33. package/storage/TaskStore.js +100 -0
  34. package/storage/TriggerStore.js +90 -0
  35. package/storage/cron_constants.js +48 -0
  36. package/storage/index.js +21 -0
  37. package/tools/appgen.js +311 -0
  38. package/tools/browser.js +634 -0
  39. package/tools/code.js +101 -0
  40. package/tools/events.js +145 -0
  41. package/tools/files.js +201 -0
  42. package/tools/images.js +253 -0
  43. package/tools/index.js +97 -0
  44. package/tools/jobs.js +159 -0
  45. package/tools/memory.js +332 -0
  46. package/tools/messages.js +135 -0
  47. package/tools/notify.js +42 -0
  48. package/tools/tasks.js +404 -0
  49. package/tools/triggers.js +159 -0
  50. package/tools/weather.js +82 -0
  51. package/tools/web.js +283 -0
  52. package/utils/providers.js +136 -0
@@ -0,0 +1,164 @@
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
+ ];
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@stevederico/dotbot",
3
+ "version": "0.16.0",
4
+ "description": "AI agent CLI and library for Node.js — streaming, multi-provider, tool execution, autonomous tasks",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "dotbot": "./bin/dotbot.js"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./core/*": "./core/*.js",
13
+ "./tools/*": "./tools/*.js",
14
+ "./storage/*": "./storage/*.js",
15
+ "./utils/*": "./utils/*.js"
16
+ },
17
+ "keywords": [
18
+ "ai",
19
+ "agent",
20
+ "llm",
21
+ "anthropic",
22
+ "openai",
23
+ "xai",
24
+ "chatbot",
25
+ "autonomous",
26
+ "tasks",
27
+ "triggers"
28
+ ],
29
+ "author": "Steve Derico",
30
+ "license": "MIT",
31
+ "peerDependencies": {
32
+ "mongodb": "^6.0.0"
33
+ },
34
+ "dependencies": {
35
+ "playwright": "^1.58.2"
36
+ },
37
+ "devDependencies": {},
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/stevederico/dotbot.git"
41
+ }
42
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * CronStore Interface
3
+ *
4
+ * Abstract interface for scheduled task storage. Implementations must provide
5
+ * all methods defined here.
6
+ */
7
+ export class CronStore {
8
+ /**
9
+ * Initialize the cron store
10
+ *
11
+ * @param {Object} options - Store-specific initialization options
12
+ * @param {Function} options.onTaskFire - Callback when a task fires: (task) => Promise<void>
13
+ */
14
+ async init(options = {}) {
15
+ throw new Error('CronStore.init() must be implemented');
16
+ }
17
+
18
+ /**
19
+ * Stop the cron polling loop
20
+ */
21
+ stop() {
22
+ throw new Error('CronStore.stop() must be implemented');
23
+ }
24
+
25
+ /**
26
+ * Create a scheduled task
27
+ *
28
+ * @param {Object} params
29
+ * @param {string} params.name - Short task name
30
+ * @param {string} params.prompt - Message to inject when task fires
31
+ * @param {string} [params.sessionId] - Session to inject into
32
+ * @param {string} [params.userId] - Owner user ID
33
+ * @param {string} params.runAt - ISO 8601 datetime for first run
34
+ * @param {number} [params.intervalMs] - Repeat interval in milliseconds
35
+ * @param {boolean} [params.recurring] - Whether task repeats
36
+ * @param {string} [params.taskId] - Associated task ID
37
+ * @returns {Promise<Object>} Created task document
38
+ */
39
+ async createTask({ name, prompt, sessionId, userId, runAt, intervalMs, recurring, taskId }) {
40
+ throw new Error('CronStore.createTask() must be implemented');
41
+ }
42
+
43
+ /**
44
+ * List tasks for a session
45
+ *
46
+ * @param {string} [sessionId] - Session ID to filter by
47
+ * @returns {Promise<Array>} Task list sorted by next run time
48
+ */
49
+ async listTasks(sessionId) {
50
+ throw new Error('CronStore.listTasks() must be implemented');
51
+ }
52
+
53
+ /**
54
+ * List tasks for multiple session IDs
55
+ *
56
+ * @param {string[]} sessionIds - Array of session IDs
57
+ * @param {string} [userId] - User ID to filter by
58
+ * @returns {Promise<Array>} Task list sorted by next run time
59
+ */
60
+ async listTasksBySessionIds(sessionIds, userId) {
61
+ throw new Error('CronStore.listTasksBySessionIds() must be implemented');
62
+ }
63
+
64
+ /**
65
+ * Get a task by ID
66
+ *
67
+ * @param {string} id - Task document ID
68
+ * @returns {Promise<Object|null>} Task document or null
69
+ */
70
+ async getTask(id) {
71
+ throw new Error('CronStore.getTask() must be implemented');
72
+ }
73
+
74
+ /**
75
+ * Delete a task by its ID
76
+ *
77
+ * @param {string} id - Task document ID
78
+ * @returns {Promise<any>} Delete result
79
+ */
80
+ async deleteTask(id) {
81
+ throw new Error('CronStore.deleteTask() must be implemented');
82
+ }
83
+
84
+ /**
85
+ * Toggle a task's enabled/disabled state
86
+ *
87
+ * @param {string} id - Task document ID
88
+ * @param {boolean} enabled - Whether the task should be enabled
89
+ * @returns {Promise<any>} Update result
90
+ */
91
+ async toggleTask(id, enabled) {
92
+ throw new Error('CronStore.toggleTask() must be implemented');
93
+ }
94
+
95
+ /**
96
+ * Update a task's details
97
+ *
98
+ * @param {string} id - Task document ID
99
+ * @param {Object} updates - Fields to update
100
+ * @returns {Promise<any>} Update result
101
+ */
102
+ async updateTask(id, updates) {
103
+ throw new Error('CronStore.updateTask() must be implemented');
104
+ }
105
+
106
+ /**
107
+ * Ensure a single recurring heartbeat task exists for a user
108
+ *
109
+ * @param {string} userId - User ID
110
+ * @returns {Promise<Object|null>} Created task or null if already exists
111
+ */
112
+ async ensureHeartbeat(userId) {
113
+ throw new Error('CronStore.ensureHeartbeat() must be implemented');
114
+ }
115
+
116
+ /**
117
+ * Get heartbeat status for a user
118
+ *
119
+ * @param {string} userId - User ID
120
+ * @returns {Promise<Object|null>} Heartbeat task or null
121
+ */
122
+ async getHeartbeatStatus(userId) {
123
+ throw new Error('CronStore.getHeartbeatStatus() must be implemented');
124
+ }
125
+
126
+ /**
127
+ * Reset/update an existing heartbeat to use the latest prompt
128
+ *
129
+ * @param {string} userId - User ID
130
+ * @returns {Promise<Object|null>} New heartbeat task or null
131
+ */
132
+ async resetHeartbeat(userId) {
133
+ throw new Error('CronStore.resetHeartbeat() must be implemented');
134
+ }
135
+
136
+ /**
137
+ * Manually trigger the heartbeat task immediately
138
+ *
139
+ * @param {string} userId - User ID
140
+ * @returns {Promise<boolean>} True if heartbeat was fired
141
+ */
142
+ async triggerHeartbeatNow(userId) {
143
+ throw new Error('CronStore.triggerHeartbeatNow() must be implemented');
144
+ }
145
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * EventStore Interface
3
+ *
4
+ * Abstract interface for event/analytics storage. Implementations must provide
5
+ * all methods defined here.
6
+ */
7
+ export class EventStore {
8
+ /**
9
+ * Initialize the event store
10
+ *
11
+ * @param {Object} db - Database instance (implementation-specific)
12
+ * @param {Object} options - Store-specific initialization options
13
+ */
14
+ async init(db, options = {}) {
15
+ throw new Error('EventStore.init() must be implemented');
16
+ }
17
+
18
+ /**
19
+ * Log an event
20
+ *
21
+ * @param {Object} params
22
+ * @param {string} params.userId - User ID
23
+ * @param {string} params.type - Event type (message_sent, message_received, tool_call, task_created, task_completed, trigger_fired)
24
+ * @param {Object} [params.data] - Event-specific data (e.g., { tool: 'web_search' })
25
+ * @param {number} [params.timestamp] - Unix ms timestamp (defaults to now)
26
+ * @returns {Promise<Object>} Created event document
27
+ */
28
+ async logEvent({ userId, type, data, timestamp }) {
29
+ throw new Error('EventStore.logEvent() must be implemented');
30
+ }
31
+
32
+ /**
33
+ * Query events with filters
34
+ *
35
+ * @param {Object} params
36
+ * @param {string} params.userId - User ID
37
+ * @param {string} [params.type] - Filter by event type
38
+ * @param {string} [params.startDate] - ISO date start (inclusive)
39
+ * @param {string} [params.endDate] - ISO date end (inclusive)
40
+ * @param {number} [params.limit=100] - Max results
41
+ * @returns {Promise<Array>} Matching events
42
+ */
43
+ async query({ userId, type, startDate, endDate, limit }) {
44
+ throw new Error('EventStore.query() must be implemented');
45
+ }
46
+
47
+ /**
48
+ * Get aggregated usage statistics
49
+ *
50
+ * @param {Object} params
51
+ * @param {string} params.userId - User ID
52
+ * @param {string} [params.startDate] - ISO date start
53
+ * @param {string} [params.endDate] - ISO date end
54
+ * @param {string} [params.groupBy='type'] - Group by: type, day, week, month
55
+ * @returns {Promise<Object>} Summary statistics
56
+ */
57
+ async summary({ userId, startDate, endDate, groupBy }) {
58
+ throw new Error('EventStore.summary() must be implemented');
59
+ }
60
+
61
+ /**
62
+ * Delete events older than a given date
63
+ *
64
+ * @param {string} userId - User ID
65
+ * @param {string} beforeDate - ISO date cutoff
66
+ * @returns {Promise<Object>} Delete result with count
67
+ */
68
+ async deleteOldEvents(userId, beforeDate) {
69
+ throw new Error('EventStore.deleteOldEvents() must be implemented');
70
+ }
71
+ }
@@ -0,0 +1,175 @@
1
+ import crypto from 'crypto';
2
+ import { SessionStore } from './SessionStore.js';
3
+ import { defaultSystemPrompt } from './MongoAdapter.js';
4
+ import { toStandardFormat } from '../core/normalize.js';
5
+
6
+ /**
7
+ * In-memory SessionStore implementation for testing
8
+ */
9
+ export class MemorySessionStore extends SessionStore {
10
+ constructor() {
11
+ super();
12
+ this.sessions = new Map();
13
+ this.prefsFetcher = null;
14
+ this.systemPromptBuilder = defaultSystemPrompt;
15
+ this.heartbeatEnsurer = null;
16
+ }
17
+
18
+ async init(options = {}) {
19
+ this.prefsFetcher = options.prefsFetcher || null;
20
+ this.systemPromptBuilder = options.systemPromptBuilder || defaultSystemPrompt;
21
+ this.heartbeatEnsurer = options.heartbeatEnsurer || null;
22
+ console.log('[sessions] initialized with in-memory store');
23
+ }
24
+
25
+ async buildSystemPrompt(owner) {
26
+ const prefs = this.prefsFetcher ? await this.prefsFetcher(owner) : {};
27
+ return this.systemPromptBuilder(prefs);
28
+ }
29
+
30
+ async createSession(owner, model = 'gpt-oss:20b', provider = 'ollama') {
31
+ const session = {
32
+ id: crypto.randomUUID(),
33
+ owner,
34
+ title: '',
35
+ messages: [{ role: 'system', content: await this.buildSystemPrompt(owner) }],
36
+ model,
37
+ provider,
38
+ createdAt: new Date(),
39
+ updatedAt: new Date(),
40
+ };
41
+ this.sessions.set(session.id, session);
42
+ return session;
43
+ }
44
+
45
+ async getOrCreateDefaultSession(owner) {
46
+ // Find most recent session for this owner
47
+ const userSessions = Array.from(this.sessions.values())
48
+ .filter(s => s.owner === owner)
49
+ .sort((a, b) => b.updatedAt - a.updatedAt);
50
+
51
+ let session = userSessions[0];
52
+ if (!session) {
53
+ session = await this.createSession(owner);
54
+ } else {
55
+ // Refresh system prompt timestamp
56
+ session.messages[0] = { role: 'system', content: await this.buildSystemPrompt(owner) };
57
+ }
58
+ if (this.heartbeatEnsurer) {
59
+ this.heartbeatEnsurer(owner).catch((err) => {
60
+ console.error(`[session] failed to ensure heartbeat for ${owner}:`, err.message);
61
+ });
62
+ }
63
+ return session;
64
+ }
65
+
66
+ async getSession(sessionId, owner) {
67
+ const session = this.sessions.get(sessionId);
68
+ if (!session || session.owner !== owner) return null;
69
+
70
+ // Refresh system prompt timestamp
71
+ session.messages[0] = { role: 'system', content: await this.buildSystemPrompt(owner) };
72
+ return session;
73
+ }
74
+
75
+ async getSessionInternal(sessionId) {
76
+ const session = this.sessions.get(sessionId);
77
+ if (!session) return null;
78
+
79
+ session.messages[0] = { role: 'system', content: await this.buildSystemPrompt(session.owner) };
80
+ return session;
81
+ }
82
+
83
+ /**
84
+ * Save session with normalized messages.
85
+ * Converts any provider-specific message formats to standard format before persisting.
86
+ *
87
+ * @param {string} sessionId - Session UUID
88
+ * @param {Array} messages - Messages (provider-specific or standard format)
89
+ * @param {string} model - Model identifier
90
+ * @param {string} [provider] - Provider name
91
+ */
92
+ async saveSession(sessionId, messages, model, provider) {
93
+ const session = this.sessions.get(sessionId);
94
+ if (!session) throw new Error(`Session ${sessionId} not found`);
95
+
96
+ session.messages = toStandardFormat(messages);
97
+ session.model = model;
98
+ session.updatedAt = new Date();
99
+ if (provider) session.provider = provider;
100
+
101
+ // Auto-populate title from first user message if empty
102
+ if (!session.title) {
103
+ const firstUserMsg = messages.find((m) => m.role === 'user');
104
+ if (firstUserMsg) {
105
+ session.title = firstUserMsg.content.slice(0, 60);
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Add a message to a session, normalizing to standard format before saving.
112
+ *
113
+ * @param {string} sessionId - Session UUID
114
+ * @param {Object} message - Message object (any provider format)
115
+ * @returns {Promise<Object>} Updated session
116
+ */
117
+ async addMessage(sessionId, message) {
118
+ const session = await this.getSessionInternal(sessionId);
119
+ if (!session) throw new Error(`Session ${sessionId} not found`);
120
+ if (!message._ts) message._ts = Date.now();
121
+ const normalized = toStandardFormat([message]);
122
+ session.messages.push(...normalized);
123
+ await this.saveSession(sessionId, session.messages, session.model);
124
+ return session;
125
+ }
126
+
127
+ async setModel(sessionId, model) {
128
+ const session = this.sessions.get(sessionId);
129
+ if (session) {
130
+ session.model = model;
131
+ session.updatedAt = new Date();
132
+ }
133
+ }
134
+
135
+ async setProvider(sessionId, provider) {
136
+ const session = this.sessions.get(sessionId);
137
+ if (session) {
138
+ session.provider = provider;
139
+ session.updatedAt = new Date();
140
+ }
141
+ }
142
+
143
+ async clearSession(sessionId) {
144
+ const session = this.sessions.get(sessionId);
145
+ if (session) {
146
+ session.messages = [{ role: 'system', content: await this.buildSystemPrompt(session.owner) }];
147
+ session.updatedAt = new Date();
148
+ }
149
+ }
150
+
151
+ async listSessions(owner) {
152
+ return Array.from(this.sessions.values())
153
+ .filter(s => s.owner === owner)
154
+ .sort((a, b) => b.updatedAt - a.updatedAt)
155
+ .slice(0, 50)
156
+ .map(s => ({
157
+ id: s.id,
158
+ title: s.title || '',
159
+ model: s.model,
160
+ provider: s.provider || 'ollama',
161
+ createdAt: s.createdAt,
162
+ updatedAt: s.updatedAt,
163
+ messageCount: s.messages.length,
164
+ }));
165
+ }
166
+
167
+ async deleteSession(sessionId, owner) {
168
+ const session = this.sessions.get(sessionId);
169
+ if (session && session.owner === owner) {
170
+ this.sessions.delete(sessionId);
171
+ return { deletedCount: 1 };
172
+ }
173
+ return { deletedCount: 0 };
174
+ }
175
+ }