@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,311 @@
1
+ /**
2
+ * App Generation Tools
3
+ *
4
+ * Generate React components from natural language prompts using AI providers.
5
+ * Returns executable JavaScript code using React.createElement() (no JSX).
6
+ */
7
+
8
+ import { AI_PROVIDERS } from '../utils/providers.js';
9
+
10
+ /**
11
+ * System prompt for app generation.
12
+ * Instructs AI to generate React components with specific constraints.
13
+ */
14
+ export const APP_GENERATION_PROMPT = `Create a React component named "App" for a desktop window.
15
+
16
+ CRITICAL: Use React.createElement() - NO JSX allowed. JSX like <div> will fail.
17
+
18
+ Requirements:
19
+ - FIRST LINE: Window size directive // @window WIDTHxHEIGHT
20
+ - Choose size based on app type:
21
+ - Small utilities/timers: 400x500 to 500x600
22
+ - Calculators/forms: 500x600 to 650x700
23
+ - Lists/content apps: 650x700 to 800x700
24
+ - Dashboards/editors: 900x750 to 1200x800
25
+ - Use: useState, useEffect, useRef (NOT React.useState)
26
+ - Styling: Tailwind classes via className with DARK MODE support
27
+ - Dark mode: Use dark: prefix for all colors
28
+ - Backgrounds: bg-white dark:bg-gray-800
29
+ - Text: text-gray-900 dark:text-white
30
+ - Borders: border-gray-200 dark:border-gray-700
31
+ - Buttons: bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700
32
+ - NEVER use alert(), prompt(), confirm() or any native browser dialogs
33
+ - For user input, build inline forms/modals using React state and createElement
34
+ - Return ONLY JavaScript code
35
+ - NO markdown, NO explanations, NO import/export
36
+
37
+ Example with window size and dark mode:
38
+ // @window 500x600
39
+ const App = () => {
40
+ const [count, setCount] = useState(0);
41
+ return React.createElement('div', { className: 'p-6 bg-white dark:bg-gray-800 h-full' },
42
+ React.createElement('h1', { className: 'text-2xl font-bold text-gray-900 dark:text-white mb-4' }, 'Counter App'),
43
+ React.createElement('button', {
44
+ onClick: () => setCount(count + 1),
45
+ className: 'bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white px-4 py-2 rounded'
46
+ }, 'Count: ', count)
47
+ );
48
+ };`;
49
+
50
+ /**
51
+ * Clean generated code by removing markdown, normalizing hooks, extracting window size.
52
+ *
53
+ * @param {string} code - Raw AI-generated code
54
+ * @returns {{ code: string, windowSize: { width: number, height: number } }}
55
+ */
56
+ export function cleanGeneratedCode(code) {
57
+ if (!code) return { code: '', windowSize: { width: 800, height: 650 } };
58
+
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, '')
68
+ // Remove HTML document wrappers
69
+ .replace(/<html[^>]*>[\s\S]*<\/html>/gi, '')
70
+ .replace(/<head[^>]*>[\s\S]*<\/head>/gi, '')
71
+ .replace(/<body[^>]*>([\s\S]*)<\/body>/gi, '$1')
72
+ .replace(/<script[^>]*>[\s\S]*<\/script>/gi, '')
73
+ .replace(/<style[^>]*>[\s\S]*<\/style>/gi, '')
74
+ .replace(/<!DOCTYPE[^>]*>/gi, '')
75
+ .trim();
76
+
77
+ // Find component start
78
+ const componentPatterns = [
79
+ /const\s+App\s*=/,
80
+ /function\s+App\s*\(/,
81
+ /export\s+default\s+function/,
82
+ /export\s+function/
83
+ ];
84
+
85
+ for (const pattern of componentPatterns) {
86
+ const match = cleanCode.match(pattern);
87
+ if (match && match.index > 0) {
88
+ cleanCode = cleanCode.substring(match.index);
89
+ break;
90
+ }
91
+ }
92
+
93
+ // Normalize React hooks (remove React. prefix)
94
+ cleanCode = cleanCode
95
+ .replace(/React\.useState/g, 'useState')
96
+ .replace(/React\.useEffect/g, 'useEffect')
97
+ .replace(/React\.useRef/g, 'useRef')
98
+ .replace(/React\.useCallback/g, 'useCallback')
99
+ .replace(/React\.useMemo/g, 'useMemo')
100
+ .replace(/React\.useContext/g, 'useContext')
101
+ // Remove full-screen classes that break windowed layout
102
+ .replace(/min-h-screen/g, '')
103
+ .replace(/h-screen/g, '')
104
+ .replace(/w-screen/g, '')
105
+ .replace(/fixed\s+inset-0/g, '');
106
+
107
+ // Extract window size directive
108
+ const windowMatch = cleanCode.match(/\/\/\s*@window\s+(\d+)x(\d+)/i);
109
+ let windowSize = { width: 800, height: 650 };
110
+
111
+ if (windowMatch) {
112
+ windowSize = {
113
+ width: parseInt(windowMatch[1]),
114
+ height: parseInt(windowMatch[2])
115
+ };
116
+ cleanCode = cleanCode.replace(/\/\/\s*@window\s+\d+x\d+\n?/i, '').trim();
117
+ }
118
+
119
+ return { code: cleanCode, windowSize };
120
+ }
121
+
122
+ /**
123
+ * Validate that code looks like a valid React component.
124
+ *
125
+ * @param {string} code - Generated code to validate
126
+ * @returns {{ valid: boolean, error?: string }}
127
+ */
128
+ export function validateGeneratedCode(code) {
129
+ if (!code || typeof code !== 'string') {
130
+ return { valid: false, error: 'Generated code is empty or invalid' };
131
+ }
132
+
133
+ const hasHTMLDoctype = code.includes('<!DOCTYPE') || code.includes('<html') || code.includes('<body');
134
+ if (hasHTMLDoctype) {
135
+ return { valid: false, error: 'Generated code appears to be HTML document instead of React component' };
136
+ }
137
+
138
+ const hasReactComponent = code.includes('=>') || code.includes('function') || code.includes('const') || code.includes('return');
139
+ if (!hasReactComponent) {
140
+ return { valid: false, error: 'Generated code is not valid JavaScript' };
141
+ }
142
+
143
+ return { valid: true };
144
+ }
145
+
146
+ /**
147
+ * Extract app name from prompt (first 2-3 words).
148
+ *
149
+ * @param {string} prompt - User prompt
150
+ * @returns {string}
151
+ */
152
+ export function extractAppName(prompt) {
153
+ const words = prompt.trim().split(/\s+/);
154
+ // Skip action words like "create", "build", "make"
155
+ const actionWords = ['create', 'build', 'make', 'generate', 'write', 'design', 'a', 'an', 'the'];
156
+ const filtered = words.filter(w => !actionWords.includes(w.toLowerCase()));
157
+ return filtered.slice(0, 2).join(' ') || 'Generated App';
158
+ }
159
+
160
+ /**
161
+ * App generation tools array
162
+ */
163
+ export const appgenTools = [
164
+ {
165
+ name: 'app_generate',
166
+ description: 'Generate a React app component from a natural language description. Returns executable JavaScript code that uses React.createElement() (no JSX). The code can be executed in a browser with React and hooks in scope.',
167
+ parameters: {
168
+ type: 'object',
169
+ properties: {
170
+ prompt: {
171
+ type: 'string',
172
+ description: 'Description of the app to generate (e.g., "a todo list app with dark mode" or "a pomodoro timer with sound alerts")'
173
+ },
174
+ provider: {
175
+ type: 'string',
176
+ description: 'AI provider to use for generation (anthropic, openai, xai, cerebras, ollama). Defaults to xai.',
177
+ enum: ['anthropic', 'openai', 'xai', 'cerebras', 'ollama']
178
+ },
179
+ model: {
180
+ type: 'string',
181
+ description: 'Specific model to use. If not provided, uses the provider default.'
182
+ }
183
+ },
184
+ required: ['prompt']
185
+ },
186
+ execute: async (input, signal, context) => {
187
+ const { prompt, provider: providerId = 'xai', model: modelOverride } = input;
188
+
189
+ if (!prompt || !prompt.trim()) {
190
+ return JSON.stringify({ success: false, error: 'Prompt is required' });
191
+ }
192
+
193
+ // Get provider config
194
+ const provider = AI_PROVIDERS[providerId];
195
+ if (!provider) {
196
+ return JSON.stringify({ success: false, error: `Unknown provider: ${providerId}` });
197
+ }
198
+
199
+ // Get API key from context.providers
200
+ const apiKey = context?.providers?.[providerId]?.apiKey;
201
+ if (!provider.local && !apiKey) {
202
+ return JSON.stringify({ success: false, error: `No API key configured for ${provider.name}` });
203
+ }
204
+
205
+ const model = modelOverride || provider.defaultModel;
206
+
207
+ // Build messages based on provider (Anthropic has no system role)
208
+ const isAnthropic = providerId === 'anthropic';
209
+ const messages = isAnthropic
210
+ ? [{ role: 'user', content: `${APP_GENERATION_PROMPT}\n\nCreate a desktop app: ${prompt}` }]
211
+ : [{ role: 'system', content: APP_GENERATION_PROMPT }, { role: 'user', content: `Create a desktop app: ${prompt}` }];
212
+
213
+ try {
214
+ // Make API request with 60s timeout
215
+ const controller = new AbortController();
216
+ const timeoutId = setTimeout(() => controller.abort(), 60000);
217
+
218
+ // Combine user signal with timeout
219
+ if (signal) {
220
+ signal.addEventListener('abort', () => controller.abort());
221
+ }
222
+
223
+ const requestBody = provider.formatRequest(messages, model);
224
+ const headers = provider.headers(apiKey);
225
+
226
+ const response = await fetch(`${provider.apiUrl}${provider.endpoint}`, {
227
+ method: 'POST',
228
+ headers,
229
+ body: JSON.stringify(requestBody),
230
+ signal: controller.signal
231
+ });
232
+
233
+ clearTimeout(timeoutId);
234
+
235
+ if (!response.ok) {
236
+ const errorText = await response.text();
237
+ return JSON.stringify({
238
+ success: false,
239
+ error: `${provider.name} API error: ${response.status} - ${errorText.slice(0, 200)}`
240
+ });
241
+ }
242
+
243
+ const data = await response.json();
244
+ const generatedCode = provider.formatResponse(data);
245
+
246
+ if (!generatedCode) {
247
+ return JSON.stringify({ success: false, error: 'No code generated' });
248
+ }
249
+
250
+ // Clean and validate code
251
+ const { code: cleanCode, windowSize } = cleanGeneratedCode(generatedCode);
252
+ const validation = validateGeneratedCode(cleanCode);
253
+
254
+ if (!validation.valid) {
255
+ return JSON.stringify({ success: false, error: validation.error });
256
+ }
257
+
258
+ const appName = extractAppName(prompt);
259
+
260
+ return JSON.stringify({
261
+ success: true,
262
+ code: cleanCode,
263
+ appName,
264
+ windowSize,
265
+ provider: providerId,
266
+ model
267
+ });
268
+
269
+ } catch (error) {
270
+ if (error.name === 'AbortError') {
271
+ // Could be timeout or user cancellation
272
+ return JSON.stringify({ success: false, error: 'Request timed out or was cancelled' });
273
+ }
274
+ return JSON.stringify({ success: false, error: error.message });
275
+ }
276
+ }
277
+ },
278
+
279
+ {
280
+ name: 'app_validate',
281
+ description: 'Validate that generated React component code is syntactically correct and follows the expected structure.',
282
+ parameters: {
283
+ type: 'object',
284
+ properties: {
285
+ code: {
286
+ type: 'string',
287
+ description: 'The generated React component code to validate'
288
+ }
289
+ },
290
+ required: ['code']
291
+ },
292
+ execute: async (input) => {
293
+ const { code } = input;
294
+ const validation = validateGeneratedCode(code);
295
+
296
+ if (!validation.valid) {
297
+ return JSON.stringify({ valid: false, error: validation.error });
298
+ }
299
+
300
+ // Additional syntax check - try to parse as function
301
+ try {
302
+ new Function('React', 'useState', 'useEffect', 'useRef', 'useCallback', 'useMemo', code);
303
+ return JSON.stringify({ valid: true });
304
+ } catch (syntaxError) {
305
+ return JSON.stringify({ valid: false, error: `Syntax error: ${syntaxError.message}` });
306
+ }
307
+ }
308
+ }
309
+ ];
310
+
311
+ export default appgenTools;