millas 0.2.12-beta-1 → 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 (120) hide show
  1. package/package.json +3 -2
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +516 -199
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +318 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +393 -97
  12. package/src/admin/static/admin.css +1422 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +87 -1046
  19. package/src/admin/views/pages/detail.njk +56 -21
  20. package/src/admin/views/pages/error.njk +65 -0
  21. package/src/admin/views/pages/form.njk +47 -599
  22. package/src/admin/views/pages/list.njk +270 -62
  23. package/src/admin/views/partials/form-field.njk +53 -0
  24. package/src/admin/views/partials/form-footer.njk +28 -0
  25. package/src/admin/views/partials/form-readonly.njk +114 -0
  26. package/src/admin/views/partials/form-scripts.njk +480 -0
  27. package/src/admin/views/partials/form-widget.njk +297 -0
  28. package/src/admin/views/partials/icons.njk +64 -0
  29. package/src/admin/views/partials/json-dialog.njk +80 -0
  30. package/src/admin/views/partials/json-editor.njk +37 -0
  31. package/src/ai/AIManager.js +954 -0
  32. package/src/ai/AITokenBudget.js +250 -0
  33. package/src/ai/PromptGuard.js +216 -0
  34. package/src/ai/agents.js +218 -0
  35. package/src/ai/conversation.js +213 -0
  36. package/src/ai/drivers.js +734 -0
  37. package/src/ai/files.js +249 -0
  38. package/src/ai/media.js +303 -0
  39. package/src/ai/pricing.js +152 -0
  40. package/src/ai/provider_tools.js +114 -0
  41. package/src/ai/types.js +356 -0
  42. package/src/auth/Auth.js +18 -2
  43. package/src/auth/AuthUser.js +65 -44
  44. package/src/cli.js +3 -1
  45. package/src/commands/createsuperuser.js +267 -0
  46. package/src/commands/lang.js +589 -0
  47. package/src/commands/migrate.js +154 -81
  48. package/src/commands/serve.js +3 -4
  49. package/src/container/AppInitializer.js +101 -20
  50. package/src/container/Application.js +31 -1
  51. package/src/container/MillasApp.js +10 -3
  52. package/src/container/MillasConfig.js +35 -6
  53. package/src/core/admin.js +5 -0
  54. package/src/core/db.js +2 -1
  55. package/src/core/foundation.js +2 -10
  56. package/src/core/lang.js +1 -0
  57. package/src/errors/HttpError.js +32 -16
  58. package/src/facades/AI.js +411 -0
  59. package/src/facades/Hash.js +67 -0
  60. package/src/facades/Process.js +144 -0
  61. package/src/hashing/Hash.js +262 -0
  62. package/src/http/HtmlEscape.js +162 -0
  63. package/src/http/MillasRequest.js +63 -7
  64. package/src/http/MillasResponse.js +70 -4
  65. package/src/http/ResponseDispatcher.js +21 -27
  66. package/src/http/SafeFilePath.js +195 -0
  67. package/src/http/SafeRedirect.js +62 -0
  68. package/src/http/SecurityBootstrap.js +70 -0
  69. package/src/http/helpers.js +40 -125
  70. package/src/http/index.js +10 -1
  71. package/src/http/middleware/CsrfMiddleware.js +258 -0
  72. package/src/http/middleware/RateLimiter.js +314 -0
  73. package/src/http/middleware/SecurityHeaders.js +281 -0
  74. package/src/i18n/I18nServiceProvider.js +91 -0
  75. package/src/i18n/Translator.js +643 -0
  76. package/src/i18n/defaults.js +122 -0
  77. package/src/i18n/index.js +164 -0
  78. package/src/i18n/locales/en.js +55 -0
  79. package/src/i18n/locales/sw.js +48 -0
  80. package/src/logger/LogRedactor.js +247 -0
  81. package/src/logger/Logger.js +1 -1
  82. package/src/logger/formatters/JsonFormatter.js +11 -4
  83. package/src/logger/formatters/PrettyFormatter.js +103 -65
  84. package/src/logger/formatters/SimpleFormatter.js +14 -3
  85. package/src/middleware/ThrottleMiddleware.js +27 -4
  86. package/src/migrations/system/0001_users.js +21 -0
  87. package/src/migrations/system/0002_admin_log.js +25 -0
  88. package/src/migrations/system/0003_sessions.js +23 -0
  89. package/src/orm/fields/index.js +210 -188
  90. package/src/orm/migration/DefaultValueParser.js +325 -0
  91. package/src/orm/migration/InteractiveResolver.js +191 -0
  92. package/src/orm/migration/Makemigrations.js +312 -0
  93. package/src/orm/migration/MigrationGraph.js +227 -0
  94. package/src/orm/migration/MigrationRunner.js +202 -108
  95. package/src/orm/migration/MigrationWriter.js +463 -0
  96. package/src/orm/migration/ModelInspector.js +143 -74
  97. package/src/orm/migration/ModelScanner.js +225 -0
  98. package/src/orm/migration/ProjectState.js +213 -0
  99. package/src/orm/migration/RenameDetector.js +175 -0
  100. package/src/orm/migration/SchemaBuilder.js +8 -81
  101. package/src/orm/migration/operations/base.js +57 -0
  102. package/src/orm/migration/operations/column.js +191 -0
  103. package/src/orm/migration/operations/fields.js +252 -0
  104. package/src/orm/migration/operations/index.js +55 -0
  105. package/src/orm/migration/operations/models.js +152 -0
  106. package/src/orm/migration/operations/registry.js +131 -0
  107. package/src/orm/migration/operations/special.js +51 -0
  108. package/src/orm/migration/utils.js +208 -0
  109. package/src/orm/model/Model.js +81 -13
  110. package/src/process/Process.js +333 -0
  111. package/src/providers/AdminServiceProvider.js +66 -9
  112. package/src/providers/AuthServiceProvider.js +40 -5
  113. package/src/providers/CacheStorageServiceProvider.js +2 -2
  114. package/src/providers/DatabaseServiceProvider.js +3 -2
  115. package/src/providers/LogServiceProvider.js +4 -1
  116. package/src/providers/MailServiceProvider.js +1 -1
  117. package/src/providers/QueueServiceProvider.js +1 -1
  118. package/src/router/MiddlewareRegistry.js +27 -2
  119. package/src/scaffold/templates.js +80 -21
  120. package/src/validation/Validator.js +348 -607
@@ -25,6 +25,11 @@ const AppInitializer = require("./AppInitializer");
25
25
  class MillasConfig {
26
26
  constructor() {
27
27
  this._config = {
28
+ // Absolute path to the project root — passed to all providers
29
+ // so they never need to call process.cwd() at runtime.
30
+ // Set via .configure(__dirname) in bootstrap/app.js.
31
+ basePath: null,
32
+
28
33
  // Service providers
29
34
  providers: [],
30
35
 
@@ -35,13 +40,14 @@ class MillasConfig {
35
40
  middleware: [],
36
41
 
37
42
  // Core service toggles
43
+ logging: true,
38
44
  database: true,
39
- cache: true,
40
- storage: true,
41
- mail: true,
42
- queue: true,
43
- events: true,
44
- logging: true,
45
+ auth: true, // AuthServiceProvider — always on by default
46
+ cache: true,
47
+ storage: true,
48
+ mail: true,
49
+ queue: true,
50
+ events: true,
45
51
 
46
52
  // Admin panel — null means disabled, {} or options object means enabled
47
53
  admin: null,
@@ -57,6 +63,22 @@ class MillasConfig {
57
63
 
58
64
  // ── Chainable config methods ───────────────────────────────────────────────
59
65
 
66
+ /**
67
+ * Set the application base path. Must be the first call.
68
+ * Pass __dirname from bootstrap/app.js — Laravel style.
69
+ *
70
+ * Millas.configure(__dirname)
71
+ *
72
+ * All providers use this to locate config files, models, and routes
73
+ * without relying on process.cwd() at runtime.
74
+ *
75
+ * @param {string} basePath — absolute path to the project root
76
+ */
77
+ configure(basePath) {
78
+ this._config.basePath = basePath;
79
+ return this;
80
+ }
81
+
60
82
  /**
61
83
  * Register application service providers.
62
84
  *
@@ -93,8 +115,15 @@ class MillasConfig {
93
115
  /**
94
116
  * Enable the admin panel.
95
117
  *
118
+ * AuthServiceProvider and AdminServiceProvider are registered
119
+ * automatically — no need to add them to .providers([]).
120
+ *
96
121
  * .withAdmin()
97
122
  * .withAdmin({ prefix: '/cms', title: 'My CMS' })
123
+ *
124
+ * First-time setup:
125
+ * millas migrate # creates users + admin_log + sessions tables
126
+ * millas createsuperuser # creates your first admin user
98
127
  */
99
128
  withAdmin(options = {}) {
100
129
  this._config.admin = options;
@@ -0,0 +1,5 @@
1
+ // ── Admin ─────────────────────────────────────────────────────────
2
+ const {Admin, AdminResource, AdminField, AdminFilter, AdminHooks, AdminInline} = require('../admin');
3
+ module.exports = {
4
+ Admin, AdminResource, AdminField, AdminFilter, AdminHooks, AdminInline
5
+ }
package/src/core/db.js CHANGED
@@ -2,7 +2,8 @@ const {
2
2
  Model,
3
3
  fields
4
4
  } = require("../orm");
5
+ const { migrations } = require("../orm/migration/operations");
5
6
 
6
7
  module.exports = {
7
- Model, fields
8
+ Model, fields,migrations
8
9
  }
@@ -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,12 +55,5 @@ module.exports = {
56
55
  Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
57
56
  // Storage
58
57
  Storage, LocalDriver, StorageServiceProvider,
59
- };
60
-
61
- // ── Admin ─────────────────────────────────────────────────────────
62
- const { Admin, AdminResource, AdminField, AdminFilter } = require('../admin');
63
- const AdminServiceProvider = require('../providers/AdminServiceProvider');
64
-
65
- Object.assign(module.exports, {
66
- Admin, AdminResource, AdminField, AdminFilter, AdminServiceProvider,
67
- });
58
+ Log: require('../logger').Log,
59
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('../i18n')
@@ -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
+ };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const { createFacade } = require('./Facade');
4
+ const { HashManager, BcryptDriver } = require('../hashing/Hash');
5
+
6
+ /**
7
+ * Hash facade — Laravel-style hashing.
8
+ *
9
+ * Resolved from the DI container as 'hash'.
10
+ * Falls back to the exported singleton if used before the container boots
11
+ * (e.g. in migrations or standalone scripts).
12
+ *
13
+ * @class
14
+ *
15
+ * ── Core ─────────────────────────────────────────────────────────────────────
16
+ * @property {function(string, object=): Promise<string>} make
17
+ * Hash a plain-text value.
18
+ * Options: { rounds: 14 } — overrides the configured rounds for this call.
19
+ *
20
+ * @property {function(string, string): Promise<boolean>} check
21
+ * Verify a plain-text value against a stored hash.
22
+ * Returns false (never throws) if either argument is falsy.
23
+ *
24
+ * @property {function(string): boolean} needsRehash
25
+ * Returns true if the hash was made with different rounds than currently
26
+ * configured. Use after login to silently upgrade stored hashes:
27
+ * if (Hash.needsRehash(user.password)) {
28
+ * user.password = await Hash.make(plaintext);
29
+ * await user.save();
30
+ * }
31
+ *
32
+ * ── Introspection ─────────────────────────────────────────────────────────────
33
+ * @property {function(string): { alg: string, rounds: number }} info
34
+ * Return metadata about a stored hash.
35
+ * Example: Hash.info(hash) → { alg: 'bcrypt', rounds: 12 }
36
+ *
37
+ * @property {function(string): boolean} isHashed
38
+ * Return true if the value looks like a bcrypt hash.
39
+ * Guards against accidentally double-hashing:
40
+ * if (!Hash.isHashed(value)) value = await Hash.make(value);
41
+ *
42
+ * ── Driver access ─────────────────────────────────────────────────────────────
43
+ * @property {function(string=): BcryptDriver} driver
44
+ * Get a specific driver instance:
45
+ * Hash.driver('bcrypt').make('secret', { rounds: 14 });
46
+ *
47
+ * ── Configuration ─────────────────────────────────────────────────────────────
48
+ * @property {function(number): HashManager} setRounds
49
+ * Change the bcrypt rounds at runtime. Busts the driver cache.
50
+ * Useful in tests: Hash.setRounds(4);
51
+ *
52
+ * @property {function(): number} getRounds
53
+ * Return the currently configured bcrypt rounds.
54
+ *
55
+ * ── Testing ───────────────────────────────────────────────────────────────────
56
+ * @property {function(object): void} swap
57
+ * Swap the underlying instance for a fake:
58
+ * Hash.swap({ make: async () => 'hashed', check: async () => true });
59
+ *
60
+ * @property {function(): void} restore
61
+ * Restore the real implementation after a swap.
62
+ *
63
+ * @see src/hashing/Hash.js
64
+ */
65
+ class Hash extends createFacade('hash') {}
66
+
67
+ module.exports = Hash