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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * TypeScript declarations for @portos/ai-toolkit/server
3
+ */
4
+
5
+ export interface ProviderService {
6
+ getAllProviders(): Promise<{ activeProvider: string | null; providers: any[] }>;
7
+ getProviderById(id: string): Promise<any | null>;
8
+ getActiveProvider(): Promise<any | null>;
9
+ setActiveProvider(id: string): Promise<any | null>;
10
+ createProvider(data: any): Promise<any>;
11
+ updateProvider(id: string, updates: any): Promise<any | null>;
12
+ deleteProvider(id: string): Promise<boolean>;
13
+ testProvider(id: string): Promise<{ success: boolean; [key: string]: any }>;
14
+ refreshProviderModels(id: string): Promise<any | null>;
15
+ }
16
+
17
+ export interface RunnerService {
18
+ createRun(options: any): Promise<any>;
19
+ executeCliRun(...args: any[]): Promise<string>;
20
+ executeApiRun(...args: any[]): Promise<string>;
21
+ stopRun(runId: string): Promise<boolean>;
22
+ getRun(runId: string): Promise<any | null>;
23
+ getRunOutput(runId: string): Promise<string | null>;
24
+ getRunPrompt(runId: string): Promise<string | null>;
25
+ listRuns(limit?: number, offset?: number, source?: string): Promise<{ total: number; runs: any[] }>;
26
+ deleteRun(runId: string): Promise<boolean>;
27
+ deleteFailedRuns(): Promise<number>;
28
+ isRunActive(runId: string): Promise<boolean>;
29
+ }
30
+
31
+ export interface PromptsService {
32
+ init(): Promise<void>;
33
+ getStages(): Record<string, any>;
34
+ getStage(name: string): any | null;
35
+ getStageTemplate(name: string): Promise<string | null>;
36
+ updateStageTemplate(name: string, content: string): Promise<void>;
37
+ updateStageConfig(name: string, config: any): Promise<void>;
38
+ createStage(stageName: string, config: any, template?: string): Promise<void>;
39
+ deleteStage(stageName: string): Promise<void>;
40
+ getVariables(): Record<string, any>;
41
+ getVariable(key: string): any | null;
42
+ updateVariable(key: string, data: any): Promise<void>;
43
+ createVariable(key: string, data: any): Promise<void>;
44
+ deleteVariable(key: string): Promise<void>;
45
+ buildPrompt(stageName: string, data?: any): Promise<string>;
46
+ previewPrompt(stageName: string, testData?: any): Promise<string>;
47
+ }
48
+
49
+ export interface AIToolkit {
50
+ services: {
51
+ providers: ProviderService;
52
+ runner: RunnerService;
53
+ prompts: PromptsService;
54
+ };
55
+ routes: {
56
+ providers: any;
57
+ runs: any;
58
+ prompts: any;
59
+ };
60
+ mountRoutes(app: any, basePath?: string): void;
61
+ }
62
+
63
+ export interface AIToolkitConfig {
64
+ dataDir?: string;
65
+ providersFile?: string;
66
+ runsDir?: string;
67
+ promptsDir?: string;
68
+ screenshotsDir?: string;
69
+ sampleProvidersFile?: string;
70
+ io?: any;
71
+ asyncHandler?: (fn: any) => any;
72
+ hooks?: {
73
+ onRunCreated?: (metadata: any) => void;
74
+ onRunStarted?: (run: any) => void;
75
+ onRunCompleted?: (metadata: any, output: string) => void;
76
+ onRunFailed?: (metadata: any, error: string, output: string) => void;
77
+ };
78
+ maxConcurrentRuns?: number;
79
+ }
80
+
81
+ export function createAIToolkit(config?: AIToolkitConfig): AIToolkit;
82
+ export function createProviderService(config?: Partial<AIToolkitConfig>): ProviderService;
83
+ export function createRunnerService(config?: Partial<AIToolkitConfig>): RunnerService;
84
+ export function createPromptsService(config?: Partial<AIToolkitConfig>): PromptsService;
85
+ export function createProvidersRoutes(service: ProviderService, options?: any): any;
86
+ export function createRunsRoutes(service: RunnerService, options?: any): any;
87
+ export function createPromptsRoutes(service: PromptsService, options?: any): any;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * AI Toolkit Server
3
+ * Configurable AI provider, runner, and prompt services with Express routes
4
+ */
5
+
6
+ import { createProviderService } from './providers.js';
7
+ import { createRunnerService } from './runner.js';
8
+ import { createPromptsService } from './prompts.js';
9
+ import { createProvidersRoutes } from './routes/providers.js';
10
+ import { createRunsRoutes } from './routes/runs.js';
11
+ import { createPromptsRoutes } from './routes/prompts.js';
12
+
13
+ export * from './validation.js';
14
+ export { createProviderService, createRunnerService, createPromptsService };
15
+ export { createProvidersRoutes, createRunsRoutes, createPromptsRoutes };
16
+
17
+ /**
18
+ * Create a complete AI toolkit instance with services and routes
19
+ */
20
+ export function createAIToolkit(config = {}) {
21
+ const {
22
+ dataDir = './data',
23
+ providersFile = 'providers.json',
24
+ runsDir = 'runs',
25
+ promptsDir = 'prompts',
26
+ screenshotsDir = './data/screenshots',
27
+ sampleProvidersFile = null,
28
+
29
+ // Socket.IO instance for real-time updates
30
+ io = null,
31
+
32
+ // Optional async handler wrapper (e.g., for error handling)
33
+ asyncHandler = (fn) => fn,
34
+
35
+ // Hooks for lifecycle events
36
+ hooks = {},
37
+
38
+ // Runner config
39
+ maxConcurrentRuns = 5
40
+ } = config;
41
+
42
+ // Create services
43
+ const providerService = createProviderService({
44
+ dataDir,
45
+ providersFile,
46
+ sampleFile: sampleProvidersFile
47
+ });
48
+
49
+ const runnerService = createRunnerService({
50
+ dataDir,
51
+ runsDir,
52
+ screenshotsDir,
53
+ providerService,
54
+ hooks,
55
+ maxConcurrentRuns
56
+ });
57
+
58
+ const promptsService = createPromptsService({
59
+ dataDir,
60
+ promptsDir
61
+ });
62
+
63
+ // Initialize prompts service
64
+ promptsService.init().catch(err => {
65
+ console.error(`❌ Failed to initialize prompts: ${err.message}`);
66
+ });
67
+
68
+ // Create routes
69
+ const providersRouter = createProvidersRoutes(providerService, { asyncHandler });
70
+ const runsRouter = createRunsRoutes(runnerService, { asyncHandler, io });
71
+ const promptsRouter = createPromptsRoutes(promptsService, { asyncHandler });
72
+
73
+ return {
74
+ // Services
75
+ services: {
76
+ providers: providerService,
77
+ runner: runnerService,
78
+ prompts: promptsService
79
+ },
80
+
81
+ // Routes
82
+ routes: {
83
+ providers: providersRouter,
84
+ runs: runsRouter,
85
+ prompts: promptsRouter
86
+ },
87
+
88
+ // Convenience method to mount all routes
89
+ mountRoutes(app, basePath = '/api') {
90
+ app.use(`${basePath}/providers`, providersRouter);
91
+ app.use(`${basePath}/runs`, runsRouter);
92
+ app.use(`${basePath}/prompts`, promptsRouter);
93
+ }
94
+ };
95
+ }
@@ -0,0 +1,234 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ /**
6
+ * Create a prompts service with configurable storage
7
+ */
8
+ export function createPromptsService(config = {}) {
9
+ const {
10
+ dataDir = './data',
11
+ promptsDir = 'prompts'
12
+ } = config;
13
+
14
+ const PROMPTS_PATH = join(dataDir, promptsDir);
15
+
16
+ let stageConfig = null;
17
+ let variables = null;
18
+
19
+ /**
20
+ * Load or reload prompts configuration
21
+ */
22
+ async function loadPrompts() {
23
+ const configPath = join(PROMPTS_PATH, 'stage-config.json');
24
+ const varsPath = join(PROMPTS_PATH, 'variables.json');
25
+
26
+ if (existsSync(configPath)) {
27
+ stageConfig = JSON.parse(await readFile(configPath, 'utf-8'));
28
+ } else {
29
+ stageConfig = { stages: {} };
30
+ }
31
+
32
+ if (existsSync(varsPath)) {
33
+ variables = JSON.parse(await readFile(varsPath, 'utf-8'));
34
+ } else {
35
+ variables = { variables: {} };
36
+ }
37
+
38
+ console.log(`📝 Loaded ${Object.keys(stageConfig.stages || {}).length} prompt stages`);
39
+ }
40
+
41
+ /**
42
+ * Apply Mustache-like template substitution
43
+ */
44
+ function applyTemplate(template, data) {
45
+ let result = template;
46
+
47
+ // Handle sections {{#key}}...{{/key}}
48
+ result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (match, key, content) => {
49
+ const value = data[key];
50
+ if (!value) return '';
51
+ if (Array.isArray(value)) {
52
+ return value.map(item => {
53
+ if (typeof item === 'object') {
54
+ return applyTemplate(content, item);
55
+ }
56
+ return content.replace(/\{\{\.\}\}/g, item);
57
+ }).join('');
58
+ }
59
+ return applyTemplate(content, data);
60
+ });
61
+
62
+ // Handle inverted sections {{^key}}...{{/key}}
63
+ result = result.replace(/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (match, key, content) => {
64
+ return data[key] ? '' : content;
65
+ });
66
+
67
+ // Handle simple variables {{key}}
68
+ result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
69
+ return data[key] !== undefined ? String(data[key]) : '';
70
+ });
71
+
72
+ return result.trim();
73
+ }
74
+
75
+ return {
76
+ /**
77
+ * Load prompts configuration
78
+ */
79
+ async init() {
80
+ await loadPrompts();
81
+ },
82
+
83
+ /**
84
+ * Get all stages
85
+ */
86
+ getStages() {
87
+ return stageConfig?.stages || {};
88
+ },
89
+
90
+ /**
91
+ * Get a single stage
92
+ */
93
+ getStage(stageName) {
94
+ return stageConfig?.stages?.[stageName] || null;
95
+ },
96
+
97
+ /**
98
+ * Get stage template content
99
+ */
100
+ async getStageTemplate(stageName) {
101
+ const templatePath = join(PROMPTS_PATH, 'stages', `${stageName}.md`);
102
+ if (!existsSync(templatePath)) return null;
103
+ return readFile(templatePath, 'utf-8');
104
+ },
105
+
106
+ /**
107
+ * Update stage template
108
+ */
109
+ async updateStageTemplate(stageName, content) {
110
+ const stagesDir = join(PROMPTS_PATH, 'stages');
111
+ if (!existsSync(stagesDir)) await mkdir(stagesDir, { recursive: true });
112
+ await writeFile(join(stagesDir, `${stageName}.md`), content);
113
+ },
114
+
115
+ /**
116
+ * Update stage configuration
117
+ */
118
+ async updateStageConfig(stageName, updatedConfig) {
119
+ if (!stageConfig) await loadPrompts();
120
+ stageConfig.stages[stageName] = { ...stageConfig.stages[stageName], ...updatedConfig };
121
+ await writeFile(join(PROMPTS_PATH, 'stage-config.json'), JSON.stringify(stageConfig, null, 2));
122
+ },
123
+
124
+ /**
125
+ * Create a new stage
126
+ */
127
+ async createStage(stageName, config, template = '') {
128
+ if (!stageConfig) await loadPrompts();
129
+ if (stageConfig.stages[stageName]) {
130
+ throw new Error(`Stage ${stageName} already exists`);
131
+ }
132
+ stageConfig.stages[stageName] = config;
133
+ await writeFile(join(PROMPTS_PATH, 'stage-config.json'), JSON.stringify(stageConfig, null, 2));
134
+
135
+ // Create template file
136
+ const stagesDir = join(PROMPTS_PATH, 'stages');
137
+ if (!existsSync(stagesDir)) await mkdir(stagesDir, { recursive: true });
138
+ await writeFile(join(stagesDir, `${stageName}.md`), template);
139
+
140
+ console.log(`✅ Created prompt stage: ${stageName}`);
141
+ },
142
+
143
+ /**
144
+ * Delete a stage
145
+ */
146
+ async deleteStage(stageName) {
147
+ if (!stageConfig) await loadPrompts();
148
+ if (!stageConfig.stages[stageName]) {
149
+ throw new Error(`Stage ${stageName} not found`);
150
+ }
151
+ delete stageConfig.stages[stageName];
152
+ await writeFile(join(PROMPTS_PATH, 'stage-config.json'), JSON.stringify(stageConfig, null, 2));
153
+
154
+ // Delete template file if it exists
155
+ const templatePath = join(PROMPTS_PATH, 'stages', `${stageName}.md`);
156
+ if (existsSync(templatePath)) {
157
+ const { unlink } = await import('fs/promises');
158
+ await unlink(templatePath);
159
+ }
160
+
161
+ console.log(`🗑️ Deleted prompt stage: ${stageName}`);
162
+ },
163
+
164
+ /**
165
+ * Get all variables
166
+ */
167
+ getVariables() {
168
+ return variables?.variables || {};
169
+ },
170
+
171
+ /**
172
+ * Get a single variable
173
+ */
174
+ getVariable(key) {
175
+ return variables?.variables?.[key] || null;
176
+ },
177
+
178
+ /**
179
+ * Update a variable
180
+ */
181
+ async updateVariable(key, data) {
182
+ if (!variables) await loadPrompts();
183
+ variables.variables[key] = { ...variables.variables[key], ...data };
184
+ await writeFile(join(PROMPTS_PATH, 'variables.json'), JSON.stringify(variables, null, 2));
185
+ },
186
+
187
+ /**
188
+ * Create a new variable
189
+ */
190
+ async createVariable(key, data) {
191
+ if (!variables) await loadPrompts();
192
+ if (variables.variables[key]) {
193
+ throw new Error(`Variable ${key} already exists`);
194
+ }
195
+ variables.variables[key] = data;
196
+ await writeFile(join(PROMPTS_PATH, 'variables.json'), JSON.stringify(variables, null, 2));
197
+ },
198
+
199
+ /**
200
+ * Delete a variable
201
+ */
202
+ async deleteVariable(key) {
203
+ if (!variables) await loadPrompts();
204
+ delete variables.variables[key];
205
+ await writeFile(join(PROMPTS_PATH, 'variables.json'), JSON.stringify(variables, null, 2));
206
+ },
207
+
208
+ /**
209
+ * Build a prompt from template and variables
210
+ */
211
+ async buildPrompt(stageName, data = {}) {
212
+ const stage = stageConfig?.stages?.[stageName];
213
+ if (!stage) throw new Error(`Stage ${stageName} not found`);
214
+
215
+ let template = await this.getStageTemplate(stageName);
216
+ if (!template) throw new Error(`Template for ${stageName} not found`);
217
+
218
+ const allVars = { ...data };
219
+ for (const varName of stage.variables || []) {
220
+ const v = variables?.variables?.[varName];
221
+ if (v) allVars[varName] = v.content;
222
+ }
223
+
224
+ return applyTemplate(template, allVars);
225
+ },
226
+
227
+ /**
228
+ * Preview a prompt with test data
229
+ */
230
+ async previewPrompt(stageName, testData = {}) {
231
+ return this.buildPrompt(stageName, testData);
232
+ }
233
+ };
234
+ }
@@ -0,0 +1,253 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ /**
10
+ * Create a provider service with configurable storage
11
+ */
12
+ export function createProviderService(config = {}) {
13
+ const {
14
+ dataDir = './data',
15
+ providersFile = 'providers.json',
16
+ sampleFile = null
17
+ } = config;
18
+
19
+ const PROVIDERS_PATH = join(dataDir, providersFile);
20
+
21
+ async function ensureDataDir() {
22
+ if (!existsSync(dataDir)) {
23
+ await mkdir(dataDir, { recursive: true });
24
+ }
25
+ }
26
+
27
+ async function loadProviders() {
28
+ await ensureDataDir();
29
+
30
+ if (!existsSync(PROVIDERS_PATH)) {
31
+ // Copy from sample if exists
32
+ if (sampleFile && existsSync(sampleFile)) {
33
+ const sample = await readFile(sampleFile, 'utf-8');
34
+ await writeFile(PROVIDERS_PATH, sample);
35
+ return JSON.parse(sample);
36
+ }
37
+ return { activeProvider: null, providers: {} };
38
+ }
39
+
40
+ const content = await readFile(PROVIDERS_PATH, 'utf-8');
41
+ return JSON.parse(content);
42
+ }
43
+
44
+ async function saveProviders(data) {
45
+ await ensureDataDir();
46
+ await writeFile(PROVIDERS_PATH, JSON.stringify(data, null, 2));
47
+ }
48
+
49
+ return {
50
+ /**
51
+ * Get all providers with active provider info
52
+ */
53
+ async getAllProviders() {
54
+ const data = await loadProviders();
55
+ return {
56
+ activeProvider: data.activeProvider,
57
+ providers: Object.values(data.providers)
58
+ };
59
+ },
60
+
61
+ /**
62
+ * Get a specific provider by ID
63
+ */
64
+ async getProviderById(id) {
65
+ const data = await loadProviders();
66
+ return data.providers[id] || null;
67
+ },
68
+
69
+ /**
70
+ * Get the currently active provider
71
+ */
72
+ async getActiveProvider() {
73
+ const data = await loadProviders();
74
+ if (!data.activeProvider) return null;
75
+ return data.providers[data.activeProvider] || null;
76
+ },
77
+
78
+ /**
79
+ * Set the active provider
80
+ */
81
+ async setActiveProvider(id) {
82
+ const data = await loadProviders();
83
+ if (!data.providers[id]) {
84
+ return null;
85
+ }
86
+ data.activeProvider = id;
87
+ await saveProviders(data);
88
+ return data.providers[id];
89
+ },
90
+
91
+ /**
92
+ * Create a new provider
93
+ */
94
+ async createProvider(providerData) {
95
+ const data = await loadProviders();
96
+ const id = providerData.id || providerData.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
97
+
98
+ if (data.providers[id]) {
99
+ throw new Error('Provider with this ID already exists');
100
+ }
101
+
102
+ const provider = {
103
+ id,
104
+ name: providerData.name,
105
+ type: providerData.type || 'cli',
106
+ command: providerData.command || null,
107
+ args: providerData.args || [],
108
+ endpoint: providerData.endpoint || null,
109
+ apiKey: providerData.apiKey || '',
110
+ models: providerData.models || [],
111
+ defaultModel: providerData.defaultModel || null,
112
+ timeout: providerData.timeout || 300000,
113
+ enabled: providerData.enabled !== false,
114
+ envVars: providerData.envVars || {}
115
+ };
116
+
117
+ data.providers[id] = provider;
118
+
119
+ // Set as active if it's the first provider
120
+ if (!data.activeProvider) {
121
+ data.activeProvider = id;
122
+ }
123
+
124
+ await saveProviders(data);
125
+ return provider;
126
+ },
127
+
128
+ /**
129
+ * Update an existing provider
130
+ */
131
+ async updateProvider(id, updates) {
132
+ const data = await loadProviders();
133
+
134
+ if (!data.providers[id]) {
135
+ return null;
136
+ }
137
+
138
+ const provider = {
139
+ ...data.providers[id],
140
+ ...updates,
141
+ id // Prevent ID override
142
+ };
143
+
144
+ data.providers[id] = provider;
145
+ await saveProviders(data);
146
+ return provider;
147
+ },
148
+
149
+ /**
150
+ * Delete a provider
151
+ */
152
+ async deleteProvider(id) {
153
+ const data = await loadProviders();
154
+
155
+ if (!data.providers[id]) {
156
+ return false;
157
+ }
158
+
159
+ delete data.providers[id];
160
+
161
+ // Clear active if it was deleted
162
+ if (data.activeProvider === id) {
163
+ const remaining = Object.keys(data.providers);
164
+ data.activeProvider = remaining.length > 0 ? remaining[0] : null;
165
+ }
166
+
167
+ await saveProviders(data);
168
+ return true;
169
+ },
170
+
171
+ /**
172
+ * Test provider connectivity
173
+ */
174
+ async testProvider(id) {
175
+ const data = await loadProviders();
176
+ const provider = data.providers[id];
177
+
178
+ if (!provider) {
179
+ return { success: false, error: 'Provider not found' };
180
+ }
181
+
182
+ if (provider.type === 'cli') {
183
+ // Test CLI availability
184
+ const { stdout, stderr } = await execAsync(`which ${provider.command}`).catch(() => ({ stdout: '', stderr: 'not found' }));
185
+
186
+ if (!stdout.trim()) {
187
+ return { success: false, error: `Command '${provider.command}' not found in PATH` };
188
+ }
189
+
190
+ // Try to get version or help
191
+ const { stdout: versionOut } = await execAsync(`${provider.command} --version 2>/dev/null || ${provider.command} -v 2>/dev/null || echo "available"`).catch(() => ({ stdout: 'available' }));
192
+
193
+ return {
194
+ success: true,
195
+ path: stdout.trim(),
196
+ version: versionOut.trim()
197
+ };
198
+ }
199
+
200
+ if (provider.type === 'api') {
201
+ // Test API endpoint
202
+ const modelsUrl = `${provider.endpoint}/models`;
203
+ const response = await fetch(modelsUrl, {
204
+ headers: provider.apiKey ? { 'Authorization': `Bearer ${provider.apiKey}` } : {}
205
+ }).catch(err => ({ ok: false, error: err.message }));
206
+
207
+ if (!response.ok) {
208
+ return { success: false, error: `API not reachable: ${response.error || response.status}` };
209
+ }
210
+
211
+ const models = await response.json().catch(() => ({ data: [] }));
212
+ return {
213
+ success: true,
214
+ endpoint: provider.endpoint,
215
+ models: models.data?.map(m => m.id) || []
216
+ };
217
+ }
218
+
219
+ return { success: false, error: 'Unknown provider type' };
220
+ },
221
+
222
+ /**
223
+ * Refresh models from API provider
224
+ */
225
+ async refreshProviderModels(id) {
226
+ const data = await loadProviders();
227
+ const provider = data.providers[id];
228
+
229
+ if (!provider || provider.type !== 'api') {
230
+ return null;
231
+ }
232
+
233
+ const modelsUrl = `${provider.endpoint}/models`;
234
+ const response = await fetch(modelsUrl, {
235
+ headers: provider.apiKey ? { 'Authorization': `Bearer ${provider.apiKey}` } : {}
236
+ }).catch(() => null);
237
+
238
+ if (!response?.ok) return null;
239
+
240
+ const responseData = await response.json().catch(() => ({ data: [] }));
241
+ const models = responseData.data?.map(m => m.id) || [];
242
+
243
+ const updatedProvider = {
244
+ ...data.providers[id],
245
+ models
246
+ };
247
+
248
+ data.providers[id] = updatedProvider;
249
+ await saveProviders(data);
250
+ return updatedProvider;
251
+ }
252
+ };
253
+ }