@openenthrium/oe-runtime-sdk 1.7.5

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 (91) hide show
  1. package/README.md +139 -0
  2. package/index.js +58 -0
  3. package/package.json +64 -0
  4. package/src/data/connectionTypes.json +18587 -0
  5. package/src/engine/index.js +183 -0
  6. package/src/engine/llm.js +71 -0
  7. package/src/engine/promptBuilder.js +38 -0
  8. package/src/index.js +156 -0
  9. package/src/middleware/auth.js +63 -0
  10. package/src/providers/embedding/index.js +69 -0
  11. package/src/providers/llm/index.js +136 -0
  12. package/src/routes/admin.js +686 -0
  13. package/src/routes/agents.js +193 -0
  14. package/src/routes/apiKeys.js +55 -0
  15. package/src/routes/audio.js +111 -0
  16. package/src/routes/auth.js +83 -0
  17. package/src/routes/chat.js +381 -0
  18. package/src/routes/connectors.js +435 -0
  19. package/src/routes/dashboard.js +244 -0
  20. package/src/routes/documents.js +443 -0
  21. package/src/routes/embed.js +103 -0
  22. package/src/routes/marketplace.js +49 -0
  23. package/src/routes/models.js +81 -0
  24. package/src/routes/oauth.js +423 -0
  25. package/src/routes/projects.js +238 -0
  26. package/src/routes/settings.js +45 -0
  27. package/src/routes/setup.js +42 -0
  28. package/src/routes/sso.js +218 -0
  29. package/src/routes/superadmin.js +51 -0
  30. package/src/routes/templates.js +75 -0
  31. package/src/routes/threads.js +53 -0
  32. package/src/routes/workspaces.js +464 -0
  33. package/src/telemetry/bootstrap.js +39 -0
  34. package/src/telemetry/registration.js +16 -0
  35. package/src/utils/activityLog.js +16 -0
  36. package/src/utils/agentChain.js +141 -0
  37. package/src/utils/buildVisualization.js +102 -0
  38. package/src/utils/dlpScanner.js +57 -0
  39. package/src/utils/ingestionQueue.js +240 -0
  40. package/src/utils/prepareConnectors.js +66 -0
  41. package/src/utils/rag/_settings.js +6 -0
  42. package/src/utils/rag/chroma.js +82 -0
  43. package/src/utils/rag/lancedb.js +103 -0
  44. package/src/utils/rag/milvus.js +116 -0
  45. package/src/utils/rag/pgvector.js +104 -0
  46. package/src/utils/rag/pinecone.js +71 -0
  47. package/src/utils/rag/qdrant.js +106 -0
  48. package/src/utils/rag/weaviate.js +137 -0
  49. package/src/utils/rag/zilliz.js +115 -0
  50. package/src/utils/scheduler.js +168 -0
  51. package/src/utils/tier.js +106 -0
  52. package/src/utils/tools/adapters/_template.js +74 -0
  53. package/src/utils/tools/adapters/box.js +71 -0
  54. package/src/utils/tools/adapters/confluence.js +80 -0
  55. package/src/utils/tools/adapters/database.js +215 -0
  56. package/src/utils/tools/adapters/dropbox.js +74 -0
  57. package/src/utils/tools/adapters/elasticsearch.js +90 -0
  58. package/src/utils/tools/adapters/filesystem.js +197 -0
  59. package/src/utils/tools/adapters/freshdesk.js +87 -0
  60. package/src/utils/tools/adapters/gdrive.js +326 -0
  61. package/src/utils/tools/adapters/github.js +169 -0
  62. package/src/utils/tools/adapters/gmail.js +157 -0
  63. package/src/utils/tools/adapters/graphql.js +73 -0
  64. package/src/utils/tools/adapters/hubspot.js +101 -0
  65. package/src/utils/tools/adapters/image-gen.js +120 -0
  66. package/src/utils/tools/adapters/jira.js +86 -0
  67. package/src/utils/tools/adapters/kafka.js +126 -0
  68. package/src/utils/tools/adapters/ldap.js +118 -0
  69. package/src/utils/tools/adapters/mcp-client.js +138 -0
  70. package/src/utils/tools/adapters/mongodb.js +119 -0
  71. package/src/utils/tools/adapters/mqtt.js +106 -0
  72. package/src/utils/tools/adapters/music-gen.js +118 -0
  73. package/src/utils/tools/adapters/notion.js +93 -0
  74. package/src/utils/tools/adapters/ocr.js +107 -0
  75. package/src/utils/tools/adapters/onedrive.js +72 -0
  76. package/src/utils/tools/adapters/redis.js +104 -0
  77. package/src/utils/tools/adapters/rest-api.js +119 -0
  78. package/src/utils/tools/adapters/s3.js +142 -0
  79. package/src/utils/tools/adapters/search.js +80 -0
  80. package/src/utils/tools/adapters/sftp.js +121 -0
  81. package/src/utils/tools/adapters/shell.js +97 -0
  82. package/src/utils/tools/adapters/slack.js +83 -0
  83. package/src/utils/tools/adapters/speech.js +160 -0
  84. package/src/utils/tools/adapters/ssh.js +113 -0
  85. package/src/utils/tools/adapters/video-gen.js +152 -0
  86. package/src/utils/tools/adapters/web3.js +111 -0
  87. package/src/utils/tools/adapters/zendesk.js +82 -0
  88. package/src/utils/tools/adapters/zoho-mail.js +246 -0
  89. package/src/utils/tools/registry.js +229 -0
  90. package/src/utils/vectorStore.js +68 -0
  91. package/src/utils/workflowEngine.js +4 -0
@@ -0,0 +1,137 @@
1
+ const { embed } = require("../../providers/embedding");
2
+ const getSetting = require("./_settings");
3
+ const { v5: uuidv5 } = require("uuid");
4
+
5
+ const UUID_NS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
6
+
7
+ async function getConfig(cfg = {}) {
8
+ const url = cfg.url || (await getSetting("vector_db_url")) || "http://localhost:8080";
9
+ const apiKey = cfg.apiKey || (await getSetting("vector_db_api_key")) || null;
10
+ return { url, apiKey };
11
+ }
12
+
13
+ function className(workspaceSlug) {
14
+ return "Ws" + workspaceSlug.split(/[-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join("");
15
+ }
16
+
17
+ async function req(baseUrl, apiKey, method, path, body) {
18
+ const headers = { "Content-Type": "application/json" };
19
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
20
+ const res = await fetch(`${baseUrl}${path}`, {
21
+ method,
22
+ headers,
23
+ body: body != null ? JSON.stringify(body) : undefined,
24
+ signal: AbortSignal.timeout(15000),
25
+ });
26
+ if (!res.ok && res.status !== 422) {
27
+ const text = await res.text();
28
+ throw new Error(`Weaviate ${method} ${path}: ${res.status} ${text}`);
29
+ }
30
+ const text = await res.text();
31
+ return text ? JSON.parse(text) : {};
32
+ }
33
+
34
+ async function ensureClass(baseUrl, apiKey, cls, vectorDim) {
35
+ try {
36
+ await req(baseUrl, apiKey, "GET", `/v1/schema/${cls}`);
37
+ } catch {
38
+ await req(baseUrl, apiKey, "POST", "/v1/schema", {
39
+ class: cls,
40
+ vectorizer: "none",
41
+ properties: [
42
+ { name: "documentUid", dataType: ["text"], tokenization: "field" },
43
+ { name: "text", dataType: ["text"] },
44
+ { name: "metadata", dataType: ["text"] },
45
+ ],
46
+ });
47
+ }
48
+ }
49
+
50
+ async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
51
+ const { batchSize = 50, onProgress, shouldCancel } = options;
52
+ const { url, apiKey } = await getConfig(config);
53
+ const cls = className(workspaceSlug);
54
+
55
+ let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
56
+ let classReady = false;
57
+
58
+ for (let i = 0; i < chunks.length; i += batchSize) {
59
+ if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
60
+
61
+ const batch = chunks.slice(i, i + batchSize);
62
+ const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
63
+ embeddingTokens += tokensUsed;
64
+ if (!embeddingModel) embeddingModel = model;
65
+
66
+ if (!classReady) {
67
+ await ensureClass(url, apiKey, cls, embeddings[0].length);
68
+ classReady = true;
69
+ }
70
+
71
+ const objects = batch.map((c, j) => ({
72
+ class: cls,
73
+ id: uuidv5(`${documentUid}_${i + j}`, UUID_NS),
74
+ vector: embeddings[j],
75
+ properties: {
76
+ documentUid,
77
+ text: c.text,
78
+ metadata: JSON.stringify(c.metadata || {}),
79
+ },
80
+ }));
81
+
82
+ await req(url, apiKey, "POST", "/v1/batch/objects", { objects });
83
+
84
+ chunksProcessed += batch.length;
85
+ if (onProgress) await onProgress(chunksProcessed);
86
+ }
87
+ return { chunksProcessed, embeddingTokens, embeddingModel };
88
+ }
89
+
90
+ async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
91
+ try {
92
+ const { url, apiKey } = await getConfig(config);
93
+ const cls = className(workspaceSlug);
94
+ const { embeddings } = await embed([query]);
95
+ const gql = {
96
+ query: `{
97
+ Get {
98
+ ${cls}(
99
+ nearVector: { vector: [${embeddings[0].join(",")}] }
100
+ limit: ${topK}
101
+ ) {
102
+ text metadata documentUid
103
+ _additional { certainty }
104
+ }
105
+ }
106
+ }`,
107
+ };
108
+ const data = await req(url, apiKey, "POST", "/v1/graphql", gql);
109
+ const hits = data?.data?.Get?.[cls] || [];
110
+ return hits.map(h => ({
111
+ text: h.text || "",
112
+ metadata: (() => { try { return JSON.parse(h.metadata || "{}"); } catch { return {}; } })(),
113
+ score: h._additional?.certainty || 0,
114
+ }));
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
121
+ try {
122
+ const { url, apiKey } = await getConfig(config);
123
+ const cls = className(workspaceSlug);
124
+ await req(url, apiKey, "DELETE", "/v1/batch/objects", {
125
+ match: {
126
+ class: cls,
127
+ where: {
128
+ operator: "Equal",
129
+ path: ["documentUid"],
130
+ valueText: documentUid,
131
+ },
132
+ },
133
+ });
134
+ } catch {}
135
+ }
136
+
137
+ module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
@@ -0,0 +1,115 @@
1
+ const { embed } = require("../../providers/embedding");
2
+ const getSetting = require("./_settings");
3
+
4
+ let _client = null, _clientKey = null;
5
+
6
+ async function getClient(cfg = {}) {
7
+ const address = cfg.url || (await getSetting("vector_db_url")) || "";
8
+ const token = cfg.apiKey || (await getSetting("vector_db_api_key")) || "";
9
+ const key = `${address}:${token}`;
10
+ if (!_client || _clientKey !== key) {
11
+ const { MilvusClient } = require("@zilliz/milvus2-sdk-node");
12
+ _client = new MilvusClient({ address, token, ssl: true });
13
+ _clientKey = key;
14
+ }
15
+ return _client;
16
+ }
17
+
18
+ function colName(workspaceSlug) {
19
+ return `ws_${workspaceSlug.replace(/[-]/g, "_")}`;
20
+ }
21
+
22
+ async function ensureCollection(client, name, dim) {
23
+ const exists = await client.hasCollection({ collection_name: name });
24
+ if (!exists.value) {
25
+ await client.createCollection({
26
+ collection_name: name,
27
+ fields: [
28
+ { name: "id", data_type: "VarChar", max_length: 128, is_primary_key: true },
29
+ { name: "documentUid", data_type: "VarChar", max_length: 128 },
30
+ { name: "text", data_type: "VarChar", max_length: 65535 },
31
+ { name: "metadata", data_type: "VarChar", max_length: 65535 },
32
+ { name: "embedding", data_type: "FloatVector", dim },
33
+ ],
34
+ });
35
+ await client.createIndex({
36
+ collection_name: name,
37
+ field_name: "embedding",
38
+ index_type: "AUTOINDEX",
39
+ metric_type: "COSINE",
40
+ });
41
+ await client.loadCollection({ collection_name: name });
42
+ }
43
+ }
44
+
45
+ async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
46
+ const { batchSize = 50, onProgress, shouldCancel } = options;
47
+ const client = await getClient(config);
48
+ const col = colName(workspaceSlug);
49
+
50
+ let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
51
+ let collectionReady = false;
52
+
53
+ for (let i = 0; i < chunks.length; i += batchSize) {
54
+ if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
55
+
56
+ const batch = chunks.slice(i, i + batchSize);
57
+ const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
58
+ embeddingTokens += tokensUsed;
59
+ if (!embeddingModel) embeddingModel = model;
60
+
61
+ if (!collectionReady) {
62
+ await ensureCollection(client, col, embeddings[0].length);
63
+ collectionReady = true;
64
+ }
65
+
66
+ await client.insert({
67
+ collection_name: col,
68
+ data: batch.map((c, j) => ({
69
+ id: `${documentUid}_${i + j}`.slice(0, 128),
70
+ documentUid: documentUid.slice(0, 128),
71
+ text: (c.text || "").slice(0, 65535),
72
+ metadata: JSON.stringify(c.metadata || {}).slice(0, 65535),
73
+ embedding: embeddings[j],
74
+ })),
75
+ });
76
+
77
+ chunksProcessed += batch.length;
78
+ if (onProgress) await onProgress(chunksProcessed);
79
+ }
80
+ return { chunksProcessed, embeddingTokens, embeddingModel };
81
+ }
82
+
83
+ async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
84
+ try {
85
+ const client = await getClient(config);
86
+ const { embeddings } = await embed([query]);
87
+ const results = await client.search({
88
+ collection_name: colName(workspaceSlug),
89
+ vectors: [embeddings[0]],
90
+ vector_type: "FloatVector",
91
+ limit: topK,
92
+ output_fields: ["text", "metadata", "documentUid"],
93
+ metric_type: "COSINE",
94
+ });
95
+ return (results.results || []).map(r => ({
96
+ text: r.text || "",
97
+ metadata: (() => { try { return JSON.parse(r.metadata || "{}"); } catch { return {}; } })(),
98
+ score: r.score,
99
+ }));
100
+ } catch {
101
+ return [];
102
+ }
103
+ }
104
+
105
+ async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
106
+ try {
107
+ const client = await getClient(config);
108
+ await client.deleteEntities({
109
+ collection_name: colName(workspaceSlug),
110
+ expr: `documentUid == "${documentUid}"`,
111
+ });
112
+ } catch {}
113
+ }
114
+
115
+ module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
@@ -0,0 +1,168 @@
1
+ const cron = require("node-cron");
2
+ const { getToolDefinitions, getAnthropicToolDefinitions, executeTool } = require("./tools/registry");
3
+ const { getLLMClient, getSetting } = require("../providers/llm");
4
+
5
+ function applyParams(template, paramDefs, paramValues) {
6
+ let result = template || "";
7
+ for (const p of (paramDefs || [])) {
8
+ const val = String(paramValues?.[p.name] ?? p.default ?? "");
9
+ result = result.split(`{{${p.name}}}`).join(val);
10
+ }
11
+ return result;
12
+ }
13
+
14
+ const jobs = new Map(); // agentId -> cron.ScheduledTask
15
+
16
+ async function runAgent(agent, db) {
17
+ const run = await db.agentRun.create({
18
+ data: { agentId: agent.id, status: "running", triggerType: "scheduled", input: null },
19
+ });
20
+
21
+ try {
22
+ const connectorIds = JSON.parse(agent.connectorIds || "[]");
23
+ const connectors = connectorIds.length
24
+ ? await db.connector.findMany({ where: { id: { in: connectorIds }, status: "active" } })
25
+ : [];
26
+
27
+ const workspace = await db.workspace.findUnique({ where: { id: agent.workspaceId } });
28
+ const paramDefs = JSON.parse(agent.params || "[]");
29
+ const { buildSystemPrompt } = require("./workflowEngine");
30
+ const systemPrompt = applyParams(
31
+ buildSystemPrompt(agent) || "You are a helpful AI agent. Complete the task given to you using the available tools.",
32
+ paramDefs, null
33
+ ) + "\n\nIMPORTANT: This is an automated scheduled run. Execute the task immediately using the available tools. Do not ask for clarification.";
34
+
35
+ const userTask = "Run the scheduled agent task.";
36
+
37
+ const { provider, client } = await getLLMClient();
38
+ const model = (await getSetting("llm_model")) || process.env.OPENAI_MODEL || process.env.OLLAMA_MODEL || "gpt-4o";
39
+
40
+ const MAX_ROUNDS = workspace?.defaultAgentMaxRounds || 25;
41
+ let fullOutput = "";
42
+
43
+ if (provider === "anthropic") {
44
+ const tools = getAnthropicToolDefinitions(connectors);
45
+ const msgs = [{ role: "user", content: userTask }];
46
+
47
+ for (let round = 0; round < MAX_ROUNDS; round++) {
48
+ const resp = await client.messages.create({
49
+ model: model || "claude-3-5-sonnet-20241022",
50
+ max_tokens: 4096,
51
+ system: systemPrompt,
52
+ messages: msgs,
53
+ tools: tools.length ? tools : undefined,
54
+ temperature: 0.3,
55
+ });
56
+
57
+ if (resp.stop_reason !== "tool_use" || !tools.length) {
58
+ fullOutput = resp.content?.find(b => b.type === "text")?.text || "";
59
+ break;
60
+ }
61
+
62
+ const toolUseBlocks = resp.content.filter(b => b.type === "tool_use");
63
+ msgs.push({ role: "assistant", content: resp.content });
64
+ const toolResults = [];
65
+ for (const tb of toolUseBlocks) {
66
+ const result = await executeTool(tb.name, tb.input, connectors, db);
67
+ toolResults.push({ type: "tool_result", tool_use_id: tb.id, content: String(result) });
68
+ }
69
+ msgs.push({ role: "user", content: toolResults });
70
+
71
+ if (round === MAX_ROUNDS - 1) {
72
+ const finalResp = await client.messages.create({
73
+ model: model || "claude-3-5-sonnet-20241022",
74
+ max_tokens: 4096, system: systemPrompt, messages: msgs, temperature: 0.3,
75
+ });
76
+ fullOutput = finalResp.content?.find(b => b.type === "text")?.text || "";
77
+ }
78
+ }
79
+ } else {
80
+ const tools = getToolDefinitions(connectors);
81
+ const messages = [{ role: "system", content: systemPrompt }, { role: "user", content: userTask }];
82
+
83
+ for (let round = 0; round < MAX_ROUNDS; round++) {
84
+ const reqBody = { model, messages, temperature: 0.3, max_tokens: 4096 };
85
+ if (tools.length) { reqBody.tools = tools; reqBody.tool_choice = "auto"; }
86
+
87
+ const resp = await client.chat.completions.create(reqBody);
88
+ const choice = resp.choices[0];
89
+
90
+ if (choice.finish_reason !== "tool_calls" || !tools.length) {
91
+ fullOutput = choice.message.content || "";
92
+ break;
93
+ }
94
+
95
+ messages.push(choice.message);
96
+ for (const tc of choice.message.tool_calls || []) {
97
+ let args = {};
98
+ try { args = JSON.parse(tc.function.arguments); } catch { /* */ }
99
+ const result = await executeTool(tc.function.name, args, connectors, db);
100
+ messages.push({ role: "tool", tool_call_id: tc.id, content: String(result) });
101
+ }
102
+
103
+ if (round === MAX_ROUNDS - 1) {
104
+ const finalResp = await client.chat.completions.create({ model, messages, temperature: 0.3, max_tokens: 4096 });
105
+ fullOutput = finalResp.choices[0].message.content || "";
106
+ }
107
+ }
108
+ }
109
+
110
+ await db.agentRun.update({
111
+ where: { id: run.id },
112
+ data: { status: "success", output: fullOutput, completedAt: new Date() },
113
+ });
114
+ console.log(`[scheduler] Agent "${agent.name}" (id=${agent.id}) completed.`);
115
+
116
+ // Chain to next agent if configured
117
+ const { maybeChain } = require("./agentChain");
118
+ await maybeChain(agent, fullOutput, db);
119
+ } catch (err) {
120
+ await db.agentRun.update({
121
+ where: { id: run.id },
122
+ data: { status: "error", error: err.message, completedAt: new Date() },
123
+ });
124
+ console.error(`[scheduler] Agent "${agent.name}" failed:`, err.message);
125
+ }
126
+ }
127
+
128
+ function scheduleAgent(agent, db) {
129
+ if (jobs.has(agent.id)) {
130
+ jobs.get(agent.id).stop();
131
+ jobs.delete(agent.id);
132
+ }
133
+ if (!agent.enabled || agent.triggerType !== "scheduled" || !agent.cronExpression) return;
134
+ if (!cron.validate(agent.cronExpression)) {
135
+ console.warn(`[scheduler] Invalid cron "${agent.cronExpression}" for agent ${agent.id}`);
136
+ return;
137
+ }
138
+ const task = cron.schedule(agent.cronExpression, () => runAgent(agent, db));
139
+ jobs.set(agent.id, task);
140
+ console.log(`[scheduler] Scheduled agent "${agent.name}" (id=${agent.id}) with cron "${agent.cronExpression}"`);
141
+ }
142
+
143
+ function unscheduleAgent(agentId) {
144
+ if (jobs.has(agentId)) {
145
+ jobs.get(agentId).stop();
146
+ jobs.delete(agentId);
147
+ console.log(`[scheduler] Removed job for agent id=${agentId}`);
148
+ }
149
+ }
150
+
151
+ async function init(db) {
152
+ // Mark any runs that were mid-flight when the server last died
153
+ const orphaned = await db.agentRun.updateMany({
154
+ where: { status: "running" },
155
+ data: { status: "error", error: "Server restarted mid-run", completedAt: new Date() },
156
+ });
157
+ if (orphaned.count > 0) console.log(`[scheduler] Marked ${orphaned.count} orphaned run(s) as error.`);
158
+
159
+ const agents = await db.agent.findMany({
160
+ where: { triggerType: "scheduled", enabled: true },
161
+ });
162
+ for (const agent of agents) {
163
+ scheduleAgent(agent, db);
164
+ }
165
+ console.log(`[scheduler] Initialized ${agents.length} scheduled agent(s).`);
166
+ }
167
+
168
+ module.exports = { init, scheduleAgent, unscheduleAgent, runAgent };
@@ -0,0 +1,106 @@
1
+ const TIERS = {
2
+ starter: {
3
+ maxConnectors: 10,
4
+ maxAgentRunsPerMonth: 500,
5
+ ingestionSpaceGb: 100,
6
+ bullmq: false, sso: false, customBranding: false, ha: false,
7
+ },
8
+ professional: {
9
+ maxConnectors: 50,
10
+ maxAgentRunsPerMonth: 5000,
11
+ ingestionSpaceGb: 1024,
12
+ bullmq: true, sso: false, customBranding: true, ha: false,
13
+ },
14
+ enterprise: {
15
+ maxConnectors: Infinity,
16
+ maxAgentRunsPerMonth: Infinity,
17
+ ingestionSpaceGb: Infinity,
18
+ bullmq: true, sso: true, customBranding: true, ha: true,
19
+ },
20
+ };
21
+
22
+ // Sync fallback (no DB) — used when db is not available
23
+ function getTier() {
24
+ const name = (process.env.OE_TIER || "starter").toLowerCase();
25
+ const base = TIERS[name] || TIERS.starter;
26
+ return { name, ...base };
27
+ }
28
+
29
+ // Async — checks DB settings first, then env, then defaults
30
+ async function getTierFromDB(db) {
31
+ if (!db) return getTier();
32
+
33
+ try {
34
+ const rows = await db.setting.findMany({
35
+ where: { key: { in: ["tier.maxConnectors", "tier.maxAgentRunsPerMonth", "tier.ingestionSpaceGb", "tier.maxWorkspaces", "tier.maxUsers"] } },
36
+ });
37
+ const s = {};
38
+ for (const r of rows) s[r.key] = r.value;
39
+
40
+ return {
41
+ name: "custom",
42
+ maxConnectors: s["tier.maxConnectors"] ? Number(s["tier.maxConnectors"]) : Infinity,
43
+ maxAgentRunsPerMonth: s["tier.maxAgentRunsPerMonth"] ? Number(s["tier.maxAgentRunsPerMonth"]) : Infinity,
44
+ ingestionSpaceGb: s["tier.ingestionSpaceGb"] ? Number(s["tier.ingestionSpaceGb"]) : Infinity,
45
+ maxWorkspaces: s["tier.maxWorkspaces"] ? Number(s["tier.maxWorkspaces"]) : Infinity,
46
+ maxUsers: s["tier.maxUsers"] ? Number(s["tier.maxUsers"]) : Infinity,
47
+ bullmq: true, sso: true, customBranding: true, ha: true,
48
+ };
49
+ } catch {
50
+ return getTier();
51
+ }
52
+ }
53
+
54
+ async function canAddConnector(currentCount, db) {
55
+ const tier = await getTierFromDB(db);
56
+ return currentCount < tier.maxConnectors;
57
+ }
58
+
59
+ // Returns YYYY-MM string for the current month
60
+ function currentMonthKey() {
61
+ const now = new Date();
62
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
63
+ }
64
+
65
+ async function getAgentRunsThisMonth(db) {
66
+ if (!db) return 0;
67
+ try {
68
+ const [monthRow, countRow] = await Promise.all([
69
+ db.setting.findUnique({ where: { key: "usage.agentRuns.month" } }),
70
+ db.setting.findUnique({ where: { key: "usage.agentRuns.count" } }),
71
+ ]);
72
+ if (monthRow?.value !== currentMonthKey()) return 0;
73
+ return parseInt(countRow?.value || "0");
74
+ } catch { return 0; }
75
+ }
76
+
77
+ async function incrementAgentRun(db) {
78
+ if (!db) return;
79
+ const month = currentMonthKey();
80
+ try {
81
+ const monthRow = await db.setting.findUnique({ where: { key: "usage.agentRuns.month" } });
82
+ if (monthRow?.value !== month) {
83
+ // New month — reset
84
+ await Promise.all([
85
+ db.setting.upsert({ where: { key: "usage.agentRuns.month" }, create: { key: "usage.agentRuns.month", value: month }, update: { value: month } }),
86
+ db.setting.upsert({ where: { key: "usage.agentRuns.count" }, create: { key: "usage.agentRuns.count", value: "1" }, update: { value: "1" } }),
87
+ ]);
88
+ } else {
89
+ const current = parseInt((await db.setting.findUnique({ where: { key: "usage.agentRuns.count" } }))?.value || "0");
90
+ await db.setting.upsert({
91
+ where: { key: "usage.agentRuns.count" },
92
+ create: { key: "usage.agentRuns.count", value: String(current + 1) },
93
+ update: { value: String(current + 1) },
94
+ });
95
+ }
96
+ } catch { /* non-fatal */ }
97
+ }
98
+
99
+ async function canRunAgent(db) {
100
+ const tier = await getTierFromDB(db);
101
+ if (!isFinite(tier.maxAgentRunsPerMonth)) return true;
102
+ const used = await getAgentRunsThisMonth(db);
103
+ return used < tier.maxAgentRunsPerMonth;
104
+ }
105
+
106
+ module.exports = { getTier, getTierFromDB, canAddConnector, getAgentRunsThisMonth, incrementAgentRun, canRunAgent };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ // ══════════════════════════════════════════════════════════════════════════════
4
+ // Open Enthrium — Connector Adapter Template
5
+ // Copy this file, rename it to <your-connector>.js, and implement the three
6
+ // exported functions below.
7
+ //
8
+ // Steps to contribute a new connector:
9
+ // 1. Copy this file → adapters/your-connector.js
10
+ // 2. Add an entry → server/src/data/connectionTypes.json
11
+ // { "id": "your-connector", "label": "Your Connector", "color": "bg-blue-600", "initial": "YC", "cat": "Your Category" }
12
+ // 3. Restart the server — auto-discovery picks up your file immediately.
13
+ //
14
+ // That's it. No registry edits, no seed script changes required.
15
+ // ══════════════════════════════════════════════════════════════════════════════
16
+
17
+ // ── Tool definitions (OpenAI format) ─────────────────────────────────────────
18
+ // Return an array of tools this connector exposes to the agent.
19
+ // Each tool becomes a callable function in the agentic loop.
20
+ function getToolDefinitions(connector) {
21
+ return [
22
+ {
23
+ type: "function",
24
+ function: {
25
+ name: `conn_${connector.id}_your_action`,
26
+ description: `What this action does on "${connector.name}"`,
27
+ parameters: {
28
+ type: "object",
29
+ properties: {
30
+ your_param: { type: "string", description: "Describe what this param does" },
31
+ },
32
+ required: ["your_param"],
33
+ },
34
+ },
35
+ },
36
+ // Add more tools here…
37
+ ];
38
+ }
39
+
40
+ // ── Tool definitions (Anthropic format) ──────────────────────────────────────
41
+ // Converts OpenAI-style definitions to Anthropic's input_schema format.
42
+ // In most cases you don't need to change this — just keep it as-is.
43
+ function getAnthropicToolDefinitions(connector) {
44
+ return getToolDefinitions(connector).map(t => ({
45
+ name: t.function.name,
46
+ description: t.function.description,
47
+ input_schema: t.function.parameters,
48
+ }));
49
+ }
50
+
51
+ // ── Execute ───────────────────────────────────────────────────────────────────
52
+ // Called by the engine when the agent invokes one of your tools.
53
+ //
54
+ // Parameters:
55
+ // action — the part after conn_<id>_ (e.g. "your_action")
56
+ // args — object of parsed arguments from the agent
57
+ // connector — connector row from DB: connector.id, connector.name, connector.auth (JSON string)
58
+ // db — Prisma client (available if you need to persist anything)
59
+ //
60
+ // Return a string — the engine sends it back to the LLM as the tool result.
61
+ async function executeTool(action, args, connector, db) {
62
+ const auth = JSON.parse(connector.auth || "{}");
63
+ // auth contains whatever fields the user configured (apiKey, baseUrl, etc.)
64
+
65
+ if (action === "your_action") {
66
+ // TODO: implement your logic here
67
+ // const result = await yourApiCall(auth.apiKey, args.your_param);
68
+ return JSON.stringify({ result: "replace with real implementation" });
69
+ }
70
+
71
+ return `Unknown action: ${action}`;
72
+ }
73
+
74
+ module.exports = { getToolDefinitions, getAnthropicToolDefinitions, executeTool };
@@ -0,0 +1,71 @@
1
+ const axios = require("axios");
2
+
3
+ function auth(connector) { return connector.authConfig ? JSON.parse(connector.authConfig) : {}; }
4
+ function cfg(connector) { return connector.config ? JSON.parse(connector.config) : {}; }
5
+
6
+ async function getToken(connector, db) {
7
+ const a = auth(connector);
8
+ const c = cfg(connector);
9
+ if (!a.refreshToken) return a.accessToken;
10
+ if (a.expiresAt && Date.now() < a.expiresAt - 60000) return a.accessToken;
11
+ const { data } = await axios.post("https://api.box.com/oauth2/token",
12
+ new URLSearchParams({ grant_type: "refresh_token", refresh_token: a.refreshToken, client_id: c.clientId, client_secret: c.clientSecret }),
13
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
14
+ const newAuth = { ...a, accessToken: data.access_token, refreshToken: data.refresh_token || a.refreshToken, expiresAt: Date.now() + data.expires_in * 1000 };
15
+ if (db) await db.connector.update({ where: { id: connector.id }, data: { authConfig: JSON.stringify(newAuth) } });
16
+ return data.access_token;
17
+ }
18
+
19
+ function client(token) {
20
+ return axios.create({ baseURL: "https://api.box.com/2.0", headers: { Authorization: `Bearer ${token}` } });
21
+ }
22
+
23
+ const TOOLS = c => [
24
+ { action: "list_files", desc: `List files/folders in Box via ${c.name}.`,
25
+ params: { folderId: { type: "string", description: 'Folder ID. Use "0" for root.' } }, required: [] },
26
+ { action: "read_file", desc: `Read the text content of a Box file via ${c.name}.`,
27
+ params: { fileId: { type: "string", description: "Box file ID." } }, required: ["fileId"] },
28
+ { action: "search", desc: `Search files in Box via ${c.name}.`,
29
+ params: { query: { type: "string", description: "Search query." } }, required: ["query"] },
30
+ ];
31
+
32
+ function getToolDefinitions(connector) {
33
+ return TOOLS(connector).map(t => ({ type: "function", function: { name: `conn_${connector.id}_${t.action}`, description: t.desc, parameters: { type: "object", properties: t.params, required: t.required } } }));
34
+ }
35
+ function getAnthropicToolDefinitions(connector) {
36
+ return TOOLS(connector).map(t => ({ name: `conn_${connector.id}_${t.action}`, description: t.desc, input_schema: { type: "object", properties: t.params, required: t.required } }));
37
+ }
38
+
39
+ async function executeTool(action, args, connector, db) {
40
+ const token = await getToken(connector, db);
41
+ const api = client(token);
42
+ try {
43
+ if (action === "list_files") {
44
+ const id = args.folderId || "0";
45
+ const res = await api.get(`/folders/${id}/items?limit=100`);
46
+ const entries = res.data.entries || [];
47
+ return entries.map(e => `${e.type === "folder" ? "📁" : "📄"} ${e.name} (id: ${e.id})`).join("\n") || "Empty folder.";
48
+ }
49
+ if (action === "read_file") {
50
+ const res = await api.get(`/files/${args.fileId}/content`, { responseType: "text" });
51
+ return String(res.data).slice(0, 8000);
52
+ }
53
+ if (action === "search") {
54
+ const res = await api.get(`/search?query=${encodeURIComponent(args.query)}&limit=20`);
55
+ const entries = res.data.entries || [];
56
+ return entries.map(e => `${e.name} (id: ${e.id}, type: ${e.type})`).join("\n") || "No results.";
57
+ }
58
+ return `Unknown Box action: ${action}`;
59
+ } catch (err) { return `Box error: ${err.response?.data?.message || err.message}`; }
60
+ }
61
+
62
+ async function testConnection(authConfig, config, db, connectorId) {
63
+ try {
64
+ const connector = { authConfig: JSON.stringify(authConfig), config: JSON.stringify(config || {}), id: connectorId };
65
+ const token = await getToken(connector, db);
66
+ const res = await client(token).get("/users/me");
67
+ return !!res.data.id;
68
+ } catch { return false; }
69
+ }
70
+
71
+ module.exports = { getToolDefinitions, getAnthropicToolDefinitions, executeTool, testConnection, getToken };