@airoom/nextmin-node 2.0.1 → 2.0.3

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 (45) hide show
  1. package/README.md +151 -0
  2. package/dist/api/apiRouter.d.ts +28 -1
  3. package/dist/api/apiRouter.js +138 -8
  4. package/dist/api/router/mountBatchRoutes.d.ts +2 -0
  5. package/dist/api/router/mountBatchRoutes.js +90 -0
  6. package/dist/api/router/mountCrudRoutes.js +79 -20
  7. package/dist/api/router/setupAuthRoutes.js +475 -38
  8. package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
  9. package/dist/api/router/setupChatWidgetRoutes.js +207 -0
  10. package/dist/api/router/setupFileRoutes.js +258 -12
  11. package/dist/api/router/utils.d.ts +4 -1
  12. package/dist/api/router/utils.js +25 -7
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +145 -64
  15. package/dist/database/DatabaseAdapter.d.ts +2 -0
  16. package/dist/database/NMAdapter.d.ts +11 -0
  17. package/dist/database/NMAdapter.js +688 -73
  18. package/dist/database/QueryEngine.js +7 -4
  19. package/dist/files/FileStorageAdapter.d.ts +6 -0
  20. package/dist/files/LocalFileStorageAdapter.d.ts +6 -0
  21. package/dist/files/LocalFileStorageAdapter.js +39 -3
  22. package/dist/files/S3FileStorageAdapter.d.ts +8 -1
  23. package/dist/files/S3FileStorageAdapter.js +86 -11
  24. package/dist/files/filename.js +6 -4
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +3 -1
  27. package/dist/models/BaseModel.d.ts +19 -2
  28. package/dist/models/BaseModel.js +4 -4
  29. package/dist/policy/authorize.d.ts +1 -1
  30. package/dist/policy/authorize.js +64 -13
  31. package/dist/schemas/Users.json +20 -10
  32. package/dist/services/AggregateService.d.ts +8 -0
  33. package/dist/services/AggregateService.js +87 -0
  34. package/dist/services/IndexingService.d.ts +24 -0
  35. package/dist/services/IndexingService.js +555 -0
  36. package/dist/services/LocalAIService.d.ts +34 -0
  37. package/dist/services/LocalAIService.js +471 -0
  38. package/dist/services/OpenAIService.d.ts +16 -0
  39. package/dist/services/OpenAIService.js +586 -0
  40. package/dist/utils/Events.js +1 -1
  41. package/dist/utils/Logger.d.ts +2 -1
  42. package/dist/utils/Logger.js +15 -6
  43. package/dist/utils/SchemaLoader.d.ts +2 -2
  44. package/dist/utils/SchemaLoader.js +44 -6
  45. package/package.json +12 -4
@@ -0,0 +1,586 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OpenAIService = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const Logger_1 = __importDefault(require("../utils/Logger"));
10
+ /**
11
+ * Helper to fetch with retry on HTTP 429 (Rate Limit).
12
+ * Dynamically parses the wait time from OpenAI's error messages.
13
+ */
14
+ async function fetchWithRetry(url, options, retries = 5, baseDelay = 2000) {
15
+ const response = await fetch(url, options);
16
+ if (response.status === 429 && retries > 0) {
17
+ const responseText = await response.clone().text();
18
+ let waitTime = baseDelay;
19
+ try {
20
+ const errObj = JSON.parse(responseText);
21
+ const msg = errObj?.error?.message;
22
+ // OpenAI message format: "please slow down and try again after 43.912857 seconds"
23
+ const match = msg?.match(/try again after (\d+\.?\d*) seconds/i);
24
+ if (match && match[1]) {
25
+ waitTime = (parseFloat(match[1]) + 1.5) * 1000; // convert to ms + 1.5s safety buffer
26
+ }
27
+ }
28
+ catch { }
29
+ const seconds = Math.ceil(waitTime / 1000);
30
+ console.log(`[OpenAIService] Rate limit exceeded. Waiting ${seconds} seconds before retrying (Remaining retries: ${retries})...`);
31
+ await new Promise(resolve => setTimeout(resolve, waitTime));
32
+ return fetchWithRetry(url, options, retries - 1, baseDelay * 2);
33
+ }
34
+ return response;
35
+ }
36
+ class OpenAIService {
37
+ static getConfigPath() {
38
+ return path_1.default.join(process.cwd(), '.nextmin', 'config.json');
39
+ }
40
+ static readOpenAIMetadata() {
41
+ const p = this.getConfigPath();
42
+ if (!fs_1.default.existsSync(p))
43
+ return {};
44
+ try {
45
+ const content = JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
46
+ return content.openai || {};
47
+ }
48
+ catch {
49
+ return {};
50
+ }
51
+ }
52
+ static writeOpenAIMetadata(metadata) {
53
+ const p = this.getConfigPath();
54
+ try {
55
+ let config = {};
56
+ if (fs_1.default.existsSync(p)) {
57
+ try {
58
+ config = JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
59
+ }
60
+ catch { }
61
+ }
62
+ config.openai = {
63
+ ...(config.openai || {}),
64
+ ...metadata
65
+ };
66
+ fs_1.default.writeFileSync(p, JSON.stringify(config, null, 2), 'utf-8');
67
+ }
68
+ catch (err) {
69
+ Logger_1.default.error('OpenAIService', 'Failed to write OpenAI metadata to config.json', err);
70
+ }
71
+ }
72
+ /**
73
+ * Syncs local scraped text files to the OpenAI Vector Store.
74
+ * Uses parallel file uploads and batch attachments to optimize speed.
75
+ */
76
+ async syncFilesToVectorStore(force = false) {
77
+ const apiKey = process.env.OPENAI_API_KEY;
78
+ if (!apiKey) {
79
+ Logger_1.default.warn('OpenAIService', 'OPENAI_API_KEY not found in environment. Skipping OpenAI sync.');
80
+ return;
81
+ }
82
+ const metadata = OpenAIService.readOpenAIMetadata();
83
+ let vectorStoreId = metadata.vectorStoreId;
84
+ const fileIds = metadata.fileIds || {};
85
+ const fileHashes = metadata.fileHashes || {};
86
+ // If force is true, reset and delete the existing vector store
87
+ if (force && vectorStoreId) {
88
+ Logger_1.default.info('OpenAIService', `Force rebuild requested. Deleting old OpenAI vector store: ${vectorStoreId}`);
89
+ try {
90
+ await fetchWithRetry(`https://api.openai.com/v1/vector_stores/${vectorStoreId}`, {
91
+ method: 'DELETE',
92
+ headers: {
93
+ 'Authorization': `Bearer ${apiKey}`,
94
+ 'OpenAI-Beta': 'assistants=v2'
95
+ }
96
+ });
97
+ }
98
+ catch (err) {
99
+ Logger_1.default.warn('OpenAIService', `Failed to delete old vector store ${vectorStoreId}`, err);
100
+ }
101
+ // Also delete all existing files in metadata from OpenAI
102
+ const existingFileIds = Object.values(fileIds);
103
+ if (existingFileIds.length > 0) {
104
+ Logger_1.default.info('OpenAIService', `Deleting ${existingFileIds.length} existing files from OpenAI...`);
105
+ const fileDeletePromises = existingFileIds.map(async (fileId) => {
106
+ try {
107
+ await fetchWithRetry(`https://api.openai.com/v1/files/${fileId}`, {
108
+ method: 'DELETE',
109
+ headers: {
110
+ 'Authorization': `Bearer ${apiKey}`
111
+ }
112
+ });
113
+ }
114
+ catch (err) {
115
+ // ignore
116
+ }
117
+ });
118
+ await Promise.all(fileDeletePromises);
119
+ }
120
+ vectorStoreId = '';
121
+ // Reset in-memory maps
122
+ for (const key of Object.keys(fileIds))
123
+ delete fileIds[key];
124
+ for (const key of Object.keys(fileHashes))
125
+ delete fileHashes[key];
126
+ // Save the cleared state to disk immediately
127
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId: '', fileIds: {}, fileHashes: {} });
128
+ }
129
+ const manifestPath = path_1.default.join(process.cwd(), '.nextmin', 'indexing-manifest.json');
130
+ if (!fs_1.default.existsSync(manifestPath)) {
131
+ Logger_1.default.warn('OpenAIService', 'indexing-manifest.json not found. Skipping file sync.');
132
+ return;
133
+ }
134
+ let manifest = {};
135
+ try {
136
+ manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
137
+ }
138
+ catch {
139
+ Logger_1.default.error('OpenAIService', 'Failed to parse indexing-manifest.json');
140
+ return;
141
+ }
142
+ const currentHashes = manifest.hashes || {};
143
+ const scrappedPagesDir = path_1.default.join(process.cwd(), '.nextmin', 'scraped-pages');
144
+ // 1. Create Vector Store if it doesn't exist
145
+ if (!vectorStoreId) {
146
+ Logger_1.default.info('OpenAIService', 'Creating OpenAI vector store...');
147
+ try {
148
+ const vsRes = await fetchWithRetry('https://api.openai.com/v1/vector_stores', {
149
+ method: 'POST',
150
+ headers: {
151
+ 'Authorization': `Bearer ${apiKey}`,
152
+ 'Content-Type': 'application/json',
153
+ 'OpenAI-Beta': 'assistants=v2'
154
+ },
155
+ body: JSON.stringify({ name: 'NextMin Scraped Pages Store' })
156
+ });
157
+ if (!vsRes.ok) {
158
+ const errBody = await vsRes.text();
159
+ throw new Error(`Failed to create OpenAI vector store: ${vsRes.statusText}. Response: ${errBody}`);
160
+ }
161
+ const vsData = await vsRes.json();
162
+ vectorStoreId = vsData.id;
163
+ Logger_1.default.info('OpenAIService', `Created OpenAI vector store: ${vectorStoreId}`);
164
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
165
+ }
166
+ catch (err) {
167
+ Logger_1.default.error('OpenAIService', 'Failed to initialize OpenAI vector store', err);
168
+ throw err;
169
+ }
170
+ }
171
+ // Auto-Recovery: If fileIds is empty, but we have a vectorStoreId,
172
+ // fetch all files from OpenAI (paginated) and reconstruct the local config mapping.
173
+ if (Object.keys(fileIds).length === 0) {
174
+ Logger_1.default.info('OpenAIService', 'Local file mappings are empty. Fetching uploaded files from OpenAI to reconstruct configuration...');
175
+ try {
176
+ let hasMore = true;
177
+ let after = '';
178
+ let restoredCount = 0;
179
+ const totalToFind = Object.keys(currentHashes).length;
180
+ let page = 1;
181
+ while (hasMore) {
182
+ Logger_1.default.info('OpenAIService', `Fetching page ${page} of files from OpenAI...`);
183
+ const url = 'https://api.openai.com/v1/files?limit=10000' + (after ? `&after=${after}` : '');
184
+ const filesRes = await fetchWithRetry(url, {
185
+ method: 'GET',
186
+ headers: {
187
+ 'Authorization': `Bearer ${apiKey}`
188
+ }
189
+ });
190
+ if (!filesRes.ok) {
191
+ const errBody = await filesRes.text();
192
+ throw new Error(`Failed to list files from OpenAI: ${filesRes.statusText}. Response: ${errBody}`);
193
+ }
194
+ const filesData = await filesRes.json();
195
+ const openAIFiles = filesData.data || [];
196
+ if (openAIFiles.length === 0)
197
+ break;
198
+ for (const file of openAIFiles) {
199
+ if (file.filename && file.filename.startsWith('page-') && file.filename.endsWith('.txt')) {
200
+ const key = file.filename.slice(0, -4); // Remove .txt
201
+ // Restoring only if the file is present in our current local manifest and not already mapped
202
+ if (currentHashes[key] && !fileIds[key]) {
203
+ fileIds[key] = file.id;
204
+ fileHashes[key] = currentHashes[key]; // Assume contents match for now
205
+ restoredCount++;
206
+ }
207
+ }
208
+ }
209
+ // If we have successfully recovered mappings for all local files, we can stop early
210
+ if (Object.keys(fileIds).length >= totalToFind) {
211
+ Logger_1.default.info('OpenAIService', 'All local file mappings have been recovered. Stopping search early.');
212
+ break;
213
+ }
214
+ hasMore = filesData.has_more;
215
+ if (hasMore && openAIFiles.length > 0) {
216
+ after = openAIFiles[openAIFiles.length - 1].id;
217
+ page++;
218
+ }
219
+ else {
220
+ break;
221
+ }
222
+ }
223
+ if (restoredCount > 0) {
224
+ Logger_1.default.info('OpenAIService', `Successfully restored ${restoredCount} file mappings from OpenAI.`);
225
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
226
+ }
227
+ }
228
+ catch (err) {
229
+ Logger_1.default.error('OpenAIService', 'Failed to auto-recover file mappings from OpenAI. Aborting sync to prevent file duplication.', err);
230
+ throw err;
231
+ }
232
+ }
233
+ // 2. Identify files to delete/prune
234
+ const deletePromises = [];
235
+ const keysToDelete = [];
236
+ for (const key of Object.keys(fileIds)) {
237
+ const localHash = currentHashes[key];
238
+ const openAIHash = fileHashes[key];
239
+ const fileId = fileIds[key];
240
+ if (!localHash || localHash !== openAIHash) {
241
+ Logger_1.default.info('OpenAIService', `Queuing pruning of outdated/deleted file on OpenAI: ${key} (${fileId})`);
242
+ keysToDelete.push(key);
243
+ const deleteProc = (async () => {
244
+ try {
245
+ // Delete from vector store
246
+ await fetchWithRetry(`https://api.openai.com/v1/vector_stores/${vectorStoreId}/files/${fileId}`, {
247
+ method: 'DELETE',
248
+ headers: {
249
+ 'Authorization': `Bearer ${apiKey}`,
250
+ 'OpenAI-Beta': 'assistants=v2'
251
+ }
252
+ });
253
+ // Delete the file itself
254
+ await fetchWithRetry(`https://api.openai.com/v1/files/${fileId}`, {
255
+ method: 'DELETE',
256
+ headers: {
257
+ 'Authorization': `Bearer ${apiKey}`
258
+ }
259
+ });
260
+ }
261
+ catch (err) {
262
+ Logger_1.default.warn('OpenAIService', `Failed to delete file ${fileId} from OpenAI`, err);
263
+ }
264
+ })();
265
+ deletePromises.push(deleteProc);
266
+ }
267
+ }
268
+ // Wait for all deletions to finish in parallel
269
+ if (deletePromises.length > 0) {
270
+ await Promise.all(deletePromises);
271
+ for (const key of keysToDelete) {
272
+ delete fileIds[key];
273
+ delete fileHashes[key];
274
+ }
275
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
276
+ }
277
+ // 3. Identify files to upload
278
+ const keysToUpload = [];
279
+ for (const [key, localHash] of Object.entries(currentHashes)) {
280
+ if (!fileIds[key]) {
281
+ keysToUpload.push(key);
282
+ }
283
+ }
284
+ if (keysToUpload.length > 0) {
285
+ Logger_1.default.info('OpenAIService', `Uploading and syncing ${keysToUpload.length} files to OpenAI...`);
286
+ const totalFiles = keysToUpload.length;
287
+ let uploadedCount = 0;
288
+ const BATCH_SIZE = 1500;
289
+ const CONCURRENCY_LIMIT = 15;
290
+ for (let i = 0; i < keysToUpload.length; i += BATCH_SIZE) {
291
+ const batchKeys = keysToUpload.slice(i, i + BATCH_SIZE);
292
+ const batchFileIds = [];
293
+ const batchSuccessfulKeys = [];
294
+ // Upload the batch files in parallel (up to CONCURRENCY_LIMIT concurrently)
295
+ for (let j = 0; j < batchKeys.length; j += CONCURRENCY_LIMIT) {
296
+ const subBatchKeys = batchKeys.slice(j, j + CONCURRENCY_LIMIT);
297
+ const uploadPromises = subBatchKeys.map(async (key) => {
298
+ const filePath = path_1.default.join(scrappedPagesDir, `${key}.txt`);
299
+ if (!fs_1.default.existsSync(filePath)) {
300
+ Logger_1.default.warn('OpenAIService', `Scraped file not found locally: ${filePath}`);
301
+ return;
302
+ }
303
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
304
+ try {
305
+ const formData = new FormData();
306
+ formData.append('purpose', 'assistants');
307
+ const blob = new Blob([content], { type: 'text/plain' });
308
+ formData.append('file', blob, `${key}.txt`);
309
+ const uploadRes = await fetchWithRetry('https://api.openai.com/v1/files', {
310
+ method: 'POST',
311
+ headers: {
312
+ 'Authorization': `Bearer ${apiKey}`
313
+ },
314
+ body: formData
315
+ });
316
+ if (!uploadRes.ok) {
317
+ const errBody = await uploadRes.text();
318
+ throw new Error(`Upload failed: ${uploadRes.statusText}. Response: ${errBody}`);
319
+ }
320
+ const uploadData = await uploadRes.json();
321
+ batchFileIds.push(uploadData.id);
322
+ batchSuccessfulKeys.push(key);
323
+ // Map in memory
324
+ fileIds[key] = uploadData.id;
325
+ fileHashes[key] = currentHashes[key];
326
+ }
327
+ catch (err) {
328
+ Logger_1.default.error('OpenAIService', `Failed to upload file ${key} to OpenAI`, err);
329
+ }
330
+ finally {
331
+ uploadedCount++;
332
+ if (uploadedCount % 25 === 0 || uploadedCount === totalFiles) {
333
+ const pct = ((uploadedCount / totalFiles) * 100).toFixed(1);
334
+ console.log(`[OpenAIService] Sync Progress: ${uploadedCount}/${totalFiles} files (${pct}%)...`);
335
+ }
336
+ }
337
+ });
338
+ await Promise.all(uploadPromises);
339
+ }
340
+ // Attach this chunk to the vector store immediately
341
+ if (batchFileIds.length > 0) {
342
+ Logger_1.default.info('OpenAIService', `Attaching chunk of ${batchFileIds.length} files to Vector Store...`);
343
+ try {
344
+ const batchRes = await fetchWithRetry(`https://api.openai.com/v1/vector_stores/${vectorStoreId}/file_batches`, {
345
+ method: 'POST',
346
+ headers: {
347
+ 'Authorization': `Bearer ${apiKey}`,
348
+ 'Content-Type': 'application/json',
349
+ 'OpenAI-Beta': 'assistants=v2'
350
+ },
351
+ body: JSON.stringify({ file_ids: batchFileIds })
352
+ });
353
+ if (!batchRes.ok) {
354
+ const errBody = await batchRes.clone().text();
355
+ let isNotFoundError = false;
356
+ try {
357
+ const errObj = JSON.parse(errBody);
358
+ if (errObj?.error?.message?.includes('No vector store found')) {
359
+ isNotFoundError = true;
360
+ }
361
+ }
362
+ catch { }
363
+ if (isNotFoundError || batchRes.status === 404) {
364
+ Logger_1.default.warn('OpenAIService', `Vector store ${vectorStoreId} was not found on OpenAI. Recreating one...`);
365
+ const vsRes = await fetchWithRetry('https://api.openai.com/v1/vector_stores', {
366
+ method: 'POST',
367
+ headers: {
368
+ 'Authorization': `Bearer ${apiKey}`,
369
+ 'Content-Type': 'application/json',
370
+ 'OpenAI-Beta': 'assistants=v2'
371
+ },
372
+ body: JSON.stringify({ name: 'NextMin Scraped Pages Store' })
373
+ });
374
+ if (vsRes.ok) {
375
+ const vsData = await vsRes.json();
376
+ vectorStoreId = vsData.id;
377
+ Logger_1.default.info('OpenAIService', `Recreated missing OpenAI vector store: ${vectorStoreId}`);
378
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
379
+ // Retry the attachment
380
+ const retryRes = await fetchWithRetry(`https://api.openai.com/v1/vector_stores/${vectorStoreId}/file_batches`, {
381
+ method: 'POST',
382
+ headers: {
383
+ 'Authorization': `Bearer ${apiKey}`,
384
+ 'Content-Type': 'application/json',
385
+ 'OpenAI-Beta': 'assistants=v2'
386
+ },
387
+ body: JSON.stringify({ file_ids: batchFileIds })
388
+ });
389
+ if (retryRes.ok) {
390
+ Logger_1.default.info('OpenAIService', `Batch of ${batchFileIds.length} files attached successfully on retry.`);
391
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
392
+ continue; // Chunk attached successfully!
393
+ }
394
+ else {
395
+ const retryErrBody = await retryRes.text();
396
+ throw new Error(`Batch attach retry failed: ${retryRes.statusText}. Response: ${retryErrBody}`);
397
+ }
398
+ }
399
+ else {
400
+ const vsErrBody = await vsRes.text();
401
+ throw new Error(`Failed to recreate OpenAI vector store: ${vsRes.statusText}. Response: ${vsErrBody}`);
402
+ }
403
+ }
404
+ throw new Error(`Batch attach failed: ${batchRes.statusText}. Response: ${errBody}`);
405
+ }
406
+ Logger_1.default.info('OpenAIService', `Batch of ${batchFileIds.length} files attached successfully.`);
407
+ // Save state progressively to config.json
408
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
409
+ }
410
+ catch (err) {
411
+ Logger_1.default.error('OpenAIService', `Failed to attach file batch chunk to Vector Store. Deleting successfully uploaded files from OpenAI to prevent orphans.`, err);
412
+ // Delete the files from OpenAI that were uploaded in this batch to prevent orphans
413
+ const rollbackPromises = batchSuccessfulKeys.map(async (key) => {
414
+ const fileId = fileIds[key];
415
+ if (fileId) {
416
+ try {
417
+ await fetchWithRetry(`https://api.openai.com/v1/files/${fileId}`, {
418
+ method: 'DELETE',
419
+ headers: {
420
+ 'Authorization': `Bearer ${apiKey}`
421
+ }
422
+ });
423
+ }
424
+ catch (deleteErr) {
425
+ Logger_1.default.warn('OpenAIService', `Failed to delete orphaned file ${fileId} from OpenAI during rollback`, deleteErr);
426
+ }
427
+ }
428
+ delete fileIds[key];
429
+ delete fileHashes[key];
430
+ });
431
+ await Promise.all(rollbackPromises);
432
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
433
+ }
434
+ }
435
+ }
436
+ }
437
+ // Save final metadata state
438
+ OpenAIService.writeOpenAIMetadata({ vectorStoreId, fileIds, fileHashes });
439
+ Logger_1.default.info('OpenAIService', 'OpenAI Vector Store sync complete.');
440
+ }
441
+ /**
442
+ * Generates a streaming answer from OpenAI Responses API.
443
+ * Note: For user chat queries, we DO NOT retry on 429 because we want to fail-fast
444
+ * and fall back to the local Gemma offline model immediately without blocking.
445
+ */
446
+ async generateAnswerStream(localThreadId, message, systemPrompt, onTextChunk, disableFileSearch = false) {
447
+ const apiKey = process.env.OPENAI_API_KEY;
448
+ if (!apiKey) {
449
+ throw new Error('OPENAI_API_KEY is not defined.');
450
+ }
451
+ const metadata = OpenAIService.readOpenAIMetadata();
452
+ const vectorStoreId = metadata.vectorStoreId;
453
+ const threadMappings = metadata.threadMappings || {};
454
+ const latestResponseId = threadMappings[localThreadId] || null;
455
+ Logger_1.default.info('OpenAIService', `Sending question to OpenAI Responses API. Thread: ${localThreadId}, PrevResponseId: ${latestResponseId}`);
456
+ const tools = [];
457
+ if (vectorStoreId && !disableFileSearch) {
458
+ tools.push({
459
+ type: 'file_search',
460
+ vector_store_ids: [vectorStoreId]
461
+ });
462
+ }
463
+ else {
464
+ Logger_1.default.info('OpenAIService', `Generating response without file_search. Reason: ${disableFileSearch ? 'disabled for page context' : 'no vectorStoreId found'}`);
465
+ }
466
+ const payload = {
467
+ model: process.env.OPENAI_MODEL || 'gpt-5.6-luna',
468
+ input: message,
469
+ tools: tools.length > 0 ? tools : undefined,
470
+ previous_response_id: latestResponseId,
471
+ instructions: systemPrompt,
472
+ stream: true
473
+ };
474
+ // No retry on chat generation for immediate offline fallback
475
+ const response = await fetch('https://api.openai.com/v1/responses', {
476
+ method: 'POST',
477
+ headers: {
478
+ 'Authorization': `Bearer ${apiKey}`,
479
+ 'Content-Type': 'application/json',
480
+ 'OpenAI-Beta': 'assistants=v2'
481
+ },
482
+ body: JSON.stringify(payload)
483
+ });
484
+ if (!response.ok) {
485
+ const errBody = await response.text();
486
+ let isPrevResponseError = false;
487
+ let isVectorStoreError = false;
488
+ try {
489
+ const errObj = JSON.parse(errBody);
490
+ if (errObj?.error?.code === 'previous_response_not_found' ||
491
+ errObj?.error?.message?.includes('previous_response_id')) {
492
+ isPrevResponseError = true;
493
+ }
494
+ const errMsg = errObj?.error?.message || '';
495
+ if (errMsg.includes('Vector store') && (errMsg.includes('not found') || errMsg.includes('missing') || errMsg.includes('vs_'))) {
496
+ isVectorStoreError = true;
497
+ }
498
+ }
499
+ catch { }
500
+ if (isVectorStoreError && vectorStoreId) {
501
+ Logger_1.default.warn('OpenAIService', `Vector store ${vectorStoreId} not found on OpenAI. Clearing vectorStoreId and retrying without file_search.`);
502
+ const currentMetadata = OpenAIService.readOpenAIMetadata();
503
+ delete currentMetadata.vectorStoreId;
504
+ OpenAIService.writeOpenAIMetadata(currentMetadata);
505
+ return this.generateAnswerStream(localThreadId, message, systemPrompt, onTextChunk, disableFileSearch);
506
+ }
507
+ if (isPrevResponseError && latestResponseId) {
508
+ Logger_1.default.warn('OpenAIService', `Previous response ID ${latestResponseId} expired or not found. Retrying without previous_response_id.`);
509
+ delete threadMappings[localThreadId];
510
+ OpenAIService.writeOpenAIMetadata({ threadMappings });
511
+ return this.generateAnswerStream(localThreadId, message, systemPrompt, onTextChunk, disableFileSearch);
512
+ }
513
+ throw new Error(`OpenAI Responses API call failed with status ${response.status}: ${errBody}`);
514
+ }
515
+ const reader = response.body?.getReader();
516
+ if (!reader) {
517
+ throw new Error('Response body reader is not available');
518
+ }
519
+ const decoder = new TextDecoder('utf-8');
520
+ let buffer = '';
521
+ let fullAnswer = '';
522
+ let newResponseId = '';
523
+ try {
524
+ while (true) {
525
+ const { done, value } = await reader.read();
526
+ if (done)
527
+ break;
528
+ buffer += decoder.decode(value, { stream: true });
529
+ const lines = buffer.split('\n');
530
+ buffer = lines.pop() || '';
531
+ for (const line of lines) {
532
+ const trimmed = line.trim();
533
+ if (!trimmed)
534
+ continue;
535
+ if (trimmed.startsWith('data:')) {
536
+ const dataStr = trimmed.slice(5).trim();
537
+ if (dataStr === '[DONE]')
538
+ continue;
539
+ try {
540
+ const data = JSON.parse(dataStr);
541
+ const dataType = data.type;
542
+ if (dataType === 'response.failed') {
543
+ const errMsg = data.response?.error?.message || '';
544
+ const errCode = data.response?.error?.code || '';
545
+ if (errMsg.includes('Vector store') || errCode === 'invalid_vector_store_ids') {
546
+ Logger_1.default.warn('OpenAIService', `Vector store was not found or invalid: ${errMsg}. Clearing vectorStoreId.`);
547
+ const currentMetadata = OpenAIService.readOpenAIMetadata();
548
+ delete currentMetadata.vectorStoreId;
549
+ OpenAIService.writeOpenAIMetadata(currentMetadata);
550
+ throw new Error('VECTOR_STORE_NOT_FOUND');
551
+ }
552
+ throw new Error(`OpenAI response failed: ${errMsg}`);
553
+ }
554
+ if ((dataType === 'response.created' || dataType === 'response.completed') && data.response?.id) {
555
+ newResponseId = data.response.id;
556
+ }
557
+ if (dataType === 'response.output_text.delta' && data.delta) {
558
+ onTextChunk(data.delta);
559
+ fullAnswer += data.delta;
560
+ }
561
+ }
562
+ catch (e) {
563
+ if (e instanceof Error && (e.message === 'VECTOR_STORE_NOT_FOUND' || e.message.startsWith('OpenAI response failed:'))) {
564
+ throw e;
565
+ }
566
+ // Ignore other parsing errors for partial/non-JSON data lines
567
+ }
568
+ }
569
+ }
570
+ }
571
+ }
572
+ catch (err) {
573
+ if (err.message === 'VECTOR_STORE_NOT_FOUND') {
574
+ Logger_1.default.info('OpenAIService', 'Retrying OpenAI Responses completion without invalid vector store ID...');
575
+ return this.generateAnswerStream(localThreadId, message, systemPrompt, onTextChunk, disableFileSearch);
576
+ }
577
+ throw err;
578
+ }
579
+ if (newResponseId) {
580
+ threadMappings[localThreadId] = newResponseId;
581
+ OpenAIService.writeOpenAIMetadata({ threadMappings });
582
+ }
583
+ return fullAnswer;
584
+ }
585
+ }
586
+ exports.OpenAIService = OpenAIService;
@@ -21,7 +21,7 @@ class NextMinEvents extends events_1.EventEmitter {
21
21
  * Typed emit to provide better DX (Developer Experience)
22
22
  */
23
23
  emitEvent(event, payload) {
24
- Logger_1.default.info('NextMinEvents', `Emitting event: ${event}`);
24
+ Logger_1.default.debug('NextMinEvents', `Emitting event: ${event}`);
25
25
  this.emit(event, payload);
26
26
  }
27
27
  }
@@ -1,8 +1,9 @@
1
1
  export declare class Logger {
2
- private isDevelopment;
2
+ private get isDevelopment();
3
3
  constructor();
4
4
  private getCallerInfo;
5
5
  private formatMessage;
6
+ debug(label: string, ...messages: any[]): void;
6
7
  log(label: string, ...messages: any[]): void;
7
8
  info(label: string, ...messages: any[]): void;
8
9
  warn(label: string, ...messages: any[]): void;
@@ -8,9 +8,11 @@ const kleur_1 = __importDefault(require("kleur"));
8
8
  const util_1 = require("util");
9
9
  const path_1 = __importDefault(require("path"));
10
10
  class Logger {
11
- constructor() {
12
- this.isDevelopment = process.env.APP_MODE !== 'production' && process.env.NODE_ENV !== 'production';
11
+ get isDevelopment() {
12
+ return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
13
+ process.env.NODE_ENV?.toLowerCase() !== 'production');
13
14
  }
15
+ constructor() { }
14
16
  getCallerInfo() {
15
17
  const originalPrepareStackTrace = Error.prepareStackTrace;
16
18
  Error.prepareStackTrace = (err, stack) => stack;
@@ -45,12 +47,21 @@ class Logger {
45
47
  case 'ERROR':
46
48
  formattedLabel = kleur_1.default.red(`[ERROR] ${label} (${callerInfo}):`);
47
49
  break;
50
+ case 'DEBUG':
51
+ formattedLabel = kleur_1.default.magenta(`[DEBUG] ${label} (${callerInfo}):`);
52
+ break;
48
53
  default:
49
54
  formattedLabel = `[${level}] ${label} (${callerInfo}):`;
50
55
  }
51
56
  const formattedMessage = (0, util_1.format)(...messages);
52
57
  return `${formattedLabel} ${formattedMessage}`;
53
58
  }
59
+ debug(label, ...messages) {
60
+ if (this.isDevelopment) {
61
+ const formatted = this.formatMessage('DEBUG', label, messages);
62
+ console.debug(formatted);
63
+ }
64
+ }
54
65
  log(label, ...messages) {
55
66
  if (this.isDevelopment) {
56
67
  const formatted = this.formatMessage('LOG', label, messages);
@@ -70,10 +81,8 @@ class Logger {
70
81
  }
71
82
  }
72
83
  error(label, ...messages) {
73
- if (this.isDevelopment) {
74
- const formatted = this.formatMessage('ERROR', label, messages);
75
- console.error(formatted);
76
- }
84
+ const formatted = this.formatMessage('ERROR', label, messages);
85
+ console.error(formatted);
77
86
  }
78
87
  }
79
88
  exports.Logger = Logger;
@@ -17,7 +17,7 @@ export declare class SchemaLoader {
17
17
  schemas: {
18
18
  [key: string]: Schema;
19
19
  };
20
- private isDevelopment;
20
+ private get isDevelopment();
21
21
  private nonOverridableSchemas;
22
22
  private userSchemasDir;
23
23
  private packageSchemasDir;
@@ -46,7 +46,7 @@ export declare class SchemaLoader {
46
46
  * We only shallow-clone values to avoid leaking references.
47
47
  */
48
48
  private sanitizeAttributes;
49
- closeWatcher(): void;
49
+ closeWatcher(): Promise<void>;
50
50
  private coerceIndexDir;
51
51
  /** Build desired single-field indexes per model from schemas.
52
52
  * - respects attribute.index (true|1|-1|'asc'|'desc')