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
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ const { AIMessage } = require('./types');
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // ConversationThread — DB-persisted conversation history
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ /**
10
+ * DB-persisted conversation thread.
11
+ * Requires knex to be available via DatabaseManager.
12
+ *
13
+ * Tables created by the AI migration:
14
+ * ai_conversations — id, user_id, agent, title, meta, created_at, updated_at
15
+ * ai_conversation_messages — id, conversation_id, role, content, meta, created_at
16
+ */
17
+ class ConversationThread {
18
+ constructor({ id = null, userId = null, agent = null, systemPrompt = null, db = null } = {}) {
19
+ this._id = id;
20
+ this._userId = userId;
21
+ this._agent = agent;
22
+ this._systemPrompt = systemPrompt;
23
+ this._messages = [];
24
+ this._loaded = false;
25
+ this._db = db;
26
+ this._maxMessages = null;
27
+ this._summaryFn = null;
28
+ }
29
+
30
+ // ── Configuration ──────────────────────────────────────────────────────────
31
+
32
+ system(prompt) { this._systemPrompt = prompt; return this; }
33
+ limit(n) { this._maxMessages = n; return this; }
34
+ summariseWith(fn) { this._summaryFn = fn; return this; }
35
+ get id() { return this._id; }
36
+ get userId() { return this._userId; }
37
+
38
+ // ── Persistence ────────────────────────────────────────────────────────────
39
+
40
+ /** Create a new conversation in the DB and return this thread. */
41
+ async create(title = null) {
42
+ const db = this._requireDb();
43
+ const [id] = await db('ai_conversations').insert({
44
+ user_id: this._userId,
45
+ agent: this._agent,
46
+ title: title || (this._agent ? `${this._agent} conversation` : 'New conversation'),
47
+ meta: JSON.stringify({}),
48
+ created_at: new Date(),
49
+ updated_at: new Date(),
50
+ });
51
+ this._id = id;
52
+ return this;
53
+ }
54
+
55
+ /** Load messages from the DB. */
56
+ async load() {
57
+ if (!this._id) throw new Error('Cannot load: conversation not yet created. Call create() first.');
58
+ const db = this._requireDb();
59
+ const rows = await db('ai_conversation_messages').where('conversation_id', this._id).orderBy('created_at', 'asc');
60
+ this._messages = rows.map(r => new AIMessage(r.role, this._parseContent(r.content), r.meta ? JSON.parse(r.meta) : {}));
61
+ this._loaded = true;
62
+ return this;
63
+ }
64
+
65
+ /** Add a message and persist it to the DB. */
66
+ async addAndSave(role, content) {
67
+ const msg = new AIMessage(role, content);
68
+ this._messages.push(msg);
69
+ if (this._id) {
70
+ const db = this._requireDb();
71
+ await db('ai_conversation_messages').insert({
72
+ conversation_id: this._id,
73
+ role,
74
+ content: typeof content === 'string' ? content : JSON.stringify(content),
75
+ meta: JSON.stringify({}),
76
+ created_at: new Date(),
77
+ });
78
+ await db('ai_conversations').where('id', this._id).update({ updated_at: new Date() });
79
+ }
80
+ return this;
81
+ }
82
+
83
+ /** Add user message and persist. */
84
+ addUser(content) { return this.addAndSave('user', content); }
85
+ /** Add assistant message and persist. */
86
+ addAssistant(content) { return this.addAndSave('assistant', content); }
87
+
88
+ /** In-memory only — no persistence. */
89
+ addLocal(role, content) { this._messages.push(new AIMessage(role, content)); return this; }
90
+
91
+ /** Delete this conversation and all its messages from the DB. */
92
+ async delete() {
93
+ if (!this._id) return;
94
+ const db = this._requireDb();
95
+ await db('ai_conversation_messages').where('conversation_id', this._id).delete();
96
+ await db('ai_conversations').where('id', this._id).delete();
97
+ this._id = null;
98
+ }
99
+
100
+ // ── Memory management ──────────────────────────────────────────────────────
101
+
102
+ async toArray() {
103
+ let msgs = [...this._messages];
104
+ if (this._maxMessages && msgs.length > this._maxMessages) {
105
+ const overflow = msgs.slice(0, msgs.length - this._maxMessages);
106
+ msgs = msgs.slice(msgs.length - this._maxMessages);
107
+ if (this._summaryFn) {
108
+ const summary = await this._summaryFn(overflow);
109
+ msgs.unshift(new AIMessage('system', `Earlier conversation summary:\n${summary}`));
110
+ }
111
+ }
112
+ return msgs.map(m => m.toJSON());
113
+ }
114
+
115
+ get length() { return this._messages.length; }
116
+ get lastReply() {
117
+ const last = [...this._messages].reverse().find(m => m.role === 'assistant');
118
+ return last ? (typeof last.content === 'string' ? last.content : last.content?.[0]?.text || '') : null;
119
+ }
120
+
121
+ clear() { this._messages = []; return this; }
122
+
123
+ // ── Static factory methods ─────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Start a new conversation for a user.
127
+ * const thread = await ConversationThread.forUser(user.id, 'SalesCoach').create();
128
+ */
129
+ static forUser(userId, agent = null, db = null) {
130
+ return new ConversationThread({ userId, agent, db: db || ConversationThread._db });
131
+ }
132
+
133
+ /**
134
+ * Continue an existing conversation by ID.
135
+ * const thread = await ConversationThread.continue(conversationId, db).load();
136
+ */
137
+ static async continue(id, db = null) {
138
+ const thread = new ConversationThread({ id, db: db || ConversationThread._db });
139
+ await thread.load();
140
+ return thread;
141
+ }
142
+
143
+ /**
144
+ * List conversations for a user.
145
+ * const convos = await ConversationThread.forUser(userId).list();
146
+ */
147
+ static async list(userId, agent = null, limit = 50) {
148
+ const db = ConversationThread._db;
149
+ if (!db) throw new Error('Database not available.');
150
+ let q = db('ai_conversations').where('user_id', userId).orderBy('updated_at', 'desc').limit(limit);
151
+ if (agent) q = q.where('agent', agent);
152
+ return q;
153
+ }
154
+
155
+ // ── Internal ───────────────────────────────────────────────────────────────
156
+
157
+ _requireDb() {
158
+ const db = this._db || ConversationThread._db;
159
+ if (!db) throw new Error('Database not available. The AI ConversationThread requires a knex instance. Make sure DatabaseServiceProvider is registered.');
160
+ return db;
161
+ }
162
+
163
+ _parseContent(raw) {
164
+ try { return JSON.parse(raw); } catch { return raw; }
165
+ }
166
+ }
167
+
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // Migration — creates the AI conversation tables
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+
172
+ const AI_MIGRATIONS = {
173
+ dependencies: [],
174
+ operations: [],
175
+
176
+ /**
177
+ * Run this to create the AI conversation tables.
178
+ * Called automatically by `millas migrate` if registered.
179
+ */
180
+ async up(knex) {
181
+ const hasConversations = await knex.schema.hasTable('ai_conversations');
182
+ if (!hasConversations) {
183
+ await knex.schema.createTable('ai_conversations', table => {
184
+ table.increments('id').primary();
185
+ table.string('user_id').nullable().index();
186
+ table.string('agent').nullable().index();
187
+ table.string('title').nullable();
188
+ table.text('meta').nullable();
189
+ table.timestamps(true, true);
190
+ });
191
+ }
192
+
193
+ const hasMessages = await knex.schema.hasTable('ai_conversation_messages');
194
+ if (!hasMessages) {
195
+ await knex.schema.createTable('ai_conversation_messages', table => {
196
+ table.increments('id').primary();
197
+ table.integer('conversation_id').unsigned().notNullable().references('id').inTable('ai_conversations').onDelete('CASCADE');
198
+ table.string('role').notNullable();
199
+ table.text('content').notNullable();
200
+ table.text('meta').nullable();
201
+ table.timestamp('created_at').defaultTo(knex.fn.now());
202
+ table.index('conversation_id');
203
+ });
204
+ }
205
+ },
206
+
207
+ async down(knex) {
208
+ await knex.schema.dropTableIfExists('ai_conversation_messages');
209
+ await knex.schema.dropTableIfExists('ai_conversations');
210
+ },
211
+ };
212
+
213
+ module.exports = { ConversationThread, AI_MIGRATIONS };