@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.
- package/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +4 -2
- package/src/controllers/admin.controller.js +11 -5
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +138 -0
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/helpers/mongooseHelper.js +6 -6
- package/src/helpers/scriptBase.js +2 -2
- package/src/middleware.js +136 -29
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/Markdown.js +75 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/jsonConfigs.service.js +22 -10
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +328 -37
- package/src/services/telegram.service.js +130 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +36 -5
- package/views/admin-experiments.ejs +1 -1
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +221 -4
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +3 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
+
logger.log('Processing alias update. newAlias:', newAlias);
|
|
226
238
|
|
|
227
239
|
if (newAlias === null || newAlias === undefined || newAlias === '') {
|
|
228
240
|
doc.alias = undefined;
|
|
229
|
-
|
|
241
|
+
logger.log('Setting alias to undefined');
|
|
230
242
|
} else {
|
|
231
243
|
const normalizedAlias = normalizeAlias(newAlias);
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
logger.log('Document before save:', doc.toObject());
|
|
256
268
|
await doc.save();
|
|
257
|
-
|
|
269
|
+
logger.log('Document after save:', doc.toObject());
|
|
258
270
|
|
|
259
271
|
clearJsonConfigCache(oldSlug);
|
|
260
272
|
clearJsonConfigCache(doc.slug);
|