@exulu/backend 1.14.0 → 1.15.0

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/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- # [1.14.0](https://github.com/Qventu/exulu-backend/compare/v1.13.0...v1.14.0) (2025-08-21)
1
+ # [1.15.0](https://github.com/Qventu/exulu-backend/compare/v1.14.1...v1.15.0) (2025-08-22)
2
2
 
3
3
 
4
4
  ### Features
5
5
 
6
- * upgraded ai SDK, and fixed statistics ([5044729](https://github.com/Qventu/exulu-backend/commit/50447296d7ad61e40830c5b3fc8195d160e216a1))
6
+ * image generation for agent profile images, added "ByIds" to graphql Schema ([c084070](https://github.com/Qventu/exulu-backend/commit/c084070b9f61830fcdf0745da7727c70b718fa64))
7
7
 
8
8
  # [1.1.0](https://github.com/Qventu/exulu-backend/compare/v1.0.1...v1.1.0) (2025-07-30)
9
9
 
package/dist/index.cjs CHANGED
@@ -305,57 +305,80 @@ var bullmqDecorator = async ({
305
305
  // src/registry/utils/map-types.ts
306
306
  var mapType = (t, type, name, defaultValue, unique) => {
307
307
  if (type === "text" || type === "enum") {
308
- t.text(name);
308
+ if (defaultValue) {
309
+ t.text(name).defaultTo(defaultValue);
310
+ } else {
311
+ t.text(name);
312
+ }
309
313
  if (unique) t.unique(name);
310
- if (defaultValue) t.defaultTo(defaultValue);
311
314
  return;
312
315
  }
313
316
  if (type === "longText") {
314
- t.text(name);
317
+ if (defaultValue) {
318
+ t.text(name).defaultTo(defaultValue);
319
+ } else {
320
+ t.text(name);
321
+ }
315
322
  if (unique) t.unique(name);
316
- if (defaultValue) t.defaultTo(defaultValue);
317
323
  return;
318
324
  }
319
325
  if (type === "shortText") {
320
- t.string(name, 100);
326
+ if (defaultValue) {
327
+ t.string(name, 100).defaultTo(defaultValue);
328
+ } else {
329
+ t.string(name, 100);
330
+ }
321
331
  if (unique) t.unique(name);
322
- if (defaultValue) t.defaultTo(defaultValue);
323
332
  return;
324
333
  }
325
334
  if (type === "number") {
326
- t.float(name);
335
+ if (defaultValue) {
336
+ t.float(name).defaultTo(defaultValue);
337
+ } else {
338
+ t.float(name);
339
+ }
327
340
  if (unique) t.unique(name);
328
- if (defaultValue) t.defaultTo(defaultValue);
329
341
  return;
330
342
  }
331
343
  if (type === "boolean") {
332
344
  t.boolean(name).defaultTo(defaultValue || false);
333
345
  if (unique) t.unique(name);
334
- if (defaultValue) t.defaultTo(defaultValue);
335
346
  return;
336
347
  }
337
348
  if (type === "code") {
338
- t.text(name);
349
+ if (defaultValue) {
350
+ t.text(name).defaultTo(defaultValue);
351
+ } else {
352
+ t.text(name);
353
+ }
339
354
  if (unique) t.unique(name);
340
- if (defaultValue) t.defaultTo(defaultValue);
341
355
  return;
342
356
  }
343
357
  if (type === "json") {
344
- t.jsonb(name);
358
+ if (defaultValue) {
359
+ t.jsonb(name).defaultTo(defaultValue);
360
+ } else {
361
+ t.jsonb(name);
362
+ }
345
363
  if (unique) t.unique(name);
346
- if (defaultValue) t.defaultTo(defaultValue);
347
364
  return;
348
365
  }
349
366
  if (type === "date") {
350
- t.timestamp(name);
367
+ if (defaultValue) {
368
+ t.timestamp(name).defaultTo(defaultValue);
369
+ } else {
370
+ t.timestamp(name);
371
+ }
351
372
  if (unique) t.unique(name);
352
- if (defaultValue) t.defaultTo(defaultValue);
353
373
  return;
354
374
  }
355
375
  if (type === "uuid") {
356
- t.uuid(name);
376
+ if (defaultValue) {
377
+ t.uuid(name).defaultTo(defaultValue);
378
+ } else {
379
+ t.uuid(name);
380
+ }
357
381
  if (unique) t.unique(name);
358
- if (defaultValue) t.defaultTo(defaultValue);
359
382
  return;
360
383
  }
361
384
  throw new Error("Invalid type: " + type);
@@ -566,6 +589,7 @@ var ExuluAgent = class {
566
589
  this.config = config;
567
590
  this.type = type;
568
591
  this.capabilities = capabilities || {
592
+ text: false,
569
593
  images: [],
570
594
  files: [],
571
595
  audio: [],
@@ -2461,6 +2485,14 @@ function createQueries(table) {
2461
2485
  const result = await query.first();
2462
2486
  return result;
2463
2487
  },
2488
+ [`${tableNameSingular}ByIds`]: async (_, args, context, info) => {
2489
+ const { db: db3 } = context;
2490
+ const requestedFields = getRequestedFields(info);
2491
+ let query = db3.from(tableNamePlural).select(requestedFields).whereIn("id", args.ids);
2492
+ query = applyAccessControl(table, context.user, query);
2493
+ const result = await query;
2494
+ return result;
2495
+ },
2464
2496
  [`${tableNameSingular}One`]: async (_, args, context, info) => {
2465
2497
  const { filters = [], sort } = args;
2466
2498
  const { db: db3 } = context;
@@ -2625,6 +2657,7 @@ function createSDL(tables) {
2625
2657
  console.log("[EXULU] Adding table >>>>>", tableNamePlural);
2626
2658
  typeDefs += `
2627
2659
  ${tableNameSingular}ById(id: ID!): ${tableNameSingular}
2660
+ ${tableNameSingular}ByIds(ids: [ID!]!): [${tableNameSingular}]!
2628
2661
  ${tableNamePlural}Pagination(limit: Int, page: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingularUpperCaseFirst}PaginationResult
2629
2662
  ${tableNameSingular}One(filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}
2630
2663
  ${tableNamePlural}Statistics(filters: [Filter${tableNameSingularUpperCaseFirst}], groupBy: String): [StatisticsResult]!
@@ -2861,6 +2894,10 @@ var agentsSchema = {
2861
2894
  name: "name",
2862
2895
  type: "text"
2863
2896
  },
2897
+ {
2898
+ name: "image",
2899
+ type: "text"
2900
+ },
2864
2901
  {
2865
2902
  name: "description",
2866
2903
  type: "text"
@@ -2903,6 +2940,10 @@ var usersSchema = {
2903
2940
  type: "number",
2904
2941
  index: true
2905
2942
  },
2943
+ {
2944
+ name: "favourite_agents",
2945
+ type: "json"
2946
+ },
2906
2947
  {
2907
2948
  name: "firstname",
2908
2949
  type: "text"
@@ -3656,6 +3697,9 @@ Intelligence Management Platform
3656
3697
  };
3657
3698
 
3658
3699
  // src/registry/routes.ts
3700
+ var import_openai = __toESM(require("openai"), 1);
3701
+ var import_fs = __toESM(require("fs"), 1);
3702
+ var import_node_crypto = require("crypto");
3659
3703
  var REQUEST_SIZE_LIMIT = "50mb";
3660
3704
  var global_queues = {
3661
3705
  logs_cleaner: "logs-cleaner"
@@ -3836,6 +3880,7 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
3836
3880
  rights_mode: agent.rights_mode,
3837
3881
  slug: backend?.slug,
3838
3882
  rateLimit: backend?.rateLimit,
3883
+ image: agent.image,
3839
3884
  streaming: backend?.streaming,
3840
3885
  capabilities: backend?.capabilities,
3841
3886
  RBAC: agent.RBAC,
@@ -3883,6 +3928,7 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
3883
3928
  model: backend?.model?.create({ apiKey: "" }).modelId,
3884
3929
  active: agent.active,
3885
3930
  public: agent.public,
3931
+ image: agent.image,
3886
3932
  type: agent.type,
3887
3933
  rights_mode: agent.rights_mode,
3888
3934
  slug: backend?.slug,
@@ -3906,6 +3952,97 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
3906
3952
  inputSchema: tool2.inputSchema ? (0, import_zodex.zerialize)(tool2.inputSchema) : null
3907
3953
  })));
3908
3954
  });
3955
+ app.post("/generate/agent/image", async (req, res) => {
3956
+ console.log("[EXULU] generate/agent/image", req.body);
3957
+ const authenticationResult = await requestValidators.authenticate(req);
3958
+ if (!authenticationResult.user?.id) {
3959
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3960
+ return;
3961
+ }
3962
+ const { name, description, style } = req.body;
3963
+ if (!name || !description) {
3964
+ res.status(400).json({
3965
+ message: "Missing name or description in request."
3966
+ });
3967
+ return;
3968
+ }
3969
+ const { db: db3 } = await postgresClient();
3970
+ const variable = await db3.from("variables").where({ name: "OPENAI_IMAGE_GENERATION_API_KEY" }).first();
3971
+ if (!variable) {
3972
+ res.status(400).json({
3973
+ message: "Provider API key variable not found."
3974
+ });
3975
+ return;
3976
+ }
3977
+ let providerApiKey = variable.value;
3978
+ if (!variable.encrypted) {
3979
+ res.status(400).json({
3980
+ message: "Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
3981
+ });
3982
+ return;
3983
+ }
3984
+ if (variable.encrypted) {
3985
+ const bytes = import_crypto_js3.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
3986
+ providerApiKey = bytes.toString(import_crypto_js3.default.enc.Utf8);
3987
+ }
3988
+ const openai = new import_openai.default({
3989
+ apiKey: providerApiKey
3990
+ });
3991
+ let style_reference = "";
3992
+ if (style === "origami") {
3993
+ style_reference = "minimalistic origami-style, futuristic robot, portrait, focus on face.";
3994
+ } else if (style === "anime") {
3995
+ style_reference = "minimalistic, make it in the style of a felt puppet, futuristic robot, portrait, focus on face.";
3996
+ } else if (style === "japanese_anime") {
3997
+ style_reference = "minimalistic, make it in the style of japanese anime, futuristic robot, portrait, focus on face.";
3998
+ } else if (style === "vaporwave") {
3999
+ style_reference = "minimalistic, make it in the style of a vaporwave album cover, futuristic robot, portrait, focus on face.";
4000
+ } else if (style === "lego") {
4001
+ style_reference = "minimalistic, make it in the style of LEGO minifigures, futuristic robot, portrait, focus on face.";
4002
+ } else if (style === "paper_cut") {
4003
+ style_reference = "minimalistic, make it in the style of Paper-cut style portrait with color layers, futuristic robot, portrait, focus on face.";
4004
+ } else if (style === "felt_puppet") {
4005
+ style_reference = "minimalistic, make it in the style of a felt puppet, futuristic robot, portrait, focus on face.";
4006
+ } else if (style === "app_icon") {
4007
+ style_reference = "A playful and modern app icon design of a robot, minimal flat vector style, glossy highlights, soft shadows, centered composition, high contrast, vibrant colors, rounded corners, on a transparent background, icon-friendly, no text, no details outside the frame, size is 1024x1024.";
4008
+ } else if (style === "pixel_art") {
4009
+ style_reference = "A pixel art style of a robot, minimal flat vector style, glossy highlights, soft shadows, centered composition, high contrast, vibrant colors, rounded corners, on a transparent background, icon-friendly, no text, no details outside the frame, size is 1024x1024.";
4010
+ } else if (style === "isometric") {
4011
+ style_reference = "3D isometric icon of a robot, centered composition, on a transparent background, no text, no details outside the frame, size is 1024x1024.";
4012
+ } else {
4013
+ style_reference = "A minimalist 3D, robot, portrait, focus on face, floating in space, low-poly design with pastel colors.";
4014
+ }
4015
+ const prompt = `
4016
+ A digital portrait of ${name}, visualized as a futuristic robot.
4017
+ The robot\u2019s design reflects '${description}', with props, tools, or symbolic objects that represent its expertise or area of work.
4018
+ Example: if the agent is a financial analyst, it may hold a stack of papers; if it\u2019s a creative strategist it may be painting on a canvas.
4019
+ Style: ${style_reference}.
4020
+ The portrait should have a clean background.
4021
+ Framing: bust portrait, centered.
4022
+ Mood: friendly and intelligent.
4023
+ `;
4024
+ const result = await openai.images.generate({
4025
+ model: "gpt-image-1",
4026
+ prompt
4027
+ });
4028
+ const image_base64 = result.data?.[0]?.b64_json;
4029
+ if (!image_base64) {
4030
+ res.status(500).json({
4031
+ message: "Failed to generate image."
4032
+ });
4033
+ return;
4034
+ }
4035
+ const image_bytes = Buffer.from(image_base64, "base64");
4036
+ const uuid = (0, import_node_crypto.randomUUID)();
4037
+ if (!import_fs.default.existsSync("public")) {
4038
+ import_fs.default.mkdirSync("public");
4039
+ }
4040
+ import_fs.default.writeFileSync(`public/${uuid}.png`, image_bytes);
4041
+ res.status(200).json({
4042
+ message: "Image generated successfully.",
4043
+ image: `${process.env.BACKEND}/${uuid}.png`
4044
+ });
4045
+ });
3909
4046
  app.get("/tools/:id", async (req, res) => {
3910
4047
  const id = req.params.id;
3911
4048
  if (!id) {
@@ -4652,6 +4789,7 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
4652
4789
  }
4653
4790
  }
4654
4791
  });
4792
+ app.use(import_express4.default.static("public"));
4655
4793
  return app;
4656
4794
  };
4657
4795
  var createCustomAnthropicStreamingMessage = (message) => {
@@ -4716,7 +4854,7 @@ var bullmq = {
4716
4854
  };
4717
4855
 
4718
4856
  // src/registry/workers.ts
4719
- var fs2 = __toESM(require("fs"), 1);
4857
+ var fs3 = __toESM(require("fs"), 1);
4720
4858
  var import_path = __toESM(require("path"), 1);
4721
4859
  var defaultLogsDir = import_path.default.join(process.cwd(), "logs");
4722
4860
  var redisConnection;
@@ -4850,16 +4988,16 @@ var createLogsCleanerWorker = (logsDir) => {
4850
4988
  global_queues.logs_cleaner,
4851
4989
  async (job) => {
4852
4990
  console.log(`[EXULU] recurring job ${job.id}.`);
4853
- const folder = fs2.readdirSync(logsDir);
4991
+ const folder = fs3.readdirSync(logsDir);
4854
4992
  const files = folder.filter((file) => file.endsWith(".log"));
4855
4993
  const now = /* @__PURE__ */ new Date();
4856
4994
  const daysToKeep = job.data.ttld;
4857
4995
  const dateToKeep = new Date(now.getTime() - daysToKeep * 24 * 60 * 60 * 1e3);
4858
4996
  files.forEach((file) => {
4859
4997
  const filePath = import_path.default.join(logsDir, file);
4860
- const fileStats = fs2.statSync(filePath);
4998
+ const fileStats = fs3.statSync(filePath);
4861
4999
  if (fileStats.mtime < dateToKeep) {
4862
- fs2.unlinkSync(filePath);
5000
+ fs3.unlinkSync(filePath);
4863
5001
  }
4864
5002
  });
4865
5003
  },
@@ -4879,7 +5017,7 @@ var createLogsCleanerWorker = (logsDir) => {
4879
5017
 
4880
5018
  // src/mcp/index.ts
4881
5019
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4882
- var import_node_crypto = require("crypto");
5020
+ var import_node_crypto2 = require("crypto");
4883
5021
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
4884
5022
  var import_types = require("@modelcontextprotocol/sdk/types.js");
4885
5023
  var import_zod3 = require("zod");
@@ -4973,7 +5111,7 @@ ${code}`
4973
5111
  transport = this.transports[sessionId];
4974
5112
  } else if (!sessionId && (0, import_types.isInitializeRequest)(req.body)) {
4975
5113
  transport = new import_streamableHttp.StreamableHTTPServerTransport({
4976
- sessionIdGenerator: () => (0, import_node_crypto.randomUUID)(),
5114
+ sessionIdGenerator: () => (0, import_node_crypto2.randomUUID)(),
4977
5115
  onsessioninitialized: (sessionId2) => {
4978
5116
  this.transports[sessionId2] = transport;
4979
5117
  }
@@ -5041,6 +5179,7 @@ var defaultAgent = new ExuluAgent({
5041
5179
  description: `Basic agent without any defined tools, that can support MCP's.`,
5042
5180
  type: "agent",
5043
5181
  capabilities: {
5182
+ text: true,
5044
5183
  images: [],
5045
5184
  files: [],
5046
5185
  audio: [],
package/dist/index.d.cts CHANGED
@@ -119,6 +119,7 @@ interface ExuluAgentParams {
119
119
  description: string;
120
120
  config?: ExuluAgentConfig | undefined;
121
121
  capabilities?: {
122
+ text: boolean;
122
123
  images: string[];
123
124
  files: string[];
124
125
  audio: string[];
@@ -152,6 +153,7 @@ declare class ExuluAgent {
152
153
  }) => LanguageModel;
153
154
  };
154
155
  capabilities: {
156
+ text: boolean;
155
157
  images: string[];
156
158
  files: string[];
157
159
  audio: string[];
@@ -544,6 +546,7 @@ type User = {
544
546
  type?: "api" | "user";
545
547
  anthropic_token?: string;
546
548
  super_admin?: boolean;
549
+ favourite_agents?: string[];
547
550
  role: {
548
551
  id: string;
549
552
  name: string;
package/dist/index.d.ts CHANGED
@@ -119,6 +119,7 @@ interface ExuluAgentParams {
119
119
  description: string;
120
120
  config?: ExuluAgentConfig | undefined;
121
121
  capabilities?: {
122
+ text: boolean;
122
123
  images: string[];
123
124
  files: string[];
124
125
  audio: string[];
@@ -152,6 +153,7 @@ declare class ExuluAgent {
152
153
  }) => LanguageModel;
153
154
  };
154
155
  capabilities: {
156
+ text: boolean;
155
157
  images: string[];
156
158
  files: string[];
157
159
  audio: string[];
@@ -544,6 +546,7 @@ type User = {
544
546
  type?: "api" | "user";
545
547
  anthropic_token?: string;
546
548
  super_admin?: boolean;
549
+ favourite_agents?: string[];
547
550
  role: {
548
551
  id: string;
549
552
  name: string;
package/dist/index.js CHANGED
@@ -263,57 +263,80 @@ var bullmqDecorator = async ({
263
263
  // src/registry/utils/map-types.ts
264
264
  var mapType = (t, type, name, defaultValue, unique) => {
265
265
  if (type === "text" || type === "enum") {
266
- t.text(name);
266
+ if (defaultValue) {
267
+ t.text(name).defaultTo(defaultValue);
268
+ } else {
269
+ t.text(name);
270
+ }
267
271
  if (unique) t.unique(name);
268
- if (defaultValue) t.defaultTo(defaultValue);
269
272
  return;
270
273
  }
271
274
  if (type === "longText") {
272
- t.text(name);
275
+ if (defaultValue) {
276
+ t.text(name).defaultTo(defaultValue);
277
+ } else {
278
+ t.text(name);
279
+ }
273
280
  if (unique) t.unique(name);
274
- if (defaultValue) t.defaultTo(defaultValue);
275
281
  return;
276
282
  }
277
283
  if (type === "shortText") {
278
- t.string(name, 100);
284
+ if (defaultValue) {
285
+ t.string(name, 100).defaultTo(defaultValue);
286
+ } else {
287
+ t.string(name, 100);
288
+ }
279
289
  if (unique) t.unique(name);
280
- if (defaultValue) t.defaultTo(defaultValue);
281
290
  return;
282
291
  }
283
292
  if (type === "number") {
284
- t.float(name);
293
+ if (defaultValue) {
294
+ t.float(name).defaultTo(defaultValue);
295
+ } else {
296
+ t.float(name);
297
+ }
285
298
  if (unique) t.unique(name);
286
- if (defaultValue) t.defaultTo(defaultValue);
287
299
  return;
288
300
  }
289
301
  if (type === "boolean") {
290
302
  t.boolean(name).defaultTo(defaultValue || false);
291
303
  if (unique) t.unique(name);
292
- if (defaultValue) t.defaultTo(defaultValue);
293
304
  return;
294
305
  }
295
306
  if (type === "code") {
296
- t.text(name);
307
+ if (defaultValue) {
308
+ t.text(name).defaultTo(defaultValue);
309
+ } else {
310
+ t.text(name);
311
+ }
297
312
  if (unique) t.unique(name);
298
- if (defaultValue) t.defaultTo(defaultValue);
299
313
  return;
300
314
  }
301
315
  if (type === "json") {
302
- t.jsonb(name);
316
+ if (defaultValue) {
317
+ t.jsonb(name).defaultTo(defaultValue);
318
+ } else {
319
+ t.jsonb(name);
320
+ }
303
321
  if (unique) t.unique(name);
304
- if (defaultValue) t.defaultTo(defaultValue);
305
322
  return;
306
323
  }
307
324
  if (type === "date") {
308
- t.timestamp(name);
325
+ if (defaultValue) {
326
+ t.timestamp(name).defaultTo(defaultValue);
327
+ } else {
328
+ t.timestamp(name);
329
+ }
309
330
  if (unique) t.unique(name);
310
- if (defaultValue) t.defaultTo(defaultValue);
311
331
  return;
312
332
  }
313
333
  if (type === "uuid") {
314
- t.uuid(name);
334
+ if (defaultValue) {
335
+ t.uuid(name).defaultTo(defaultValue);
336
+ } else {
337
+ t.uuid(name);
338
+ }
315
339
  if (unique) t.unique(name);
316
- if (defaultValue) t.defaultTo(defaultValue);
317
340
  return;
318
341
  }
319
342
  throw new Error("Invalid type: " + type);
@@ -524,6 +547,7 @@ var ExuluAgent = class {
524
547
  this.config = config;
525
548
  this.type = type;
526
549
  this.capabilities = capabilities || {
550
+ text: false,
527
551
  images: [],
528
552
  files: [],
529
553
  audio: [],
@@ -2419,6 +2443,14 @@ function createQueries(table) {
2419
2443
  const result = await query.first();
2420
2444
  return result;
2421
2445
  },
2446
+ [`${tableNameSingular}ByIds`]: async (_, args, context, info) => {
2447
+ const { db: db3 } = context;
2448
+ const requestedFields = getRequestedFields(info);
2449
+ let query = db3.from(tableNamePlural).select(requestedFields).whereIn("id", args.ids);
2450
+ query = applyAccessControl(table, context.user, query);
2451
+ const result = await query;
2452
+ return result;
2453
+ },
2422
2454
  [`${tableNameSingular}One`]: async (_, args, context, info) => {
2423
2455
  const { filters = [], sort } = args;
2424
2456
  const { db: db3 } = context;
@@ -2583,6 +2615,7 @@ function createSDL(tables) {
2583
2615
  console.log("[EXULU] Adding table >>>>>", tableNamePlural);
2584
2616
  typeDefs += `
2585
2617
  ${tableNameSingular}ById(id: ID!): ${tableNameSingular}
2618
+ ${tableNameSingular}ByIds(ids: [ID!]!): [${tableNameSingular}]!
2586
2619
  ${tableNamePlural}Pagination(limit: Int, page: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingularUpperCaseFirst}PaginationResult
2587
2620
  ${tableNameSingular}One(filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}
2588
2621
  ${tableNamePlural}Statistics(filters: [Filter${tableNameSingularUpperCaseFirst}], groupBy: String): [StatisticsResult]!
@@ -2819,6 +2852,10 @@ var agentsSchema = {
2819
2852
  name: "name",
2820
2853
  type: "text"
2821
2854
  },
2855
+ {
2856
+ name: "image",
2857
+ type: "text"
2858
+ },
2822
2859
  {
2823
2860
  name: "description",
2824
2861
  type: "text"
@@ -2861,6 +2898,10 @@ var usersSchema = {
2861
2898
  type: "number",
2862
2899
  index: true
2863
2900
  },
2901
+ {
2902
+ name: "favourite_agents",
2903
+ type: "json"
2904
+ },
2864
2905
  {
2865
2906
  name: "firstname",
2866
2907
  type: "text"
@@ -3614,6 +3655,9 @@ Intelligence Management Platform
3614
3655
  };
3615
3656
 
3616
3657
  // src/registry/routes.ts
3658
+ import OpenAI from "openai";
3659
+ import fs2 from "fs";
3660
+ import { randomUUID } from "crypto";
3617
3661
  var REQUEST_SIZE_LIMIT = "50mb";
3618
3662
  var global_queues = {
3619
3663
  logs_cleaner: "logs-cleaner"
@@ -3794,6 +3838,7 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
3794
3838
  rights_mode: agent.rights_mode,
3795
3839
  slug: backend?.slug,
3796
3840
  rateLimit: backend?.rateLimit,
3841
+ image: agent.image,
3797
3842
  streaming: backend?.streaming,
3798
3843
  capabilities: backend?.capabilities,
3799
3844
  RBAC: agent.RBAC,
@@ -3841,6 +3886,7 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
3841
3886
  model: backend?.model?.create({ apiKey: "" }).modelId,
3842
3887
  active: agent.active,
3843
3888
  public: agent.public,
3889
+ image: agent.image,
3844
3890
  type: agent.type,
3845
3891
  rights_mode: agent.rights_mode,
3846
3892
  slug: backend?.slug,
@@ -3864,6 +3910,97 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
3864
3910
  inputSchema: tool2.inputSchema ? zerialize(tool2.inputSchema) : null
3865
3911
  })));
3866
3912
  });
3913
+ app.post("/generate/agent/image", async (req, res) => {
3914
+ console.log("[EXULU] generate/agent/image", req.body);
3915
+ const authenticationResult = await requestValidators.authenticate(req);
3916
+ if (!authenticationResult.user?.id) {
3917
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3918
+ return;
3919
+ }
3920
+ const { name, description, style } = req.body;
3921
+ if (!name || !description) {
3922
+ res.status(400).json({
3923
+ message: "Missing name or description in request."
3924
+ });
3925
+ return;
3926
+ }
3927
+ const { db: db3 } = await postgresClient();
3928
+ const variable = await db3.from("variables").where({ name: "OPENAI_IMAGE_GENERATION_API_KEY" }).first();
3929
+ if (!variable) {
3930
+ res.status(400).json({
3931
+ message: "Provider API key variable not found."
3932
+ });
3933
+ return;
3934
+ }
3935
+ let providerApiKey = variable.value;
3936
+ if (!variable.encrypted) {
3937
+ res.status(400).json({
3938
+ message: "Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
3939
+ });
3940
+ return;
3941
+ }
3942
+ if (variable.encrypted) {
3943
+ const bytes = CryptoJS3.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
3944
+ providerApiKey = bytes.toString(CryptoJS3.enc.Utf8);
3945
+ }
3946
+ const openai = new OpenAI({
3947
+ apiKey: providerApiKey
3948
+ });
3949
+ let style_reference = "";
3950
+ if (style === "origami") {
3951
+ style_reference = "minimalistic origami-style, futuristic robot, portrait, focus on face.";
3952
+ } else if (style === "anime") {
3953
+ style_reference = "minimalistic, make it in the style of a felt puppet, futuristic robot, portrait, focus on face.";
3954
+ } else if (style === "japanese_anime") {
3955
+ style_reference = "minimalistic, make it in the style of japanese anime, futuristic robot, portrait, focus on face.";
3956
+ } else if (style === "vaporwave") {
3957
+ style_reference = "minimalistic, make it in the style of a vaporwave album cover, futuristic robot, portrait, focus on face.";
3958
+ } else if (style === "lego") {
3959
+ style_reference = "minimalistic, make it in the style of LEGO minifigures, futuristic robot, portrait, focus on face.";
3960
+ } else if (style === "paper_cut") {
3961
+ style_reference = "minimalistic, make it in the style of Paper-cut style portrait with color layers, futuristic robot, portrait, focus on face.";
3962
+ } else if (style === "felt_puppet") {
3963
+ style_reference = "minimalistic, make it in the style of a felt puppet, futuristic robot, portrait, focus on face.";
3964
+ } else if (style === "app_icon") {
3965
+ style_reference = "A playful and modern app icon design of a robot, minimal flat vector style, glossy highlights, soft shadows, centered composition, high contrast, vibrant colors, rounded corners, on a transparent background, icon-friendly, no text, no details outside the frame, size is 1024x1024.";
3966
+ } else if (style === "pixel_art") {
3967
+ style_reference = "A pixel art style of a robot, minimal flat vector style, glossy highlights, soft shadows, centered composition, high contrast, vibrant colors, rounded corners, on a transparent background, icon-friendly, no text, no details outside the frame, size is 1024x1024.";
3968
+ } else if (style === "isometric") {
3969
+ style_reference = "3D isometric icon of a robot, centered composition, on a transparent background, no text, no details outside the frame, size is 1024x1024.";
3970
+ } else {
3971
+ style_reference = "A minimalist 3D, robot, portrait, focus on face, floating in space, low-poly design with pastel colors.";
3972
+ }
3973
+ const prompt = `
3974
+ A digital portrait of ${name}, visualized as a futuristic robot.
3975
+ The robot\u2019s design reflects '${description}', with props, tools, or symbolic objects that represent its expertise or area of work.
3976
+ Example: if the agent is a financial analyst, it may hold a stack of papers; if it\u2019s a creative strategist it may be painting on a canvas.
3977
+ Style: ${style_reference}.
3978
+ The portrait should have a clean background.
3979
+ Framing: bust portrait, centered.
3980
+ Mood: friendly and intelligent.
3981
+ `;
3982
+ const result = await openai.images.generate({
3983
+ model: "gpt-image-1",
3984
+ prompt
3985
+ });
3986
+ const image_base64 = result.data?.[0]?.b64_json;
3987
+ if (!image_base64) {
3988
+ res.status(500).json({
3989
+ message: "Failed to generate image."
3990
+ });
3991
+ return;
3992
+ }
3993
+ const image_bytes = Buffer.from(image_base64, "base64");
3994
+ const uuid = randomUUID();
3995
+ if (!fs2.existsSync("public")) {
3996
+ fs2.mkdirSync("public");
3997
+ }
3998
+ fs2.writeFileSync(`public/${uuid}.png`, image_bytes);
3999
+ res.status(200).json({
4000
+ message: "Image generated successfully.",
4001
+ image: `${process.env.BACKEND}/${uuid}.png`
4002
+ });
4003
+ });
3867
4004
  app.get("/tools/:id", async (req, res) => {
3868
4005
  const id = req.params.id;
3869
4006
  if (!id) {
@@ -4610,6 +4747,7 @@ var createExpressRoutes = async (app, agents, tools, contexts) => {
4610
4747
  }
4611
4748
  }
4612
4749
  });
4750
+ app.use(express.static("public"));
4613
4751
  return app;
4614
4752
  };
4615
4753
  var createCustomAnthropicStreamingMessage = (message) => {
@@ -4674,7 +4812,7 @@ var bullmq = {
4674
4812
  };
4675
4813
 
4676
4814
  // src/registry/workers.ts
4677
- import * as fs2 from "fs";
4815
+ import * as fs3 from "fs";
4678
4816
  import path2 from "path";
4679
4817
  var defaultLogsDir = path2.join(process.cwd(), "logs");
4680
4818
  var redisConnection;
@@ -4808,16 +4946,16 @@ var createLogsCleanerWorker = (logsDir) => {
4808
4946
  global_queues.logs_cleaner,
4809
4947
  async (job) => {
4810
4948
  console.log(`[EXULU] recurring job ${job.id}.`);
4811
- const folder = fs2.readdirSync(logsDir);
4949
+ const folder = fs3.readdirSync(logsDir);
4812
4950
  const files = folder.filter((file) => file.endsWith(".log"));
4813
4951
  const now = /* @__PURE__ */ new Date();
4814
4952
  const daysToKeep = job.data.ttld;
4815
4953
  const dateToKeep = new Date(now.getTime() - daysToKeep * 24 * 60 * 60 * 1e3);
4816
4954
  files.forEach((file) => {
4817
4955
  const filePath = path2.join(logsDir, file);
4818
- const fileStats = fs2.statSync(filePath);
4956
+ const fileStats = fs3.statSync(filePath);
4819
4957
  if (fileStats.mtime < dateToKeep) {
4820
- fs2.unlinkSync(filePath);
4958
+ fs3.unlinkSync(filePath);
4821
4959
  }
4822
4960
  });
4823
4961
  },
@@ -4837,7 +4975,7 @@ var createLogsCleanerWorker = (logsDir) => {
4837
4975
 
4838
4976
  // src/mcp/index.ts
4839
4977
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4840
- import { randomUUID } from "crypto";
4978
+ import { randomUUID as randomUUID2 } from "crypto";
4841
4979
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4842
4980
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4843
4981
  import { z as z2 } from "zod";
@@ -4931,7 +5069,7 @@ ${code}`
4931
5069
  transport = this.transports[sessionId];
4932
5070
  } else if (!sessionId && isInitializeRequest(req.body)) {
4933
5071
  transport = new StreamableHTTPServerTransport({
4934
- sessionIdGenerator: () => randomUUID(),
5072
+ sessionIdGenerator: () => randomUUID2(),
4935
5073
  onsessioninitialized: (sessionId2) => {
4936
5074
  this.transports[sessionId2] = transport;
4937
5075
  }
@@ -4999,6 +5137,7 @@ var defaultAgent = new ExuluAgent({
4999
5137
  description: `Basic agent without any defined tools, that can support MCP's.`,
5000
5138
  type: "agent",
5001
5139
  capabilities: {
5140
+ text: true,
5002
5141
  images: [],
5003
5142
  files: [],
5004
5143
  audio: [],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.14.0",
4
+ "version": "1.15.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -35,6 +35,10 @@
35
35
  "@types/bun": "latest",
36
36
  "@types/node": "^22.14.0",
37
37
  "@types/pg": "^8.15.1",
38
+ "@types/bcrypt": "^5.0.2",
39
+ "@types/cors": "^2.8.18",
40
+ "@types/express": "^5.0.1",
41
+ "@types/graphql-type-json": "^0.3.5",
38
42
  "conventional-changelog-atom": "^5.0.0",
39
43
  "husky": "^9.1.7",
40
44
  "semantic-release": "^24.2.7",
@@ -51,13 +55,7 @@
51
55
  "@aws-sdk/client-s3": "^3.338.0",
52
56
  "@aws-sdk/client-sts": "^3.338.0",
53
57
  "@aws-sdk/s3-request-presigner": "^3.338.0",
54
- "@inkjs/ui": "^2.0.0",
55
58
  "@modelcontextprotocol/sdk": "^1.14.0",
56
- "@types/bcrypt": "^5.0.2",
57
- "@types/cors": "^2.8.18",
58
- "@types/express": "^5.0.1",
59
- "@types/graphql-type-json": "^0.3.5",
60
- "@types/multer": "^1.4.12",
61
59
  "ai": "^5.0.15",
62
60
  "apollo-server": "^3.13.0",
63
61
  "bcryptjs": "^3.0.2",
@@ -69,21 +67,14 @@
69
67
  "csv-parse": "^5.6.0",
70
68
  "dotenv": "^16.5.0",
71
69
  "express": "^5.1.0",
72
- "fullscreen-ink": "^0.0.2",
73
70
  "graphql": "^16.11.0",
74
71
  "graphql-tools": "^9.0.18",
75
72
  "graphql-type-json": "^0.3.2",
76
73
  "http-proxy-middleware": "^3.0.5",
77
- "ink": "^6.0.0",
78
- "ink-big-text": "^2.0.0",
79
- "ink-gradient": "^3.0.0",
80
- "ink-select-input": "^6.2.0",
81
- "ink-table": "^3.1.0",
82
74
  "jose": "^6.0.10",
83
75
  "jsonwebtoken": "^9.0.2",
84
76
  "knex": "^3.1.0",
85
77
  "link": "^2.1.1",
86
- "multer": "^1.4.5-lts.2",
87
78
  "openai": "^4.94.0",
88
79
  "papaparse": "^5.5.2",
89
80
  "pg": "^8.16.3",
@@ -91,7 +82,6 @@
91
82
  "redis": "^4.7.0",
92
83
  "reflect-metadata": "^0.2.2",
93
84
  "tiktoken": "^1.0.21",
94
- "type-graphql": "^2.0.0-rc.2",
95
85
  "uninstall": "^0.0.0",
96
86
  "uuid": "^11.1.0",
97
87
  "zod": "^3.24.2",
@@ -14,6 +14,7 @@ export interface Agent {
14
14
  type?: "context";
15
15
  }[];
16
16
  capabilities?: {
17
+ text: boolean;
17
18
  images: string[];
18
19
  files: string[];
19
20
  audio: string[];
@@ -5,6 +5,7 @@ export type User = {
5
5
  type?: "api" | "user"
6
6
  anthropic_token?: string;
7
7
  super_admin?: boolean;
8
+ favourite_agents?: string[];
8
9
  role: {
9
10
  id: string;
10
11
  name: string;