@intranefr/superbackend 1.5.2 → 1.5.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 (41) hide show
  1. package/index.js +2 -0
  2. package/manage.js +745 -0
  3. package/package.json +4 -2
  4. package/src/controllers/admin.controller.js +11 -5
  5. package/src/controllers/adminAgents.controller.js +37 -0
  6. package/src/controllers/adminLlm.controller.js +19 -0
  7. package/src/controllers/adminMarkdowns.controller.js +157 -0
  8. package/src/controllers/adminScripts.controller.js +138 -0
  9. package/src/controllers/adminTelegram.controller.js +72 -0
  10. package/src/controllers/markdowns.controller.js +42 -0
  11. package/src/helpers/mongooseHelper.js +6 -6
  12. package/src/helpers/scriptBase.js +2 -2
  13. package/src/middleware.js +136 -29
  14. package/src/models/Agent.js +105 -0
  15. package/src/models/AgentMessage.js +82 -0
  16. package/src/models/Markdown.js +75 -0
  17. package/src/models/ScriptRun.js +8 -0
  18. package/src/models/TelegramBot.js +42 -0
  19. package/src/routes/adminAgents.routes.js +13 -0
  20. package/src/routes/adminLlm.routes.js +1 -0
  21. package/src/routes/adminMarkdowns.routes.js +16 -0
  22. package/src/routes/adminScripts.routes.js +4 -1
  23. package/src/routes/adminTelegram.routes.js +14 -0
  24. package/src/routes/markdowns.routes.js +16 -0
  25. package/src/services/agent.service.js +546 -0
  26. package/src/services/agentHistory.service.js +345 -0
  27. package/src/services/agentTools.service.js +578 -0
  28. package/src/services/jsonConfigs.service.js +22 -10
  29. package/src/services/llm.service.js +219 -6
  30. package/src/services/markdowns.service.js +522 -0
  31. package/src/services/scriptsRunner.service.js +328 -37
  32. package/src/services/telegram.service.js +130 -0
  33. package/views/admin-agents.ejs +273 -0
  34. package/views/admin-coolify-deploy.ejs +8 -8
  35. package/views/admin-dashboard.ejs +36 -5
  36. package/views/admin-experiments.ejs +1 -1
  37. package/views/admin-markdowns.ejs +905 -0
  38. package/views/admin-scripts.ejs +221 -4
  39. package/views/admin-telegram.ejs +269 -0
  40. package/views/partials/dashboard/nav-items.ejs +3 -0
  41. package/analysis-only.skill +0 -0
@@ -0,0 +1,578 @@
1
+ const mongoose = require('mongoose');
2
+ const { exec } = require('child_process');
3
+ const { promisify } = require('util');
4
+ const execAsync = promisify(exec);
5
+
6
+ const logger = {
7
+ log: (...args) => {
8
+ if (process.env.DEBUG_AGENT_TOOLS === 'true' && !process.env.TUI_MODE) console.log(...args);
9
+ },
10
+ warn: (...args) => {
11
+ if (process.env.DEBUG_AGENT_TOOLS === 'true' && !process.env.TUI_MODE) console.warn(...args);
12
+ },
13
+ error: (...args) => {
14
+ console.error(...args);
15
+ }
16
+ };
17
+
18
+ function hasTimeoutInCommand(command) {
19
+ return /^\s*timeout\s+/.test(command) ||
20
+ /\btimeout\s+\d+/.test(command) ||
21
+ /\b--timeout\b/.test(command) ||
22
+ /\b-t\s+\d+/.test(command) ||
23
+ /^\s*\w+.*--timeout=\d+/.test(command);
24
+ }
25
+ async function execWithTimeout(command) {
26
+ const timeoutInCommand = hasTimeoutInCommand(command);
27
+ if (timeoutInCommand) {
28
+ logger.log(`[exec] Command already has timeout, using as-is: ${command}`);
29
+ return await execAsync(command);
30
+ }
31
+ logger.log(`[exec] Adding 15-second timeout to command: ${command}`);
32
+ const wrappedCommand = `timeout 15s ${command}`;
33
+ try {
34
+ const { stdout, stderr } = await execAsync(wrappedCommand);
35
+ return { stdout, stderr };
36
+ } catch (error) {
37
+ if (error.code === 124) {
38
+ throw new Error(`Command timed out after 15 seconds`);
39
+ }
40
+ if (error.signal === 'SIGTERM' || error.signal === 'SIGKILL') {
41
+ throw new Error(`Command was terminated (likely timed out)`);
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ function createErrorResponse(error, options = {}) {
47
+ const {
48
+ code = 100,
49
+ type = 'internal_error',
50
+ recoverable = false,
51
+ retryAfter = null,
52
+ suggestions = [],
53
+ context = {}
54
+ } = options;
55
+ return JSON.stringify({
56
+ error: {
57
+ code,
58
+ type,
59
+ message: error.message || String(error),
60
+ recoverable,
61
+ retry_after: retryAfter,
62
+ suggestions,
63
+ context,
64
+ _raw: process.env.NODE_ENV === 'development' ? error.stack : undefined
65
+ }
66
+ }, null, 2);
67
+ }
68
+ const ERROR_CODES = {
69
+ INVALID_INPUT: 80,
70
+ MISSING_REQUIRED: 81,
71
+ PERMISSION_DENIED: 82,
72
+ NOT_FOUND: 92,
73
+ ALREADY_EXISTS: 93,
74
+ CONFLICT: 94,
75
+ CONNECTION_TIMEOUT: 105,
76
+ SERVICE_UNAVAILABLE: 106,
77
+ AUTH_FAILED: 107,
78
+ INTERNAL_ERROR: 110,
79
+ BUG: 111
80
+ };
81
+ const tools = {
82
+ 'mongo-memory': {
83
+ description: 'Persistent virtual cognitive space for the agent. Read, write, append, search, and list files in your memory workspace.',
84
+ parameters: {
85
+ type: 'object',
86
+ properties: {
87
+ operation: {
88
+ type: 'string',
89
+ enum: ['list', 'read', 'write', 'append', 'search'],
90
+ description: 'The memory operation to perform'
91
+ },
92
+ filename: {
93
+ type: 'string',
94
+ description: 'The name of the file (e.g., USER.md, TASKS.md). Required for read, write, append.'
95
+ },
96
+ content: {
97
+ type: 'string',
98
+ description: 'The content to write or append. Required for write, append.'
99
+ },
100
+ group_code: {
101
+ type: 'string',
102
+ description: 'Optional subfolder (e.g., archived, snapshots). Do NOT include the agent name prefix.'
103
+ },
104
+ query: {
105
+ type: 'string',
106
+ description: 'Search query across all agent memory files. Required for search.'
107
+ }
108
+ },
109
+ required: ['operation']
110
+ },
111
+ execute: async ({ operation, filename, content, group_code, query }, { agent }) => {
112
+ try {
113
+ if (!agent || !agent.name) throw new Error('Agent context missing');
114
+
115
+ const Markdown = mongoose.model('Markdown');
116
+ const CATEGORY = 'agents_memory';
117
+ const agentPrefix = agent.name.toLowerCase().replace(/[^a-z0-9]/g, '_');
118
+
119
+ // Resolve target group_code
120
+ let targetGroupCode = agentPrefix;
121
+ if (group_code) {
122
+ const sub = String(group_code).trim().replace(/^__+/, '');
123
+ if (sub) targetGroupCode = `${agentPrefix}__${sub}`;
124
+ }
125
+ switch (operation) {
126
+ case 'list': {
127
+ const docs = await Markdown.find({
128
+ category: CATEGORY,
129
+ group_code: targetGroupCode
130
+ }).select('slug title updatedAt').lean();
131
+
132
+ return JSON.stringify({
133
+ group_code: targetGroupCode,
134
+ files: docs.map(d => ({ filename: d.slug + '.md', title: d.title, updatedAt: d.updatedAt }))
135
+ }, null, 2);
136
+ }
137
+ case 'read': {
138
+ if (!filename) throw new Error('filename is required for read');
139
+ const slug = filename.replace(/\.md$/i, '');
140
+ const doc = await Markdown.findOne({
141
+ category: CATEGORY,
142
+ group_code: targetGroupCode,
143
+ slug
144
+ }).lean();
145
+
146
+ if (!doc) throw new Error(`File ${filename} not found in ${targetGroupCode}`);
147
+ return doc.markdownRaw;
148
+ }
149
+ case 'write': {
150
+ if (!filename) throw new Error('filename is required for write');
151
+ if (content === undefined) throw new Error('content is required for write');
152
+ const slug = filename.replace(/\.md$/i, '');
153
+
154
+ const markdownsService = require('./markdowns.service');
155
+ await markdownsService.upsertMarkdown({
156
+ title: filename,
157
+ category: CATEGORY,
158
+ group_code: targetGroupCode,
159
+ slug,
160
+ markdownRaw: content,
161
+ status: 'published'
162
+ });
163
+
164
+ // Verify write success by reading it back immediately
165
+ const verifiedDoc = await Markdown.findOne({
166
+ category: CATEGORY,
167
+ group_code: targetGroupCode,
168
+ slug
169
+ }).lean();
170
+
171
+ if (!verifiedDoc) {
172
+ throw new Error(`Write failed: Document ${filename} could not be found after write operation`);
173
+ }
174
+
175
+ // Simple content length check for verification log
176
+ const bytes = Buffer.byteLength(verifiedDoc.markdownRaw, 'utf8');
177
+ logger.log(`[mongo-memory] Successfully wrote ${filename} (${bytes} bytes) to ${targetGroupCode}. Doc ID: ${verifiedDoc._id}`);
178
+
179
+ return `File ${filename} written successfully to ${targetGroupCode} (Verified: ${bytes} bytes)`;
180
+ }
181
+ case 'append': {
182
+ if (!filename) throw new Error('filename is required for append');
183
+ if (content === undefined) throw new Error('content is required for append');
184
+ const slug = filename.replace(/\.md$/i, '');
185
+
186
+ const doc = await Markdown.findOne({
187
+ category: CATEGORY,
188
+ group_code: targetGroupCode,
189
+ slug
190
+ });
191
+
192
+ const existingContent = doc ? doc.markdownRaw : '';
193
+ const newContent = existingContent ? `${existingContent}\n${content}` : content;
194
+
195
+ const markdownsService = require('./markdowns.service');
196
+ await markdownsService.upsertMarkdown({
197
+ title: filename,
198
+ category: CATEGORY,
199
+ group_code: targetGroupCode,
200
+ slug,
201
+ markdownRaw: newContent,
202
+ status: 'published'
203
+ });
204
+
205
+ // Verify append success
206
+ const verifiedDoc = await Markdown.findOne({
207
+ category: CATEGORY,
208
+ group_code: targetGroupCode,
209
+ slug
210
+ }).lean();
211
+
212
+ if (!verifiedDoc) {
213
+ throw new Error(`Append failed: Document ${filename} could not be found after append operation`);
214
+ }
215
+
216
+ const oldBytes = Buffer.byteLength(existingContent, 'utf8');
217
+ const newBytes = Buffer.byteLength(verifiedDoc.markdownRaw, 'utf8');
218
+
219
+ if (newBytes <= oldBytes && content.length > 0) {
220
+ logger.warn(`[mongo-memory] Warning: Append might have failed for ${filename}. Size did not increase (Old: ${oldBytes}, New: ${newBytes})`);
221
+ } else {
222
+ logger.log(`[mongo-memory] Successfully appended to ${filename} (New size: ${newBytes} bytes) in ${targetGroupCode}`);
223
+ }
224
+
225
+ return `Content appended to ${filename} in ${targetGroupCode} (Verified new size: ${newBytes} bytes)`;
226
+ }
227
+ case 'search': {
228
+ if (!query) throw new Error('query is required for search');
229
+ // Regex to match agent prefix and any subfolders
230
+ const agentPrefixRegex = new RegExp(`^${agentPrefix}(?:$|__)`);
231
+
232
+ const docs = await Markdown.find({
233
+ category: CATEGORY,
234
+ group_code: { $regex: agentPrefixRegex },
235
+ $or: [
236
+ { title: { $regex: query, $options: 'i' } },
237
+ { markdownRaw: { $regex: query, $options: 'i' } }
238
+ ]
239
+ }).select('slug group_code title').limit(20).lean();
240
+
241
+ return JSON.stringify(docs.map(d => ({
242
+ filename: d.slug + '.md',
243
+ subfolder: d.group_code.replace(agentPrefix, '').replace(/^__/, '') || 'root',
244
+ title: d.title
245
+ })), null, 2);
246
+ }
247
+ default:
248
+ throw new Error(`Unknown memory operation: ${operation}`);
249
+ }
250
+ } catch (err) {
251
+ return createErrorResponse(err, {
252
+ code: ERROR_CODES.INTERNAL_ERROR,
253
+ type: 'memory_operation_failed',
254
+ recoverable: true
255
+ });
256
+ }
257
+ }
258
+ },
259
+ exec: {
260
+ description: 'Execute a shell command in the project working directory. Automatically adds a 15-second timeout to prevent hangs. Use timeout command or --timeout flag to override.',
261
+ parameters: {
262
+ type: 'object',
263
+ properties: {
264
+ command: {
265
+ type: 'string',
266
+ description: 'The shell command to execute (will automatically add 15s timeout unless already specified)'
267
+ }
268
+ },
269
+ required: ['command']
270
+ },
271
+ execute: async ({ command }) => {
272
+ try {
273
+ const { stdout, stderr } = await execWithTimeout(command);
274
+ return JSON.stringify({
275
+ stdout: stdout.trim(),
276
+ stderr: stderr.trim()
277
+ }, null, 2);
278
+ } catch (err) {
279
+ const suggestions = [];
280
+ if (err.stderr && err.stderr.includes('command not found')) {
281
+ suggestions.push('Check if the command is installed and available in PATH');
282
+ }
283
+ if (err.stderr && err.stderr.includes('not found')) {
284
+ suggestions.push('Check the command name is spelled correctly');
285
+ }
286
+ if (err.code === 'ENOENT') {
287
+ suggestions.push('Check the working directory exists and is accessible');
288
+ }
289
+ if (err.message && err.message.includes('timed out')) {
290
+ suggestions.push('The command took too long to complete (15 second timeout)');
291
+ suggestions.push('Add a timeout to your command (e.g., "timeout 30s command") or use a shorter command');
292
+ }
293
+ return createErrorResponse(err, {
294
+ code: ERROR_CODES.INTERNAL_ERROR,
295
+ type: 'shell_execution_failed',
296
+ recoverable: true,
297
+ suggestions
298
+ });
299
+ }
300
+ }
301
+ },
302
+ query_database: {
303
+ description: 'Query the MongoDB database for insights. Use Mongoose model names.',
304
+ parameters: {
305
+ type: 'object',
306
+ properties: {
307
+ modelName: {
308
+ type: 'string',
309
+ description: 'The name of the Mongoose model (e.g., User, Markdown, AuditEvent)'
310
+ },
311
+ query: {
312
+ type: 'object',
313
+ description: 'The MongoDB query object'
314
+ },
315
+ limit: {
316
+ type: 'number',
317
+ description: 'Limit the number of results',
318
+ default: 5
319
+ }
320
+ },
321
+ required: ['modelName', 'query']
322
+ },
323
+ execute: async ({ modelName, query, limit = 5 }) => {
324
+ try {
325
+ const Model = mongoose.model(modelName);
326
+ if (!Model) throw new Error(`Model ${modelName} not found`);
327
+ const results = await Model.find(query).limit(limit).lean();
328
+ return JSON.stringify(results, null, 2);
329
+ } catch (err) {
330
+ const suggestions = [];
331
+ if (err.message.includes('not found')) {
332
+ suggestions.push('Check the model name is spelled correctly');
333
+ suggestions.push('Use raw_db_query to list available models');
334
+ }
335
+ if (err.name === 'MongooseError') {
336
+ suggestions.push('Check database connection is active');
337
+ }
338
+ if (err.message.includes('not found') || err.message.includes('Schema')) {
339
+ suggestions.push('Check the model name is spelled correctly');
340
+ suggestions.push('Use raw_db_query to list available models');
341
+ }
342
+ if (err.name === 'MongooseError') {
343
+ suggestions.push('Check database connection is active');
344
+ }
345
+ return createErrorResponse(err, {
346
+ code: ERROR_CODES.NOT_FOUND,
347
+ type: 'database_query_failed',
348
+ recoverable: true,
349
+ suggestions
350
+ });
351
+ }
352
+ }
353
+ },
354
+ get_system_stats: {
355
+ description: 'Get general statistics about the system (counts of users, markdowns, etc.)',
356
+ parameters: {
357
+ type: 'object',
358
+ properties: {}
359
+ },
360
+ execute: async () => {
361
+ try {
362
+ const stats = {};
363
+ const models = mongoose.modelNames();
364
+ for (const name of models) {
365
+ stats[name] = await mongoose.model(name).countDocuments();
366
+ }
367
+ return JSON.stringify(stats, null, 2);
368
+ } catch (err) {
369
+ const suggestions = [];
370
+ if (err.name === 'MongooseError') {
371
+ suggestions.push('Check database connection is active');
372
+ suggestions.push('Ensure MongoDB server is running');
373
+ }
374
+ return createErrorResponse(err, {
375
+ code: ERROR_CODES.SERVICE_UNAVAILABLE,
376
+ type: 'database_stats_failed',
377
+ recoverable: true,
378
+ suggestions
379
+ });
380
+ }
381
+ }
382
+ },
383
+ raw_db_query: {
384
+ description: 'Execute raw MongoDB queries for database exploration. Use this to discover collection names, databases, or run admin commands.',
385
+ parameters: {
386
+ type: 'object',
387
+ properties: {
388
+ queryType: {
389
+ type: 'string',
390
+ enum: ['listDatabases', 'listCollections', 'countDocuments', 'findOne', 'aggregate', 'adminCommand'],
391
+ description: 'The type of raw query to execute'
392
+ },
393
+ database: {
394
+ type: 'string',
395
+ description: 'Database name (optional, defaults to current database)'
396
+ },
397
+ collection: {
398
+ type: 'string',
399
+ description: 'Collection name (required for collection-specific queries)'
400
+ },
401
+ filter: {
402
+ oneOf: [
403
+ { type: 'object', description: 'MongoDB filter/query object as JSON object' },
404
+ { type: 'string', description: 'MongoDB filter/query object as JSON string (will be parsed)' }
405
+ ],
406
+ description: 'MongoDB filter/query object (for countDocuments, findOne, aggregate). Can be object or JSON string.'
407
+ },
408
+ limit: {
409
+ type: 'number',
410
+ description: 'Limit the number of results (for listCollections, findOne, aggregate)',
411
+ default: 10
412
+ },
413
+ adminCommand: {
414
+ oneOf: [
415
+ { type: 'object', description: 'Admin command as JSON object' },
416
+ { type: 'string', description: 'Admin command as JSON string (will be parsed)' }
417
+ ],
418
+ description: 'Admin command to execute (for adminCommand queryType)'
419
+ }
420
+ },
421
+ required: ['queryType']
422
+ },
423
+ execute: async ({ queryType, database, collection, filter = {}, limit = 10, adminCommand }) => {
424
+ try {
425
+ if (!mongoose.connection || !mongoose.connection.db) {
426
+ throw new Error('MongoDB connection not ready. Please ensure database is connected.');
427
+ }
428
+ let db = mongoose.connection.db;
429
+
430
+ if (database && database !== mongoose.connection.name) {
431
+ db = mongoose.connection.useDb(database);
432
+ }
433
+ let parsedFilter = filter;
434
+ if (typeof filter === 'string') {
435
+ try {
436
+ parsedFilter = JSON.parse(filter);
437
+ } catch (err) {
438
+ throw new Error(`Failed to parse filter JSON: ${err.message}`);
439
+ }
440
+ }
441
+ let parsedAdminCommand = adminCommand;
442
+ if (typeof adminCommand === 'string') {
443
+ try {
444
+ parsedAdminCommand = JSON.parse(adminCommand);
445
+ } catch (err) {
446
+ throw new Error(`Failed to parse adminCommand JSON: ${err.message}`);
447
+ }
448
+ }
449
+ switch (queryType) {
450
+ case 'listDatabases': {
451
+ try {
452
+ const result = await mongoose.connection.db.admin().listDatabases();
453
+ const databases = (result?.databases || []).map(d => d.name).filter(Boolean).sort();
454
+ return JSON.stringify({ databases }, null, 2);
455
+ } catch (err) {
456
+ return createErrorResponse(err, {
457
+ code: ERROR_CODES.PERMISSION_DENIED,
458
+ type: 'admin_access_required',
459
+ recoverable: false,
460
+ suggestions: [
461
+ 'Try queryType: "listCollections" with database parameter',
462
+ 'Check if your database user has admin privileges'
463
+ ]
464
+ });
465
+ }
466
+ }
467
+ case 'listCollections': {
468
+ if (!database && !mongoose.connection.name) {
469
+ throw new Error('Database name required for listCollections');
470
+ }
471
+ const targetDb = database ? db : mongoose.connection.db;
472
+ const cursor = await targetDb.listCollections({}, { nameOnly: true });
473
+ const collections = await cursor.toArray();
474
+ const names = collections.map(c => c.name).filter(Boolean).sort();
475
+ return JSON.stringify({
476
+ database: database || mongoose.connection.name,
477
+ collections: names
478
+ }, null, 2);
479
+ }
480
+ case 'countDocuments': {
481
+ if (!collection) throw new Error('Collection name required for countDocuments');
482
+ const coll = db.collection(collection);
483
+ const count = await coll.countDocuments(parsedFilter);
484
+ return JSON.stringify({
485
+ collection,
486
+ database: database || mongoose.connection.name,
487
+ count,
488
+ filter: parsedFilter
489
+ }, null, 2);
490
+ }
491
+ case 'findOne': {
492
+ if (!collection) throw new Error('Collection name required for findOne');
493
+ const coll = db.collection(collection);
494
+ const result = await coll.findOne(parsedFilter);
495
+ return JSON.stringify({
496
+ collection,
497
+ database: database || mongoose.connection.name,
498
+ found: !!result,
499
+ result: result || null
500
+ }, null, 2);
501
+ }
502
+ case 'aggregate': {
503
+ if (!collection) throw new Error('Collection name required for aggregate');
504
+ const coll = db.collection(collection);
505
+ const pipeline = Array.isArray(parsedFilter) ? parsedFilter : [parsedFilter];
506
+ const results = await coll.aggregate(pipeline).limit(limit).toArray();
507
+ return JSON.stringify({
508
+ collection,
509
+ database: database || mongoose.connection.name,
510
+ results,
511
+ count: results.length
512
+ }, null, 2);
513
+ }
514
+ case 'adminCommand': {
515
+ if (!parsedAdminCommand) throw new Error('Admin command required for adminCommand queryType');
516
+ const result = await mongoose.connection.db.admin().command(parsedAdminCommand);
517
+ return JSON.stringify({
518
+ adminCommand: parsedAdminCommand,
519
+ result
520
+ }, null, 2);
521
+ }
522
+ default:
523
+ throw new Error(`Unknown queryType: ${queryType}. Supported: listDatabases, listCollections, countDocuments, findOne, aggregate, adminCommand`);
524
+ }
525
+ } catch (err) {
526
+ const suggestions = [];
527
+ if (err.message.includes('connection not ready')) {
528
+ suggestions.push('Check database connection is active');
529
+ suggestions.push('Ensure MongoDB server is running');
530
+ }
531
+ if (err.message.includes('JSON')) {
532
+ suggestions.push('Check your filter/adminCommand is valid JSON');
533
+ suggestions.push('Use a JSON validator if unsure');
534
+ }
535
+ if (err.message.includes('required')) {
536
+ suggestions.push('Check the required parameters are provided');
537
+ suggestions.push('Review the tool documentation for parameter requirements');
538
+ }
539
+ return createErrorResponse(err, {
540
+ code: ERROR_CODES.INVALID_INPUT,
541
+ type: 'query_execution_failed',
542
+ recoverable: true,
543
+ suggestions
544
+ });
545
+ }
546
+ }
547
+ }
548
+ }
549
+ async function executeTool(name, args, context = {}) {
550
+ const tool = tools[name];
551
+ if (!tool) {
552
+ return createErrorResponse(new Error(`Tool ${name} not found`), {
553
+ code: ERROR_CODES.NOT_FOUND,
554
+ type: 'tool_not_found',
555
+ recoverable: false,
556
+ suggestions: [
557
+ 'Check the tool name is spelled correctly',
558
+ 'List available tools using the system prompt'
559
+ ]
560
+ });
561
+ }
562
+ logger.log(`Executing tool ${name} with args:`, args);
563
+ return await tool.execute(args, context);
564
+ }
565
+ function getToolDefinitions() {
566
+ return Object.entries(tools).map(([name, tool]) => ({
567
+ type: 'function',
568
+ function: {
569
+ name,
570
+ description: tool.description,
571
+ parameters: tool.parameters
572
+ }
573
+ }));
574
+ }
575
+ module.exports = {
576
+ executeTool,
577
+ getToolDefinitions
578
+ };
@@ -2,6 +2,18 @@ const crypto = require('crypto');
2
2
 
3
3
  const JsonConfig = require('../models/JsonConfig');
4
4
 
5
+ const logger = {
6
+ log: (...args) => {
7
+ if (process.env.DEBUG_JSON_CONFIGS === 'true' && !process.env.TUI_MODE) console.log(...args);
8
+ },
9
+ warn: (...args) => {
10
+ if (process.env.DEBUG_JSON_CONFIGS === 'true' && !process.env.TUI_MODE) console.warn(...args);
11
+ },
12
+ error: (...args) => {
13
+ console.error(...args);
14
+ }
15
+ };
16
+
5
17
  const cache = new Map();
6
18
 
7
19
  function normalizeSlugBase(title) {
@@ -122,7 +134,7 @@ async function getJsonConfigById(id) {
122
134
  }
123
135
 
124
136
  async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTtlSeconds = 0, alias }) {
125
- console.log('createJsonConfig called with:', { title, jsonRaw, publicEnabled, cacheTtlSeconds, alias });
137
+ logger.log('createJsonConfig called with:', { title, jsonRaw, publicEnabled, cacheTtlSeconds, alias });
126
138
 
127
139
  const normalizedTitle = String(title || '').trim();
128
140
  if (!normalizedTitle) {
@@ -142,7 +154,7 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
142
154
  let normalizedAlias = null;
143
155
  if (alias !== undefined && alias !== null) {
144
156
  normalizedAlias = normalizeAlias(alias);
145
- console.log('Normalized alias:', normalizedAlias);
157
+ logger.log('Normalized alias:', normalizedAlias);
146
158
  if (normalizedAlias && !(await validateAliasUniqueness(normalizedAlias))) {
147
159
  const err = new Error('Alias must be unique and not conflict with existing slugs or aliases');
148
160
  err.code = 'ALIAS_NOT_UNIQUE';
@@ -175,7 +187,7 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
175
187
  }
176
188
 
177
189
  async function updateJsonConfig(id, patch) {
178
- console.log('updateJsonConfig called with id:', id, 'patch:', patch);
190
+ logger.log('updateJsonConfig called with id:', id, 'patch:', patch);
179
191
 
180
192
  const doc = await JsonConfig.findById(id);
181
193
  if (!doc) {
@@ -184,7 +196,7 @@ async function updateJsonConfig(id, patch) {
184
196
  throw err;
185
197
  }
186
198
 
187
- console.log('Found document:', doc.toObject());
199
+ logger.log('Found document:', doc.toObject());
188
200
 
189
201
  const oldSlug = doc.slug;
190
202
  const oldAlias = doc.alias;
@@ -222,14 +234,14 @@ async function updateJsonConfig(id, patch) {
222
234
 
223
235
  if (patch && Object.prototype.hasOwnProperty.call(patch, 'alias')) {
224
236
  const newAlias = patch.alias;
225
- console.log('Processing alias update. newAlias:', newAlias);
237
+ logger.log('Processing alias update. newAlias:', newAlias);
226
238
 
227
239
  if (newAlias === null || newAlias === undefined || newAlias === '') {
228
240
  doc.alias = undefined;
229
- console.log('Setting alias to undefined');
241
+ logger.log('Setting alias to undefined');
230
242
  } else {
231
243
  const normalizedAlias = normalizeAlias(newAlias);
232
- console.log('Normalized alias for update:', normalizedAlias);
244
+ logger.log('Normalized alias for update:', normalizedAlias);
233
245
 
234
246
  if (!normalizedAlias) {
235
247
  const err = new Error('Invalid alias format');
@@ -244,7 +256,7 @@ async function updateJsonConfig(id, patch) {
244
256
  }
245
257
 
246
258
  doc.alias = normalizedAlias;
247
- console.log('Setting alias to:', normalizedAlias);
259
+ logger.log('Setting alias to:', normalizedAlias);
248
260
  }
249
261
  }
250
262
 
@@ -252,9 +264,9 @@ async function updateJsonConfig(id, patch) {
252
264
  doc.slug = await generateUniqueSlugFromTitle(doc.title);
253
265
  }
254
266
 
255
- console.log('Document before save:', doc.toObject());
267
+ logger.log('Document before save:', doc.toObject());
256
268
  await doc.save();
257
- console.log('Document after save:', doc.toObject());
269
+ logger.log('Document after save:', doc.toObject());
258
270
 
259
271
  clearJsonConfigCache(oldSlug);
260
272
  clearJsonConfigCache(doc.slug);