@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.
- package/CHANGELOG.md +136 -0
- package/README.md +380 -0
- package/bin/dotbot.js +461 -0
- package/core/agent.js +779 -0
- package/core/compaction.js +261 -0
- package/core/cron_handler.js +262 -0
- package/core/events.js +229 -0
- package/core/failover.js +193 -0
- package/core/gptoss_tool_parser.js +173 -0
- package/core/init.js +154 -0
- package/core/normalize.js +324 -0
- package/core/trigger_handler.js +148 -0
- package/docs/core.md +103 -0
- package/docs/protected-files.md +59 -0
- package/examples/sqlite-session-example.js +69 -0
- package/index.js +341 -0
- package/observer/index.js +164 -0
- package/package.json +42 -0
- package/storage/CronStore.js +145 -0
- package/storage/EventStore.js +71 -0
- package/storage/MemoryStore.js +175 -0
- package/storage/MongoAdapter.js +291 -0
- package/storage/MongoCronAdapter.js +347 -0
- package/storage/MongoTaskAdapter.js +242 -0
- package/storage/MongoTriggerAdapter.js +158 -0
- package/storage/SQLiteAdapter.js +382 -0
- package/storage/SQLiteCronAdapter.js +562 -0
- package/storage/SQLiteEventStore.js +300 -0
- package/storage/SQLiteMemoryAdapter.js +240 -0
- package/storage/SQLiteTaskAdapter.js +419 -0
- package/storage/SQLiteTriggerAdapter.js +262 -0
- package/storage/SessionStore.js +149 -0
- package/storage/TaskStore.js +100 -0
- package/storage/TriggerStore.js +90 -0
- package/storage/cron_constants.js +48 -0
- package/storage/index.js +21 -0
- package/tools/appgen.js +311 -0
- package/tools/browser.js +634 -0
- package/tools/code.js +101 -0
- package/tools/events.js +145 -0
- package/tools/files.js +201 -0
- package/tools/images.js +253 -0
- package/tools/index.js +97 -0
- package/tools/jobs.js +159 -0
- package/tools/memory.js +332 -0
- package/tools/messages.js +135 -0
- package/tools/notify.js +42 -0
- package/tools/tasks.js +404 -0
- package/tools/triggers.js +159 -0
- package/tools/weather.js +82 -0
- package/tools/web.js +283 -0
- package/utils/providers.js +136 -0
package/tools/appgen.js
ADDED
|
@@ -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;
|