millas 0.2.12-beta-2 → 0.2.13

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 (57) hide show
  1. package/package.json +3 -2
  2. package/src/admin/Admin.js +122 -38
  3. package/src/admin/ViewContext.js +12 -3
  4. package/src/admin/resources/AdminResource.js +10 -0
  5. package/src/admin/static/admin.css +95 -14
  6. package/src/admin/views/layouts/base.njk +23 -34
  7. package/src/admin/views/pages/detail.njk +16 -5
  8. package/src/admin/views/pages/error.njk +65 -0
  9. package/src/admin/views/pages/list.njk +127 -2
  10. package/src/admin/views/partials/form-scripts.njk +7 -3
  11. package/src/admin/views/partials/form-widget.njk +2 -1
  12. package/src/admin/views/partials/icons.njk +64 -0
  13. package/src/ai/AIManager.js +954 -0
  14. package/src/ai/AITokenBudget.js +250 -0
  15. package/src/ai/PromptGuard.js +216 -0
  16. package/src/ai/agents.js +218 -0
  17. package/src/ai/conversation.js +213 -0
  18. package/src/ai/drivers.js +734 -0
  19. package/src/ai/files.js +249 -0
  20. package/src/ai/media.js +303 -0
  21. package/src/ai/pricing.js +152 -0
  22. package/src/ai/provider_tools.js +114 -0
  23. package/src/ai/types.js +356 -0
  24. package/src/commands/createsuperuser.js +17 -4
  25. package/src/commands/serve.js +2 -4
  26. package/src/container/AppInitializer.js +39 -15
  27. package/src/container/Application.js +31 -1
  28. package/src/core/foundation.js +1 -1
  29. package/src/errors/HttpError.js +32 -16
  30. package/src/facades/AI.js +411 -0
  31. package/src/facades/Hash.js +67 -0
  32. package/src/facades/Process.js +144 -0
  33. package/src/hashing/Hash.js +262 -0
  34. package/src/http/HtmlEscape.js +162 -0
  35. package/src/http/MillasRequest.js +63 -7
  36. package/src/http/MillasResponse.js +70 -4
  37. package/src/http/ResponseDispatcher.js +21 -27
  38. package/src/http/SafeFilePath.js +195 -0
  39. package/src/http/SafeRedirect.js +62 -0
  40. package/src/http/SecurityBootstrap.js +70 -0
  41. package/src/http/helpers.js +40 -125
  42. package/src/http/index.js +10 -1
  43. package/src/http/middleware/CsrfMiddleware.js +258 -0
  44. package/src/http/middleware/RateLimiter.js +314 -0
  45. package/src/http/middleware/SecurityHeaders.js +281 -0
  46. package/src/i18n/Translator.js +10 -2
  47. package/src/logger/LogRedactor.js +247 -0
  48. package/src/logger/Logger.js +1 -1
  49. package/src/logger/formatters/JsonFormatter.js +11 -4
  50. package/src/logger/formatters/PrettyFormatter.js +3 -1
  51. package/src/logger/formatters/SimpleFormatter.js +14 -3
  52. package/src/middleware/ThrottleMiddleware.js +27 -4
  53. package/src/process/Process.js +333 -0
  54. package/src/router/MiddlewareRegistry.js +27 -2
  55. package/src/scaffold/templates.js +3 -0
  56. package/src/validation/Validator.js +348 -607
  57. package/src/admin.zip +0 -0
@@ -49,57 +49,82 @@ class AppInitializer {
49
49
  * Boot the full application and start the HTTP server.
50
50
  * Returns a Promise that resolves once the server is listening.
51
51
  */
52
+ /**
53
+ * Boot the full application and start the HTTP server.
54
+ * Called by millas serve — no changes to the developer API.
55
+ */
52
56
  async boot() {
57
+ await this.bootKernel();
58
+ await this._serve();
59
+ }
60
+
61
+ /**
62
+ * Boot the application kernel only — DI container, providers, DB, auth,
63
+ * cache, mail, queue. No HTTP server, no routes, no listen().
64
+ *
65
+ * Used internally by CLI commands (millas migrate, millas createsuperuser, etc.)
66
+ * via MILLAS_CLI_BOOT=1 environment variable. Developers never call this directly.
67
+ *
68
+ * @returns {Application} the booted kernel
69
+ */
70
+ async bootKernel() {
53
71
  const cfg = this._config;
54
72
 
55
- // ── Build the HTTP adapter ───────────────────────────────────────────────
56
73
  const ExpressAdapter = require('../http/adapters/ExpressAdapter');
57
74
  const expressApp = express();
58
75
  this._adapter = new ExpressAdapter(expressApp);
59
76
  this._adapter.applyBodyParsers();
60
77
 
61
- // Raw adapter middleware (helmet, compression, etc.)
78
+ // ── Security applied before any routes or developer middleware ──────
79
+ // Reads config/app.js for overrides. All protections are on by default:
80
+ // security headers, CSRF, rate limiting, cookie defaults, allowed hosts.
81
+ const SecurityBootstrap = require('../http/SecurityBootstrap');
82
+ const basePath = cfg.basePath || process.cwd();
83
+ const appConfig = SecurityBootstrap.loadConfig(basePath + '/config/app');
84
+ SecurityBootstrap.apply(this._adapter.nativeApp || expressApp, appConfig);
85
+ // ─────────────────────────────────────────────────────────────────────
86
+
62
87
  for (const mw of (cfg.adapterMiddleware || [])) {
63
88
  this._adapter.applyMiddleware(mw);
64
89
  }
65
90
 
66
- // ── Build the Application kernel ─────────────────────────────────────────
67
91
  this._kernel = new Application(this._adapter);
68
92
 
69
- // Bind basePath so all providers can resolve config/model paths
70
- // without calling process.cwd() at runtime.
71
- const basePath = cfg.basePath || process.cwd();
72
93
  this._kernel._container.instance('basePath', basePath);
73
94
 
74
- // Core providers (auto-enabled unless disabled in config)
75
95
  const coreProviders = this._buildCoreProviders(cfg);
76
96
  this._kernel.providers([...coreProviders, ...cfg.providers]);
77
97
 
78
- // Named middleware aliases
79
98
  for (const {alias, handler} of (cfg.middleware || [])) {
80
99
  this._kernel.middleware(alias, handler);
81
100
  }
82
101
 
83
- // Route definitions
84
102
  if (cfg.routes) {
85
103
  this._kernel.routes(cfg.routes);
86
104
  }
87
105
 
88
- // ── Boot providers ───────────────────────────────────────────────────────
89
106
  await this._kernel.boot();
90
107
 
91
- // ── Mount routes ─────────────────────────────────────────────────────────
108
+ return this._kernel;
109
+ }
110
+
111
+ /**
112
+ * Mount routes and start the HTTP server.
113
+ * Internal — called only by boot(). CLI commands stop after bootKernel().
114
+ */
115
+ async _serve() {
116
+ const cfg = this._config;
117
+
92
118
  if (!process.env.MILLAS_ROUTE_LIST) {
93
119
  this._kernel.mountRoutes();
94
120
 
95
- // Admin panel — mounted between routes and fallbacks
96
121
  if (cfg.admin !== null) {
97
122
  try {
98
123
  const Admin = require('../admin/Admin');
99
124
  if (cfg.admin && Object.keys(cfg.admin).length) {
100
125
  Admin.configure(cfg.admin);
101
126
  }
102
- Admin.mount(expressApp);
127
+ Admin.mount(this._adapter.nativeApp);
103
128
  } catch (err) {
104
129
  process.stderr.write(`[millas] Admin mount failed: ${err.message}\n`);
105
130
  }
@@ -107,9 +132,8 @@ class AppInitializer {
107
132
 
108
133
  this._kernel.mountFallbacks();
109
134
 
110
- // ── Start the HTTP server ──────────────────────────────────────────────
111
135
  const server = new HttpServer(this._kernel, {
112
- onStart: cfg.onStart || undefined,
136
+ onStart: cfg.onStart || undefined,
113
137
  onShutdown: cfg.onShutdown || undefined,
114
138
  });
115
139
 
@@ -98,6 +98,22 @@ class Application {
98
98
  await this._providers.boot();
99
99
  this._booted = true;
100
100
 
101
+ // Wire cache, db, and storage into AI manager now that providers are booted
102
+ try {
103
+ const ai = this._container.make('ai');
104
+ if (ai) {
105
+ try { const cache = this._container.make('cache'); if (cache) ai.setCache(cache); } catch {}
106
+ try { const db = this._container.make('db'); if (db) ai.setDb(db); } catch {}
107
+ try { const store = this._container.make('storage'); if (store) ai.setStorage(store); } catch {}
108
+ // Attach files and stores API as properties
109
+ try {
110
+ const { AIFilesAPI, AIStoresAPI } = require('../ai/files');
111
+ if (!ai.files) Object.defineProperty(ai, 'files', { get: () => new AIFilesAPI(ai), configurable: true });
112
+ if (!ai.stores) Object.defineProperty(ai, 'stores', { get: () => new AIStoresAPI(ai), configurable: true });
113
+ } catch {}
114
+ }
115
+ } catch { /* ai not registered — skip */ }
116
+
101
117
  this._emitSync('platform.booted', {providers: this._providers.list()});
102
118
 
103
119
  return this;
@@ -287,9 +303,23 @@ class Application {
287
303
  });
288
304
  this._container.instance('url', urlGenerator);
289
305
 
306
+ const { HashManager } = require('../hashing/Hash');
307
+ const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: 12 } });
308
+ this._container.instance('hash', hashManager);
309
+
310
+ const ProcessManager = require('../process/Process').ProcessManager;
311
+ this._container.instance('process', new ProcessManager());
312
+
313
+ const { AIManager } = require('../ai/AIManager');
314
+ const basePath = (() => { try { return this._container.make('basePath'); } catch { return process.cwd(); } })();
315
+ let aiConfig = { default: process.env.AI_PROVIDER || 'anthropic', providers: {} };
316
+ try { aiConfig = require(basePath + '/config/ai'); } catch { /* no config — use env vars */ }
317
+ const aiManager = new AIManager(aiConfig);
318
+ this._container.instance('ai', aiManager);
319
+
290
320
 
291
321
  this._mwRegistry.register('cors', new CorsMiddleware());
292
- this._mwRegistry.register('throttle', new ThrottleMiddleware({max: 60, window: 60}));
322
+ this._mwRegistry.register('throttle', ThrottleMiddleware);
293
323
  this._mwRegistry.register('log', new LogMiddleware());
294
324
  this._mwRegistry.register('auth', AuthMiddleware);
295
325
  }
@@ -41,7 +41,6 @@ const { CacheServiceProvider, StorageServiceProvider } = require('../providers/C
41
41
  // ── Storage ───────────────────────────────────────────────────────
42
42
  const Storage = require('../storage/Storage');
43
43
  const LocalDriver = require('../storage/drivers/LocalDriver');
44
-
45
44
  module.exports = {
46
45
  // ── Millas HTTP layer ──────────────────────────────────────────
47
46
  MillasRequest, MillasResponse, ResponseDispatcher, RequestContext,
@@ -56,4 +55,5 @@ module.exports = {
56
55
  Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
57
56
  // Storage
58
57
  Storage, LocalDriver, StorageServiceProvider,
58
+ Log: require('../logger').Log,
59
59
  };
@@ -3,27 +3,43 @@
3
3
  /**
4
4
  * HttpError
5
5
  *
6
- * A structured error that carries an HTTP status code.
7
- * Thrown by Controller.abort() and the validation system.
8
- * Caught automatically by the Router's global error handler.
6
+ * A structured HTTP error with a status code, message, and optional
7
+ * field-level error map. Thrown by abort(), notFound(), forbidden() etc.
8
+ * and caught by the framework's error handler for consistent responses.
9
9
  *
10
- * Usage:
11
- * throw new HttpError(404, 'User not found')
12
- * throw new HttpError(422, 'Validation failed', { email: ['email is required'] })
10
+ * throw new HttpError(404, 'User not found');
11
+ * throw new HttpError(422, 'Validation failed', { email: ['Required'] });
13
12
  */
14
13
  class HttpError extends Error {
15
14
  /**
16
- * @param {number} status HTTP status code
17
- * @param {string} message Human-readable message
18
- * @param {object} errors Optional field-level errors (for validation)
15
+ * @param {number} status HTTP status code
16
+ * @param {string} [message]
17
+ * @param {object|null} [errors] — field-level errors { field: [msg, ...] }
19
18
  */
20
- constructor(status = 500, message = 'Internal Server Error', errors = null) {
21
- super(message);
22
- this.name = 'HttpError';
23
- this.status = status;
24
- this.statusCode = status;
25
- this.errors = errors;
19
+ constructor(status, message, errors = null) {
20
+ super(message || HttpError.defaultMessage(status));
21
+ this.name = 'HttpError';
22
+ this.status = status;
23
+ this.errors = errors || null;
24
+ }
25
+
26
+ static defaultMessage(status) {
27
+ const messages = {
28
+ 400: 'Bad Request',
29
+ 401: 'Unauthorized',
30
+ 403: 'Forbidden',
31
+ 404: 'Not Found',
32
+ 405: 'Method Not Allowed',
33
+ 409: 'Conflict',
34
+ 410: 'Gone',
35
+ 422: 'Unprocessable Entity',
36
+ 429: 'Too Many Requests',
37
+ 500: 'Internal Server Error',
38
+ 502: 'Bad Gateway',
39
+ 503: 'Service Unavailable',
40
+ };
41
+ return messages[status] || 'Error';
26
42
  }
27
43
  }
28
44
 
29
- module.exports = HttpError;
45
+ module.exports = HttpError;
@@ -0,0 +1,411 @@
1
+ 'use strict';
2
+
3
+ const { createFacade } = require('./Facade');
4
+
5
+ // ── Service & types re-exported for application use ───────────────────────────
6
+ const AIService = require('../ai/AIManager');
7
+ const { AIManager, PendingRequest } = require('../ai/AIManager');
8
+ const {
9
+ AIMessage, AIResponse, AIStreamEvent,
10
+ Tool, ToolBuilder, Thread, Prompt, Schema,
11
+ AIError, AIRateLimitError, AIStructuredOutputError, AIProviderError,
12
+ } = require('../ai/types');
13
+ const {
14
+ PendingImage, AIImageResponse,
15
+ PendingAudio, AIAudioResponse,
16
+ PendingTranscription, AITranscriptionResponse,
17
+ PendingReranking, AIRerankResponse,
18
+ } = require('../ai/media');
19
+ const { ConversationThread, AI_MIGRATIONS } = require('../ai/conversation');
20
+ const { AIFile, PendingFile, AIFilesAPI, AIVectorStore, AIStoresAPI } = require('../ai/files');
21
+ const { WebSearch, WebFetch, FileSearch } = require('../ai/provider_tools');
22
+
23
+ /**
24
+ * AI facade — multi-provider LLM client for Millas.
25
+ *
26
+ * Resolved from the DI container as 'ai'.
27
+ * Auto-configured from config/ai.js or environment variables.
28
+ *
29
+ * ─────────────────────────────────────────────────────────────────────────────
30
+ * SUPPORTED PROVIDERS
31
+ * ─────────────────────────────────────────────────────────────────────────────
32
+ *
33
+ * Provider | Text | Image | TTS | STT | Embed | Rerank
34
+ * ────────────┼──────┼───────┼─────┼─────┼───────┼───────
35
+ * anthropic | ✓ | | | | |
36
+ * openai | ✓ | ✓ | ✓ | ✓ | ✓ |
37
+ * gemini | ✓ | ✓ | | | ✓ |
38
+ * groq | ✓ | | | ✓ | |
39
+ * mistral | ✓ | | | ✓ | ✓ |
40
+ * xai | ✓ | ✓ | | | |
41
+ * deepseek | ✓ | | | | |
42
+ * azure | ✓ | | ✓ | ✓ | ✓ |
43
+ * cohere | ✓ | | | | ✓ | ✓
44
+ * ollama | ✓ | | | | ✓ |
45
+ * elevenlabs | | | ✓ | ✓ | |
46
+ *
47
+ * ─────────────────────────────────────────────────────────────────────────────
48
+ * CONFIGURATION (config/ai.js)
49
+ * ─────────────────────────────────────────────────────────────────────────────
50
+ *
51
+ * module.exports = {
52
+ * default: 'anthropic',
53
+ * audioProvider: 'elevenlabs', // default provider for TTS/STT
54
+ * rerankProvider: 'cohere', // default provider for reranking
55
+ * providers: {
56
+ * anthropic: { apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-sonnet-4-20250514' },
57
+ * openai: { apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o', embeddingModel: 'text-embedding-3-small' },
58
+ * gemini: { apiKey: process.env.GEMINI_API_KEY, model: 'gemini-2.0-flash' },
59
+ * groq: { apiKey: process.env.GROQ_API_KEY, model: 'llama-3.1-70b-versatile' },
60
+ * mistral: { apiKey: process.env.MISTRAL_API_KEY, model: 'mistral-large-latest' },
61
+ * xai: { apiKey: process.env.XAI_API_KEY, model: 'grok-2' },
62
+ * deepseek: { apiKey: process.env.DEEPSEEK_API_KEY, model: 'deepseek-chat' },
63
+ * cohere: { apiKey: process.env.COHERE_API_KEY },
64
+ * elevenlabs: { apiKey: process.env.ELEVENLABS_API_KEY },
65
+ * ollama: { baseUrl: 'http://localhost:11434', model: 'llama3.2' },
66
+ * azure: { apiKey: process.env.AZURE_OPENAI_KEY, endpoint: process.env.AZURE_OPENAI_ENDPOINT, deployment: 'gpt-4o', apiVersion: '2024-02-01' },
67
+ * },
68
+ * };
69
+ *
70
+ * ─────────────────────────────────────────────────────────────────────────────
71
+ * TEXT GENERATION
72
+ * ─────────────────────────────────────────────────────────────────────────────
73
+ *
74
+ * // Simplest call
75
+ * const res = await AI.text('What is the capital of Kenya?');
76
+ * console.log(res.text); // 'Nairobi'
77
+ * console.log(res.totalTokens); // 42
78
+ *
79
+ * // Full builder
80
+ * const res = await AI
81
+ * .using('openai')
82
+ * .model('gpt-4o')
83
+ * .system('You are concise.')
84
+ * .temperature(0.7)
85
+ * .maxTokens(500)
86
+ * .retry(3, 1000)
87
+ * .fallback(['anthropic', { provider: 'gemini', model: 'gemini-2.0-flash' }])
88
+ * .cache(600)
89
+ * .tokenBudget(10000)
90
+ * .generate('Explain recursion in one paragraph.');
91
+ *
92
+ * // Extended thinking (Anthropic claude-3-7+)
93
+ * const res = await AI
94
+ * .using('anthropic')
95
+ * .think(16000)
96
+ * .generate('Prove there are infinitely many primes.');
97
+ *
98
+ * console.log(res.thinking); // internal reasoning chain
99
+ * console.log(res.text); // final answer
100
+ *
101
+ * // Per-provider options
102
+ * await AI.providerOptions({
103
+ * openai: { reasoning: { effort: 'low' }, frequency_penalty: 0.5 },
104
+ * anthropic: { thinking: { budget_tokens: 1024 } },
105
+ * }).generate('...');
106
+ *
107
+ * ─────────────────────────────────────────────────────────────────────────────
108
+ * STREAMING
109
+ * ─────────────────────────────────────────────────────────────────────────────
110
+ *
111
+ * // Event-based
112
+ * for await (const event of AI.stream('Tell me a story')) {
113
+ * if (event.type === 'delta') process.stdout.write(event.data.text);
114
+ * if (event.type === 'thinking') console.log('[thinking]', event.data.text);
115
+ * if (event.type === 'tool_call') console.log('Tool called:', event.data.name);
116
+ * if (event.type === 'complete') console.log('Tokens:', event.data.totalTokens);
117
+ * }
118
+ *
119
+ * // Token callback shorthand
120
+ * const res = await AI
121
+ * .onToken(chunk => expressRes.write(chunk))
122
+ * .generate('Write a poem about Kenya');
123
+ *
124
+ * // SSE endpoint
125
+ * app.get('/chat', async (req, res) => {
126
+ * res.setHeader('Content-Type', 'text/event-stream');
127
+ * for await (const event of AI.stream(req.query.q)) {
128
+ * if (event.type === 'delta')
129
+ * res.write('data: ' + JSON.stringify({ text: event.data.text }) + '\n\n');
130
+ * if (event.type === 'complete')
131
+ * res.write('data: [DONE]\n\n');
132
+ * }
133
+ * res.end();
134
+ * });
135
+ *
136
+ * ─────────────────────────────────────────────────────────────────────────────
137
+ * STRUCTURED OUTPUT
138
+ * ─────────────────────────────────────────────────────────────────────────────
139
+ *
140
+ * const res = await AI
141
+ * .structured(Schema.define({
142
+ * sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
143
+ * confidence: { type: 'number', min: 0, max: 1 },
144
+ * summary: { type: 'string' },
145
+ * keywords: { type: 'array' },
146
+ * }))
147
+ * .generate('Analyse: "Great product, fast shipping!"');
148
+ *
149
+ * res.parsed.sentiment; // 'positive'
150
+ * res.parsed.confidence; // 0.97
151
+ *
152
+ * ─────────────────────────────────────────────────────────────────────────────
153
+ * TOOL CALLING & AGENTS
154
+ * ─────────────────────────────────────────────────────────────────────────────
155
+ *
156
+ * const weatherTool = Tool.define('get_weather')
157
+ * .description('Get real-time weather for a city')
158
+ * .parameters({
159
+ * type: 'object',
160
+ * properties: {
161
+ * city: { type: 'string' },
162
+ * units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
163
+ * },
164
+ * required: ['city'],
165
+ * })
166
+ * .handle(async ({ city, units = 'celsius' }) => WeatherAPI.get(city, units))
167
+ * .build();
168
+ *
169
+ * // Agentic loop — tool calls handled automatically
170
+ * const res = await AI
171
+ * .tools([weatherTool, calendarTool])
172
+ * .agent('Book a meeting for a sunny day in Nairobi next week');
173
+ *
174
+ * // Provider-native tools (executed by the AI provider, not your app)
175
+ * await AI.tools([new WebSearch().max(5)]).generate('What happened in AI today?');
176
+ * await AI.tools([new WebFetch().allow(['docs.example.com'])]).generate('Summarise https://...');
177
+ * await AI.tools([new FileSearch({ stores: ['vs_abc'] })]).generate('Find docs about...');
178
+ *
179
+ * ─────────────────────────────────────────────────────────────────────────────
180
+ * CONVERSATIONS (DB-persisted)
181
+ * ─────────────────────────────────────────────────────────────────────────────
182
+ *
183
+ * // Start a new conversation
184
+ * const thread = await AI.conversation.forUser(user.id, 'SalesCoach').create('Q3 Review');
185
+ * await thread.addUser('Analyse this transcript...');
186
+ * const res = await AI.withThread(thread).generate();
187
+ * await thread.addAssistant(res.text);
188
+ *
189
+ * const conversationId = thread.id; // store for later
190
+ *
191
+ * // Continue an existing conversation
192
+ * const thread = await AI.conversation.continue(conversationId);
193
+ * await thread.addUser('Tell me more about that.');
194
+ * const res = await AI.withThread(thread).generate();
195
+ *
196
+ * // List a user's conversations
197
+ * const history = await ConversationThread.list(user.id, 'SalesCoach', 20);
198
+ *
199
+ * // In-memory thread (no DB)
200
+ * const thread = AI.thread('You are helpful.').limit(20).summariseWith(async (old) => {
201
+ * return (await AI.text('Summarise: ' + old.map(m=>m.content).join('\n'))).text;
202
+ * });
203
+ * thread.addUser('Hello!');
204
+ *
205
+ * ─────────────────────────────────────────────────────────────────────────────
206
+ * IMAGE GENERATION
207
+ * ─────────────────────────────────────────────────────────────────────────────
208
+ *
209
+ * const img = await AI.image('A donut on a counter')
210
+ * .landscape() // or .portrait() or .square()
211
+ * .quality('high') // 'high' | 'standard' | 'low'
212
+ * .using('openai')
213
+ * .generate();
214
+ *
215
+ * await img.store('images/donut.png');
216
+ * const buffer = img.buffer; // raw Buffer
217
+ *
218
+ * // With reference images
219
+ * const img = await AI.image('Update this to an impressionist style')
220
+ * .attachments([AI.files.fromStorage('photo.jpg')])
221
+ * .generate();
222
+ *
223
+ * ─────────────────────────────────────────────────────────────────────────────
224
+ * AUDIO — TEXT TO SPEECH
225
+ * ─────────────────────────────────────────────────────────────────────────────
226
+ *
227
+ * const audio = await AI.speak('Welcome to Millas!')
228
+ * .female() // or .male() or .voice('voice-id')
229
+ * .instructions('Speak like a news anchor')
230
+ * .using('elevenlabs')
231
+ * .generate();
232
+ *
233
+ * await audio.store('audio/welcome.mp3');
234
+ * const buffer = audio.buffer;
235
+ *
236
+ * ─────────────────────────────────────────────────────────────────────────────
237
+ * AUDIO — SPEECH TO TEXT (TRANSCRIPTION)
238
+ * ─────────────────────────────────────────────────────────────────────────────
239
+ *
240
+ * // From file path
241
+ * const t = await AI.transcribe.fromPath('/recordings/call.mp3')
242
+ * .diarize() // include who said what
243
+ * .language('en')
244
+ * .generate();
245
+ *
246
+ * console.log(t.text); // full transcript
247
+ * console.log(t.speakers); // per-speaker segments
248
+ *
249
+ * // From storage
250
+ * const t = await AI.transcribe.fromStorage('uploads/meeting.mp3').generate();
251
+ *
252
+ * // From buffer
253
+ * const t = await AI.transcribe.fromBuffer(audioBuffer, 'audio.mp3').generate();
254
+ *
255
+ * ─────────────────────────────────────────────────────────────────────────────
256
+ * EMBEDDINGS
257
+ * ─────────────────────────────────────────────────────────────────────────────
258
+ *
259
+ * // Single text
260
+ * const [vector] = await AI.using('openai').embed('Napa Valley has great wine.');
261
+ *
262
+ * // Batch
263
+ * const vectors = await AI.embed(['text one', 'text two'], null, 'openai');
264
+ *
265
+ * // Local (Ollama)
266
+ * const [vector] = await AI.using('ollama').embed('Hello world');
267
+ *
268
+ * ─────────────────────────────────────────────────────────────────────────────
269
+ * RERANKING
270
+ * ─────────────────────────────────────────────────────────────────────────────
271
+ *
272
+ * const result = await AI
273
+ * .rerank(['Django is a Python framework.', 'Laravel is PHP.', 'React is JS.'])
274
+ * .limit(2)
275
+ * .using('cohere')
276
+ * .rerank('PHP frameworks');
277
+ *
278
+ * result.first.document; // 'Laravel is PHP.'
279
+ * result.first.score; // 0.95
280
+ * for (const r of result) console.log(r.score, r.document);
281
+ *
282
+ * ─────────────────────────────────────────────────────────────────────────────
283
+ * FILE STORAGE
284
+ * ─────────────────────────────────────────────────────────────────────────────
285
+ *
286
+ * // Upload a file to the provider
287
+ * const file = await AI.files.fromPath('/reports/q3.pdf').put();
288
+ * const file = await AI.files.fromStorage('uploads/report.pdf').put();
289
+ * const file = await AI.files.fromUrl('https://example.com/doc.pdf').put();
290
+ *
291
+ * console.log(file.id); // 'file-abc123'
292
+ *
293
+ * // Use a stored file in a prompt
294
+ * const res = await AI
295
+ * .withMessage([
296
+ * { type: 'text', text: 'Summarise this document.' },
297
+ * { type: 'file_ref', fileId: file.id },
298
+ * ])
299
+ * .generate();
300
+ *
301
+ * // Retrieve or delete
302
+ * const meta = await AI.files.fromId('file-abc').get();
303
+ * await AI.files.fromId('file-abc').delete();
304
+ *
305
+ * ─────────────────────────────────────────────────────────────────────────────
306
+ * VECTOR STORES (RAG)
307
+ * ─────────────────────────────────────────────────────────────────────────────
308
+ *
309
+ * // Create a store
310
+ * const store = await AI.stores.create('Knowledge Base');
311
+ *
312
+ * // Add files
313
+ * await store.add(AI.files.fromPath('/docs/manual.pdf'));
314
+ * await store.add('file-abc123', { author: 'Alice', year: 2025 });
315
+ *
316
+ * // Use it in a prompt with FileSearch tool
317
+ * await AI
318
+ * .tools([new FileSearch({ stores: [store.id] })])
319
+ * .generate('Find docs about authentication');
320
+ *
321
+ * // Remove a file
322
+ * await store.remove('file-abc123', { deleteFile: true });
323
+ *
324
+ * // Retrieve or delete the store
325
+ * const store = await AI.stores.get('vs_abc123');
326
+ * await AI.stores.delete('vs_abc123');
327
+ *
328
+ * ─────────────────────────────────────────────────────────────────────────────
329
+ * MIDDLEWARE
330
+ * ─────────────────────────────────────────────────────────────────────────────
331
+ *
332
+ * const logUsage = async (req, next) => {
333
+ * const start = Date.now();
334
+ * const res = await next(req);
335
+ * Log.info(`AI [${req.provider}] ${Date.now()-start}ms — ${res.totalTokens} tokens`);
336
+ * return res;
337
+ * };
338
+ *
339
+ * await AI.use(logUsage).generate('...');
340
+ *
341
+ * ─────────────────────────────────────────────────────────────────────────────
342
+ * PROMPT TEMPLATES
343
+ * ─────────────────────────────────────────────────────────────────────────────
344
+ *
345
+ * const prompt = AI.prompt('Translate "{{text}}" to {{language}}.')
346
+ * .with({ text: 'Hello', language: 'Swahili' });
347
+ *
348
+ * await AI.text(prompt.toString());
349
+ *
350
+ * ─────────────────────────────────────────────────────────────────────────────
351
+ * TESTING
352
+ * ─────────────────────────────────────────────────────────────────────────────
353
+ *
354
+ * AI.swap({
355
+ * text: async () => new AIResponse({ text: 'Mocked', provider: 'mock', model: 'mock' }),
356
+ * stream: async function*() { yield AIStreamEvent.delta('Mocked'); },
357
+ * });
358
+ * AI.restore();
359
+ *
360
+ * ─────────────────────────────────────────────────────────────────────────────
361
+ *
362
+ * @see src/ai/AIManager.js
363
+ * @see src/ai/drivers.js
364
+ * @see src/ai/types.js
365
+ * @see src/ai/media.js
366
+ * @see src/ai/conversation.js
367
+ * @see src/ai/files.js
368
+ * @see src/ai/provider_tools.js
369
+ */
370
+ class AI extends createFacade('ai') {}
371
+
372
+ module.exports = {
373
+ AI,
374
+ // Manager
375
+ AIManager,
376
+ PendingRequest,
377
+ // Core types
378
+ AIMessage,
379
+ AIResponse,
380
+ AIStreamEvent,
381
+ // Tool system
382
+ Tool,
383
+ ToolBuilder,
384
+ // Provider-native tools
385
+ WebSearch,
386
+ WebFetch,
387
+ FileSearch,
388
+ // Memory
389
+ Thread,
390
+ ConversationThread,
391
+ AI_MIGRATIONS,
392
+ // Templating & schema
393
+ Prompt,
394
+ Schema,
395
+ // Media
396
+ PendingImage, AIImageResponse,
397
+ PendingAudio, AIAudioResponse,
398
+ PendingTranscription, AITranscriptionResponse,
399
+ PendingReranking, AIRerankResponse,
400
+ // Files & vector stores
401
+ AIFile,
402
+ PendingFile,
403
+ AIFilesAPI,
404
+ AIVectorStore,
405
+ AIStoresAPI,
406
+ // Errors
407
+ AIError,
408
+ AIRateLimitError,
409
+ AIStructuredOutputError,
410
+ AIProviderError,
411
+ };