portos-ai-toolkit 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +76 -0
- package/src/client/api.js +102 -0
- package/src/client/components/ProviderDropdown.jsx +39 -0
- package/src/client/components/index.js +5 -0
- package/src/client/hooks/index.js +6 -0
- package/src/client/hooks/useProviders.js +96 -0
- package/src/client/hooks/useRuns.js +94 -0
- package/src/client/index.js +11 -0
- package/src/client/pages/AIProviders.jsx +665 -0
- package/src/index.js +8 -0
- package/src/server/index.d.ts +87 -0
- package/src/server/index.js +95 -0
- package/src/server/prompts.js +234 -0
- package/src/server/providers.js +253 -0
- package/src/server/providers.test.js +120 -0
- package/src/server/routes/prompts.js +96 -0
- package/src/server/routes/providers.js +105 -0
- package/src/server/routes/runs.js +157 -0
- package/src/server/runner.js +475 -0
- package/src/server/validation.js +51 -0
- package/src/shared/constants.js +26 -0
- package/src/shared/index.js +5 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, readdir, rm } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join, extname } from 'path';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a runner service with configurable storage and hooks
|
|
9
|
+
*/
|
|
10
|
+
export function createRunnerService(config = {}) {
|
|
11
|
+
const {
|
|
12
|
+
dataDir = './data',
|
|
13
|
+
runsDir = 'runs',
|
|
14
|
+
screenshotsDir = './data/screenshots',
|
|
15
|
+
providerService,
|
|
16
|
+
hooks = {},
|
|
17
|
+
maxConcurrentRuns = 5
|
|
18
|
+
} = config;
|
|
19
|
+
|
|
20
|
+
const RUNS_PATH = join(dataDir, runsDir);
|
|
21
|
+
const activeRuns = new Map();
|
|
22
|
+
|
|
23
|
+
async function ensureRunsDir() {
|
|
24
|
+
if (!existsSync(RUNS_PATH)) {
|
|
25
|
+
await mkdir(RUNS_PATH, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get MIME type from file extension
|
|
31
|
+
*/
|
|
32
|
+
function getMimeType(filepath) {
|
|
33
|
+
const ext = extname(filepath).toLowerCase();
|
|
34
|
+
const mimeTypes = {
|
|
35
|
+
'.png': 'image/png',
|
|
36
|
+
'.jpg': 'image/jpeg',
|
|
37
|
+
'.jpeg': 'image/jpeg',
|
|
38
|
+
'.gif': 'image/gif',
|
|
39
|
+
'.webp': 'image/webp'
|
|
40
|
+
};
|
|
41
|
+
return mimeTypes[ext] || 'image/png';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load an image as base64 data URL
|
|
46
|
+
*/
|
|
47
|
+
async function loadImageAsBase64(imagePath) {
|
|
48
|
+
const fullPath = imagePath.startsWith('/') ? imagePath : join(screenshotsDir, imagePath);
|
|
49
|
+
|
|
50
|
+
if (!existsSync(fullPath)) {
|
|
51
|
+
throw new Error(`Image not found: ${fullPath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const buffer = await readFile(fullPath);
|
|
55
|
+
const mimeType = getMimeType(fullPath);
|
|
56
|
+
return `data:${mimeType};base64,${buffer.toString('base64')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Safe JSON parse with fallback
|
|
61
|
+
*/
|
|
62
|
+
function safeJsonParse(str, fallback = {}) {
|
|
63
|
+
if (typeof str !== 'string') {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parsed = JSON.parse(str);
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
/**
|
|
73
|
+
* Create a new run
|
|
74
|
+
*/
|
|
75
|
+
async createRun(options) {
|
|
76
|
+
const {
|
|
77
|
+
providerId,
|
|
78
|
+
model,
|
|
79
|
+
prompt,
|
|
80
|
+
workspacePath = process.cwd(),
|
|
81
|
+
workspaceName = 'default',
|
|
82
|
+
timeout,
|
|
83
|
+
source = 'devtools'
|
|
84
|
+
} = options;
|
|
85
|
+
|
|
86
|
+
if (!providerService) {
|
|
87
|
+
throw new Error('Provider service not configured');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const provider = await providerService.getProviderById(providerId);
|
|
91
|
+
if (!provider) {
|
|
92
|
+
throw new Error('Provider not found');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!provider.enabled) {
|
|
96
|
+
throw new Error('Provider is disabled');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await ensureRunsDir();
|
|
100
|
+
|
|
101
|
+
const runId = uuidv4();
|
|
102
|
+
const runDir = join(RUNS_PATH, runId);
|
|
103
|
+
await mkdir(runDir);
|
|
104
|
+
|
|
105
|
+
const metadata = {
|
|
106
|
+
id: runId,
|
|
107
|
+
type: 'ai',
|
|
108
|
+
providerId,
|
|
109
|
+
providerName: provider.name,
|
|
110
|
+
model: model || provider.defaultModel,
|
|
111
|
+
workspacePath,
|
|
112
|
+
workspaceName,
|
|
113
|
+
source,
|
|
114
|
+
prompt: prompt.substring(0, 500),
|
|
115
|
+
startTime: new Date().toISOString(),
|
|
116
|
+
endTime: null,
|
|
117
|
+
duration: null,
|
|
118
|
+
exitCode: null,
|
|
119
|
+
success: null,
|
|
120
|
+
error: null,
|
|
121
|
+
outputSize: 0
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await writeFile(join(runDir, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
125
|
+
await writeFile(join(runDir, 'prompt.txt'), prompt);
|
|
126
|
+
await writeFile(join(runDir, 'output.txt'), '');
|
|
127
|
+
|
|
128
|
+
hooks.onRunCreated?.(metadata);
|
|
129
|
+
|
|
130
|
+
const effectiveTimeout = timeout || provider.timeout;
|
|
131
|
+
|
|
132
|
+
return { runId, runDir, provider, metadata, timeout: effectiveTimeout };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Execute a CLI run
|
|
137
|
+
*/
|
|
138
|
+
async executeCliRun(runId, provider, prompt, workspacePath, onData, onComplete, timeout) {
|
|
139
|
+
const runDir = join(RUNS_PATH, runId);
|
|
140
|
+
const outputPath = join(runDir, 'output.txt');
|
|
141
|
+
const metadataPath = join(runDir, 'metadata.json');
|
|
142
|
+
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
let output = '';
|
|
145
|
+
|
|
146
|
+
// Build command with args
|
|
147
|
+
const args = [...(provider.args || []), prompt];
|
|
148
|
+
console.log(`🚀 Executing CLI: ${provider.command} ${provider.args?.join(' ') || ''}`);
|
|
149
|
+
|
|
150
|
+
const childProcess = spawn(provider.command, args, {
|
|
151
|
+
cwd: workspacePath,
|
|
152
|
+
env: { ...process.env, ...provider.envVars },
|
|
153
|
+
shell: true
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
activeRuns.set(runId, childProcess);
|
|
157
|
+
hooks.onRunStarted?.({ runId, provider: provider.name, model: provider.defaultModel });
|
|
158
|
+
|
|
159
|
+
// Set timeout
|
|
160
|
+
const timeoutHandle = setTimeout(() => {
|
|
161
|
+
if (childProcess && !childProcess.killed) {
|
|
162
|
+
console.log(`⏱️ Run ${runId} timed out after ${timeout}ms`);
|
|
163
|
+
childProcess.kill('SIGTERM');
|
|
164
|
+
}
|
|
165
|
+
}, timeout);
|
|
166
|
+
|
|
167
|
+
childProcess.stdout?.on('data', (data) => {
|
|
168
|
+
const text = data.toString();
|
|
169
|
+
output += text;
|
|
170
|
+
onData?.(text);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
childProcess.stderr?.on('data', (data) => {
|
|
174
|
+
const text = data.toString();
|
|
175
|
+
output += text;
|
|
176
|
+
onData?.(text);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
childProcess.on('close', async (code) => {
|
|
180
|
+
clearTimeout(timeoutHandle);
|
|
181
|
+
activeRuns.delete(runId);
|
|
182
|
+
|
|
183
|
+
await writeFile(outputPath, output);
|
|
184
|
+
|
|
185
|
+
const metadata = safeJsonParse(await readFile(metadataPath, 'utf-8').catch(() => '{}'));
|
|
186
|
+
metadata.endTime = new Date().toISOString();
|
|
187
|
+
metadata.duration = Date.now() - startTime;
|
|
188
|
+
metadata.exitCode = code;
|
|
189
|
+
metadata.success = code === 0;
|
|
190
|
+
metadata.outputSize = Buffer.byteLength(output);
|
|
191
|
+
|
|
192
|
+
if (!metadata.success) {
|
|
193
|
+
metadata.error = `Process exited with code ${code}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
197
|
+
|
|
198
|
+
if (metadata.success) {
|
|
199
|
+
hooks.onRunCompleted?.(metadata, output);
|
|
200
|
+
} else {
|
|
201
|
+
hooks.onRunFailed?.(metadata, metadata.error, output);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
onComplete?.(metadata);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return runId;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Execute an API run
|
|
212
|
+
*/
|
|
213
|
+
async executeApiRun(runId, provider, model, prompt, workspacePath, screenshots, onData, onComplete) {
|
|
214
|
+
const runDir = join(RUNS_PATH, runId);
|
|
215
|
+
const outputPath = join(runDir, 'output.txt');
|
|
216
|
+
const metadataPath = join(runDir, 'metadata.json');
|
|
217
|
+
|
|
218
|
+
const startTime = Date.now();
|
|
219
|
+
let output = '';
|
|
220
|
+
|
|
221
|
+
const headers = {
|
|
222
|
+
'Content-Type': 'application/json'
|
|
223
|
+
};
|
|
224
|
+
if (provider.apiKey) {
|
|
225
|
+
headers['Authorization'] = `Bearer ${provider.apiKey}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
activeRuns.set(runId, controller);
|
|
230
|
+
|
|
231
|
+
hooks.onRunStarted?.({ runId, provider: provider.name, model });
|
|
232
|
+
|
|
233
|
+
// Build message content
|
|
234
|
+
let messageContent;
|
|
235
|
+
if (screenshots && screenshots.length > 0) {
|
|
236
|
+
console.log(`📸 Loading ${screenshots.length} screenshots for vision API`);
|
|
237
|
+
const contentParts = [];
|
|
238
|
+
|
|
239
|
+
for (const screenshotPath of screenshots) {
|
|
240
|
+
const imageDataUrl = await loadImageAsBase64(screenshotPath).catch(err => {
|
|
241
|
+
console.error(`❌ Failed to load screenshot ${screenshotPath}: ${err.message}`);
|
|
242
|
+
return null;
|
|
243
|
+
});
|
|
244
|
+
if (imageDataUrl) {
|
|
245
|
+
contentParts.push({
|
|
246
|
+
type: 'image_url',
|
|
247
|
+
image_url: { url: imageDataUrl }
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
contentParts.push({ type: 'text', text: prompt });
|
|
253
|
+
messageContent = contentParts;
|
|
254
|
+
} else {
|
|
255
|
+
messageContent = prompt;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const response = await fetch(`${provider.endpoint}/chat/completions`, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers,
|
|
261
|
+
signal: controller.signal,
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
model: model || provider.defaultModel,
|
|
264
|
+
messages: [{ role: 'user', content: messageContent }],
|
|
265
|
+
stream: true
|
|
266
|
+
})
|
|
267
|
+
}).catch(err => ({ ok: false, error: err.message }));
|
|
268
|
+
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
activeRuns.delete(runId);
|
|
271
|
+
const metadata = safeJsonParse(await readFile(metadataPath, 'utf-8').catch(() => '{}'));
|
|
272
|
+
metadata.endTime = new Date().toISOString();
|
|
273
|
+
metadata.duration = Date.now() - startTime;
|
|
274
|
+
metadata.success = false;
|
|
275
|
+
|
|
276
|
+
const errorDetails = response.error || `API error: ${response.status}`;
|
|
277
|
+
metadata.error = errorDetails;
|
|
278
|
+
metadata.errorDetails = errorDetails;
|
|
279
|
+
|
|
280
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
281
|
+
|
|
282
|
+
hooks.onRunFailed?.(metadata, errorDetails, '');
|
|
283
|
+
onComplete?.(metadata);
|
|
284
|
+
return runId;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Handle streaming response
|
|
288
|
+
const reader = response.body.getReader();
|
|
289
|
+
const decoder = new TextDecoder();
|
|
290
|
+
|
|
291
|
+
const processStream = async () => {
|
|
292
|
+
while (true) {
|
|
293
|
+
const { done, value } = await reader.read();
|
|
294
|
+
if (done) break;
|
|
295
|
+
|
|
296
|
+
const chunk = decoder.decode(value);
|
|
297
|
+
const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
|
|
298
|
+
|
|
299
|
+
for (const line of lines) {
|
|
300
|
+
const data = line.slice(6);
|
|
301
|
+
if (data === '✅') continue;
|
|
302
|
+
|
|
303
|
+
let parsed = null;
|
|
304
|
+
parsed = JSON.parse(data);
|
|
305
|
+
if (parsed?.choices?.[0]?.delta?.content) {
|
|
306
|
+
const text = parsed.choices[0].delta.content;
|
|
307
|
+
output += text;
|
|
308
|
+
onData?.(text);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await writeFile(outputPath, output);
|
|
314
|
+
activeRuns.delete(runId);
|
|
315
|
+
|
|
316
|
+
const metadata = safeJsonParse(await readFile(metadataPath, 'utf-8').catch(() => '{}'));
|
|
317
|
+
metadata.endTime = new Date().toISOString();
|
|
318
|
+
metadata.duration = Date.now() - startTime;
|
|
319
|
+
metadata.exitCode = 0;
|
|
320
|
+
metadata.success = true;
|
|
321
|
+
metadata.outputSize = Buffer.byteLength(output);
|
|
322
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
323
|
+
|
|
324
|
+
hooks.onRunCompleted?.(metadata, output);
|
|
325
|
+
onComplete?.(metadata);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
processStream().catch(async (err) => {
|
|
329
|
+
activeRuns.delete(runId);
|
|
330
|
+
|
|
331
|
+
if (output) {
|
|
332
|
+
await writeFile(outputPath, output).catch(() => {});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const metadata = safeJsonParse(await readFile(metadataPath, 'utf-8').catch(() => '{}'));
|
|
336
|
+
metadata.endTime = new Date().toISOString();
|
|
337
|
+
metadata.duration = Date.now() - startTime;
|
|
338
|
+
metadata.success = false;
|
|
339
|
+
metadata.error = err.message;
|
|
340
|
+
metadata.outputSize = Buffer.byteLength(output);
|
|
341
|
+
|
|
342
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
343
|
+
|
|
344
|
+
hooks.onRunFailed?.(metadata, err.message, output);
|
|
345
|
+
onComplete?.(metadata);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return runId;
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Stop a running run
|
|
353
|
+
*/
|
|
354
|
+
async stopRun(runId) {
|
|
355
|
+
const active = activeRuns.get(runId);
|
|
356
|
+
if (!active) return false;
|
|
357
|
+
|
|
358
|
+
if (active.kill) {
|
|
359
|
+
active.kill('SIGTERM');
|
|
360
|
+
} else if (active.abort) {
|
|
361
|
+
active.abort();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
activeRuns.delete(runId);
|
|
365
|
+
return true;
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get run metadata
|
|
370
|
+
*/
|
|
371
|
+
async getRun(runId) {
|
|
372
|
+
const runDir = join(RUNS_PATH, runId);
|
|
373
|
+
if (!existsSync(runDir)) return null;
|
|
374
|
+
|
|
375
|
+
const metadata = safeJsonParse(await readFile(join(runDir, 'metadata.json'), 'utf-8').catch(() => '{}'));
|
|
376
|
+
return metadata;
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get run output
|
|
381
|
+
*/
|
|
382
|
+
async getRunOutput(runId) {
|
|
383
|
+
const runDir = join(RUNS_PATH, runId);
|
|
384
|
+
if (!existsSync(runDir)) return null;
|
|
385
|
+
|
|
386
|
+
return readFile(join(runDir, 'output.txt'), 'utf-8');
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get run prompt
|
|
391
|
+
*/
|
|
392
|
+
async getRunPrompt(runId) {
|
|
393
|
+
const runDir = join(RUNS_PATH, runId);
|
|
394
|
+
if (!existsSync(runDir)) return null;
|
|
395
|
+
|
|
396
|
+
return readFile(join(runDir, 'prompt.txt'), 'utf-8');
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* List runs
|
|
401
|
+
*/
|
|
402
|
+
async listRuns(limit = 50, offset = 0, source = 'all') {
|
|
403
|
+
await ensureRunsDir();
|
|
404
|
+
|
|
405
|
+
const entries = await readdir(RUNS_PATH, { withFileTypes: true });
|
|
406
|
+
const runIds = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
407
|
+
|
|
408
|
+
const runs = [];
|
|
409
|
+
for (const runId of runIds) {
|
|
410
|
+
const metadataPath = join(RUNS_PATH, runId, 'metadata.json');
|
|
411
|
+
if (existsSync(metadataPath)) {
|
|
412
|
+
const metadata = safeJsonParse(await readFile(metadataPath, 'utf-8').catch(() => '{}'));
|
|
413
|
+
if (metadata.id) runs.push(metadata);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let filteredRuns = runs;
|
|
418
|
+
if (source !== 'all') {
|
|
419
|
+
filteredRuns = runs.filter(run => {
|
|
420
|
+
const runSource = run.source || 'devtools';
|
|
421
|
+
return runSource === source;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
filteredRuns.sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
total: filteredRuns.length,
|
|
429
|
+
runs: filteredRuns.slice(offset, offset + limit)
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Delete a run
|
|
435
|
+
*/
|
|
436
|
+
async deleteRun(runId) {
|
|
437
|
+
const runDir = join(RUNS_PATH, runId);
|
|
438
|
+
if (!existsSync(runDir)) return false;
|
|
439
|
+
|
|
440
|
+
await rm(runDir, { recursive: true });
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Delete all failed runs
|
|
446
|
+
*/
|
|
447
|
+
async deleteFailedRuns() {
|
|
448
|
+
await ensureRunsDir();
|
|
449
|
+
|
|
450
|
+
const entries = await readdir(RUNS_PATH, { withFileTypes: true });
|
|
451
|
+
const runIds = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
452
|
+
|
|
453
|
+
let deletedCount = 0;
|
|
454
|
+
for (const runId of runIds) {
|
|
455
|
+
const metadataPath = join(RUNS_PATH, runId, 'metadata.json');
|
|
456
|
+
if (existsSync(metadataPath)) {
|
|
457
|
+
const metadata = safeJsonParse(await readFile(metadataPath, 'utf-8').catch(() => '{}'));
|
|
458
|
+
if (metadata.success === false) {
|
|
459
|
+
await rm(join(RUNS_PATH, runId), { recursive: true });
|
|
460
|
+
deletedCount++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return deletedCount;
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Check if a run is active
|
|
470
|
+
*/
|
|
471
|
+
async isRunActive(runId) {
|
|
472
|
+
return activeRuns.has(runId);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provider schema
|
|
5
|
+
*/
|
|
6
|
+
export const providerSchema = z.object({
|
|
7
|
+
name: z.string().min(1).max(100),
|
|
8
|
+
type: z.enum(['cli', 'api']),
|
|
9
|
+
command: z.string().optional(),
|
|
10
|
+
args: z.array(z.string()).optional(),
|
|
11
|
+
endpoint: z.string().url().optional(),
|
|
12
|
+
apiKey: z.string().optional(),
|
|
13
|
+
models: z.array(z.string()).optional(),
|
|
14
|
+
defaultModel: z.string().nullable().optional(),
|
|
15
|
+
timeout: z.number().int().min(1000).max(600000).optional(),
|
|
16
|
+
enabled: z.boolean().optional(),
|
|
17
|
+
envVars: z.record(z.string()).optional()
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run schema
|
|
22
|
+
*/
|
|
23
|
+
export const runSchema = z.object({
|
|
24
|
+
type: z.enum(['ai', 'command']),
|
|
25
|
+
providerId: z.string().optional(),
|
|
26
|
+
model: z.string().optional(),
|
|
27
|
+
workspacePath: z.string().optional(),
|
|
28
|
+
workspaceName: z.string().optional(),
|
|
29
|
+
command: z.string().optional(),
|
|
30
|
+
prompt: z.string().optional(),
|
|
31
|
+
screenshots: z.array(z.string()).optional(),
|
|
32
|
+
timeout: z.number().int().min(1000).max(600000).optional()
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate data against a schema
|
|
37
|
+
* Returns { success: true, data } or { success: false, errors }
|
|
38
|
+
*/
|
|
39
|
+
export function validate(schema, data) {
|
|
40
|
+
const result = schema.safeParse(data);
|
|
41
|
+
if (result.success) {
|
|
42
|
+
return { success: true, data: result.data };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
errors: result.error.errors.map(e => ({
|
|
47
|
+
path: e.path.join('.'),
|
|
48
|
+
message: e.message
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for AI Toolkit
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const PROVIDER_TYPES = {
|
|
6
|
+
CLI: 'cli',
|
|
7
|
+
API: 'api'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const MODEL_TIERS = {
|
|
11
|
+
LIGHT: 'light',
|
|
12
|
+
MEDIUM: 'medium',
|
|
13
|
+
HEAVY: 'heavy'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const RUN_TYPES = {
|
|
17
|
+
AI: 'ai',
|
|
18
|
+
COMMAND: 'command'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_TIMEOUT = 300000; // 5 minutes
|
|
22
|
+
export const MAX_TIMEOUT = 600000; // 10 minutes
|
|
23
|
+
export const MIN_TIMEOUT = 1000; // 1 second
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_TEMPERATURE = 0.1;
|
|
26
|
+
export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
|