@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.
- package/README.md +139 -0
- package/index.js +58 -0
- package/package.json +64 -0
- package/src/data/connectionTypes.json +18587 -0
- package/src/engine/index.js +183 -0
- package/src/engine/llm.js +71 -0
- package/src/engine/promptBuilder.js +38 -0
- package/src/index.js +156 -0
- package/src/middleware/auth.js +63 -0
- package/src/providers/embedding/index.js +69 -0
- package/src/providers/llm/index.js +136 -0
- package/src/routes/admin.js +686 -0
- package/src/routes/agents.js +193 -0
- package/src/routes/apiKeys.js +55 -0
- package/src/routes/audio.js +111 -0
- package/src/routes/auth.js +83 -0
- package/src/routes/chat.js +381 -0
- package/src/routes/connectors.js +435 -0
- package/src/routes/dashboard.js +244 -0
- package/src/routes/documents.js +443 -0
- package/src/routes/embed.js +103 -0
- package/src/routes/marketplace.js +49 -0
- package/src/routes/models.js +81 -0
- package/src/routes/oauth.js +423 -0
- package/src/routes/projects.js +238 -0
- package/src/routes/settings.js +45 -0
- package/src/routes/setup.js +42 -0
- package/src/routes/sso.js +218 -0
- package/src/routes/superadmin.js +51 -0
- package/src/routes/templates.js +75 -0
- package/src/routes/threads.js +53 -0
- package/src/routes/workspaces.js +464 -0
- package/src/telemetry/bootstrap.js +39 -0
- package/src/telemetry/registration.js +16 -0
- package/src/utils/activityLog.js +16 -0
- package/src/utils/agentChain.js +141 -0
- package/src/utils/buildVisualization.js +102 -0
- package/src/utils/dlpScanner.js +57 -0
- package/src/utils/ingestionQueue.js +240 -0
- package/src/utils/prepareConnectors.js +66 -0
- package/src/utils/rag/_settings.js +6 -0
- package/src/utils/rag/chroma.js +82 -0
- package/src/utils/rag/lancedb.js +103 -0
- package/src/utils/rag/milvus.js +116 -0
- package/src/utils/rag/pgvector.js +104 -0
- package/src/utils/rag/pinecone.js +71 -0
- package/src/utils/rag/qdrant.js +106 -0
- package/src/utils/rag/weaviate.js +137 -0
- package/src/utils/rag/zilliz.js +115 -0
- package/src/utils/scheduler.js +168 -0
- package/src/utils/tier.js +106 -0
- package/src/utils/tools/adapters/_template.js +74 -0
- package/src/utils/tools/adapters/box.js +71 -0
- package/src/utils/tools/adapters/confluence.js +80 -0
- package/src/utils/tools/adapters/database.js +215 -0
- package/src/utils/tools/adapters/dropbox.js +74 -0
- package/src/utils/tools/adapters/elasticsearch.js +90 -0
- package/src/utils/tools/adapters/filesystem.js +197 -0
- package/src/utils/tools/adapters/freshdesk.js +87 -0
- package/src/utils/tools/adapters/gdrive.js +326 -0
- package/src/utils/tools/adapters/github.js +169 -0
- package/src/utils/tools/adapters/gmail.js +157 -0
- package/src/utils/tools/adapters/graphql.js +73 -0
- package/src/utils/tools/adapters/hubspot.js +101 -0
- package/src/utils/tools/adapters/image-gen.js +120 -0
- package/src/utils/tools/adapters/jira.js +86 -0
- package/src/utils/tools/adapters/kafka.js +126 -0
- package/src/utils/tools/adapters/ldap.js +118 -0
- package/src/utils/tools/adapters/mcp-client.js +138 -0
- package/src/utils/tools/adapters/mongodb.js +119 -0
- package/src/utils/tools/adapters/mqtt.js +106 -0
- package/src/utils/tools/adapters/music-gen.js +118 -0
- package/src/utils/tools/adapters/notion.js +93 -0
- package/src/utils/tools/adapters/ocr.js +107 -0
- package/src/utils/tools/adapters/onedrive.js +72 -0
- package/src/utils/tools/adapters/redis.js +104 -0
- package/src/utils/tools/adapters/rest-api.js +119 -0
- package/src/utils/tools/adapters/s3.js +142 -0
- package/src/utils/tools/adapters/search.js +80 -0
- package/src/utils/tools/adapters/sftp.js +121 -0
- package/src/utils/tools/adapters/shell.js +97 -0
- package/src/utils/tools/adapters/slack.js +83 -0
- package/src/utils/tools/adapters/speech.js +160 -0
- package/src/utils/tools/adapters/ssh.js +113 -0
- package/src/utils/tools/adapters/video-gen.js +152 -0
- package/src/utils/tools/adapters/web3.js +111 -0
- package/src/utils/tools/adapters/zendesk.js +82 -0
- package/src/utils/tools/adapters/zoho-mail.js +246 -0
- package/src/utils/tools/registry.js +229 -0
- package/src/utils/vectorStore.js +68 -0
- package/src/utils/workflowEngine.js +4 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
|
|
3
|
+
function cfg(connector) {
|
|
4
|
+
return connector.authConfig ? JSON.parse(connector.authConfig) : {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function client({ domain, apiKey }) {
|
|
8
|
+
return axios.create({
|
|
9
|
+
baseURL: `https://${domain}/api/v2`,
|
|
10
|
+
auth: { username: apiKey, password: "X" },
|
|
11
|
+
headers: { "Content-Type": "application/json" },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOOLS = c => [
|
|
16
|
+
{ action: "list_tickets", desc: `List support tickets from Freshdesk via ${c.name}.`,
|
|
17
|
+
params: { status: { type: "string", description: "Filter by status: open, pending, resolved, closed." },
|
|
18
|
+
priority: { type: "string", description: "Filter by priority: low, medium, high, urgent." } }, required: [] },
|
|
19
|
+
{ action: "get_ticket", desc: `Get a Freshdesk ticket by ID via ${c.name}.`,
|
|
20
|
+
params: { ticketId: { type: "number", description: "Ticket ID." } }, required: ["ticketId"] },
|
|
21
|
+
{ action: "create_ticket", desc: `Create a Freshdesk support ticket via ${c.name}.`,
|
|
22
|
+
params: { subject: { type: "string", description: "Ticket subject." },
|
|
23
|
+
description: { type: "string", description: "Ticket description." },
|
|
24
|
+
email: { type: "string", description: "Requester email." },
|
|
25
|
+
priority: { type: "number", description: "Priority: 1=low, 2=medium, 3=high, 4=urgent. Default: 2." } }, required: ["subject","description","email"] },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const STATUS_MAP = { open: 2, pending: 3, resolved: 4, closed: 5 };
|
|
29
|
+
const PRIORITY_MAP = { low: 1, medium: 2, high: 3, urgent: 4 };
|
|
30
|
+
|
|
31
|
+
function getToolDefinitions(connector) {
|
|
32
|
+
return TOOLS(connector).map(t => ({
|
|
33
|
+
type: "function",
|
|
34
|
+
function: { name: `conn_${connector.id}_${t.action}`, description: t.desc,
|
|
35
|
+
parameters: { type: "object", properties: t.params, required: t.required } },
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getAnthropicToolDefinitions(connector) {
|
|
40
|
+
return TOOLS(connector).map(t => ({
|
|
41
|
+
name: `conn_${connector.id}_${t.action}`, description: t.desc,
|
|
42
|
+
input_schema: { type: "object", properties: t.params, required: t.required },
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function executeTool(action, args, connector) {
|
|
47
|
+
const creds = cfg(connector);
|
|
48
|
+
if (!creds.domain || !creds.apiKey) return "Freshdesk not configured. Please add credentials in Integrations.";
|
|
49
|
+
const api = client(creds);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (action === "list_tickets") {
|
|
53
|
+
const params = new URLSearchParams({ per_page: "15" });
|
|
54
|
+
if (args.status) params.set("status", STATUS_MAP[args.status] || 2);
|
|
55
|
+
if (args.priority) params.set("priority", PRIORITY_MAP[args.priority] || 2);
|
|
56
|
+
const res = await api.get(`/tickets?${params}`);
|
|
57
|
+
const tickets = res.data || [];
|
|
58
|
+
if (!tickets.length) return "No tickets found.";
|
|
59
|
+
return tickets.map(t => `[#${t.id}] ${t.subject} — Status: ${t.status}, Priority: ${t.priority}`).join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action === "get_ticket") {
|
|
63
|
+
const res = await api.get(`/tickets/${args.ticketId}`);
|
|
64
|
+
const t = res.data;
|
|
65
|
+
return JSON.stringify({ id: t.id, subject: t.subject, status: t.status, priority: t.priority, requester: t.requester_id, created: t.created_at, description: t.description_text?.slice(0, 500) }, null, 2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (action === "create_ticket") {
|
|
69
|
+
const { subject, description, email, priority = 2 } = args;
|
|
70
|
+
const res = await api.post("/tickets", { subject, description, email, priority, source: 2 });
|
|
71
|
+
return `Created ticket #${res.data.id}: ${subject}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `Unknown Freshdesk action: ${action}`;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return `Freshdesk error: ${err.response?.data?.description || err.message}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function testConnection(authConfig) {
|
|
81
|
+
try {
|
|
82
|
+
const res = await client(authConfig).get("/tickets?per_page=1");
|
|
83
|
+
return Array.isArray(res.data);
|
|
84
|
+
} catch { return false; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { getToolDefinitions, getAnthropicToolDefinitions, executeTool, testConnection };
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const { google } = require("googleapis");
|
|
2
|
+
const { makeOAuth2Client } = require("./gmail");
|
|
3
|
+
|
|
4
|
+
async function buildDriveClient(authConfig, db, workspaceId) {
|
|
5
|
+
const oauth2 = await makeOAuth2Client(db, workspaceId, "/api/oauth/gmail/callback", {
|
|
6
|
+
clientId: authConfig.clientId,
|
|
7
|
+
clientSecret: authConfig.clientSecret,
|
|
8
|
+
});
|
|
9
|
+
oauth2.setCredentials({
|
|
10
|
+
access_token: authConfig.accessToken,
|
|
11
|
+
refresh_token: authConfig.refreshToken,
|
|
12
|
+
expiry_date: authConfig.expiresAt,
|
|
13
|
+
});
|
|
14
|
+
return google.drive({ version: "v3", auth: oauth2 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getToolDefinitions(connector) {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
type: "function",
|
|
21
|
+
function: {
|
|
22
|
+
name: `conn_${connector.id}_list_files`,
|
|
23
|
+
description: `List files in the connected Google Drive (${connector.name}). Use to find files by name or type.`,
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
query: { type: "string", description: "Search query e.g. 'name contains apollo' or 'mimeType=text/csv'" },
|
|
28
|
+
},
|
|
29
|
+
required: []
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "function",
|
|
35
|
+
function: {
|
|
36
|
+
name: `conn_${connector.id}_read_file`,
|
|
37
|
+
description: `Download and read a CSV or Excel file from Google Drive (${connector.name}). Returns rows as JSON. Use to read prospect lists, data exports, etc.`,
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
fileId: { type: "string", description: "Google Drive file ID (from list_files)." },
|
|
42
|
+
fileName: { type: "string", description: "File name to search for if fileId is unknown." },
|
|
43
|
+
limit: { type: "number", description: "Max rows to return (default 100)." },
|
|
44
|
+
offset: { type: "number", description: "Skip first N rows — use for pagination." },
|
|
45
|
+
},
|
|
46
|
+
required: []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
,
|
|
51
|
+
{
|
|
52
|
+
type: "function",
|
|
53
|
+
function: {
|
|
54
|
+
name: `conn_${connector.id}_write_file`,
|
|
55
|
+
description: `Update an existing CSV or text file in Google Drive (${connector.name}). Use after reading and modifying rows to write the updated data back.`,
|
|
56
|
+
parameters: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
fileId: { type: "string", description: "Google Drive file ID to overwrite." },
|
|
60
|
+
fileName: { type: "string", description: "File name to search for if fileId is unknown." },
|
|
61
|
+
rows: { type: "array", description: "Array of row objects — the full updated dataset to write back.", items: { type: "object" } },
|
|
62
|
+
},
|
|
63
|
+
required: ["rows"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "function",
|
|
69
|
+
function: {
|
|
70
|
+
name: `conn_${connector.id}_update_rows`,
|
|
71
|
+
description: `Partially update rows in a CSV file in Google Drive (${connector.name}) without reading the whole file. Finds rows where matchColumn equals one of matchValues and sets setColumn to setValue. All other rows are preserved unchanged. Use this instead of write_file when updating a small number of rows in a large file.`,
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
fileId: { type: "string", description: "Google Drive file ID." },
|
|
76
|
+
fileName: { type: "string", description: "File name to search for if fileId is unknown." },
|
|
77
|
+
matchColumn: { type: "string", description: "Column name to match against (e.g. 'Email')." },
|
|
78
|
+
matchValues: { type: "array", description: "List of values to match in matchColumn (e.g. list of email addresses).", items: { type: "string" } },
|
|
79
|
+
setColumn: { type: "string", description: "Column name to update (e.g. 'First Email Sent')." },
|
|
80
|
+
setValue: { type: "string", description: "Value to set in setColumn for matched rows (e.g. 'yes')." },
|
|
81
|
+
},
|
|
82
|
+
required: ["matchColumn", "matchValues", "setColumn", "setValue"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getAnthropicToolDefinitions(connector) {
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
name: `conn_${connector.id}_list_files`,
|
|
93
|
+
description: `List files in the connected Google Drive (${connector.name}).`,
|
|
94
|
+
input_schema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
query: { type: "string", description: "Search query e.g. 'name contains apollo'" },
|
|
98
|
+
},
|
|
99
|
+
required: []
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: `conn_${connector.id}_read_file`,
|
|
104
|
+
description: `Download and read a CSV or Excel file from Google Drive (${connector.name}). Returns rows as JSON.`,
|
|
105
|
+
input_schema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
fileId: { type: "string", description: "Google Drive file ID (from list_files)." },
|
|
109
|
+
fileName: { type: "string", description: "File name to search for if fileId is unknown." },
|
|
110
|
+
limit: { type: "number", description: "Max rows to return (default 100)." },
|
|
111
|
+
offset: { type: "number", description: "Skip first N rows — use for pagination." },
|
|
112
|
+
},
|
|
113
|
+
required: []
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: `conn_${connector.id}_write_file`,
|
|
118
|
+
description: `Update an existing CSV file in Google Drive (${connector.name}). Use after modifying rows to write the updated data back.`,
|
|
119
|
+
input_schema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
fileId: { type: "string", description: "Google Drive file ID to overwrite." },
|
|
123
|
+
fileName: { type: "string", description: "File name to search for if fileId is unknown." },
|
|
124
|
+
rows: { type: "array", description: "Full updated array of row objects to write back.", items: { type: "object" } },
|
|
125
|
+
},
|
|
126
|
+
required: ["rows"]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: `conn_${connector.id}_update_rows`,
|
|
131
|
+
description: `Partially update rows in a CSV file in Google Drive (${connector.name}) without reading the whole file. Finds rows where matchColumn equals one of matchValues and sets setColumn to setValue. All other rows are preserved. Use this instead of write_file when updating a small number of rows in a large file.`,
|
|
132
|
+
input_schema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
fileId: { type: "string", description: "Google Drive file ID." },
|
|
136
|
+
fileName: { type: "string", description: "File name to search for if fileId is unknown." },
|
|
137
|
+
matchColumn: { type: "string", description: "Column name to match against (e.g. 'Email')." },
|
|
138
|
+
matchValues: { type: "array", description: "List of values to match (e.g. list of email addresses).", items: { type: "string" } },
|
|
139
|
+
setColumn: { type: "string", description: "Column name to update (e.g. 'First Email Sent')." },
|
|
140
|
+
setValue: { type: "string", description: "Value to set for matched rows (e.g. 'yes')." },
|
|
141
|
+
},
|
|
142
|
+
required: ["matchColumn", "matchValues", "setColumn", "setValue"]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rowsToCSV(rows) {
|
|
149
|
+
if (!rows.length) return "";
|
|
150
|
+
const headers = Object.keys(rows[0]);
|
|
151
|
+
const escape = val => {
|
|
152
|
+
const s = String(val ?? "");
|
|
153
|
+
return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}"` : s;
|
|
154
|
+
};
|
|
155
|
+
return [headers.join(","), ...rows.map(row => headers.map(h => escape(row[h])).join(","))].join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseCSV(text) {
|
|
159
|
+
const lines = text.split(/\r?\n/).filter(l => l.trim());
|
|
160
|
+
if (!lines.length) return [];
|
|
161
|
+
const headers = lines[0].split(",").map(h => h.replace(/^"|"$/g, "").trim());
|
|
162
|
+
return lines.slice(1).map(line => {
|
|
163
|
+
const vals = line.match(/(".*?"|[^,]+|(?<=,)(?=,)|^(?=,)|(?<=,)$)/g) || [];
|
|
164
|
+
const row = {};
|
|
165
|
+
headers.forEach((h, i) => {
|
|
166
|
+
row[h] = (vals[i] || "").replace(/^"|"$/g, "").trim();
|
|
167
|
+
});
|
|
168
|
+
return row;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function executeTool(action, args, connector, db) {
|
|
173
|
+
const authConfig = connector.authConfig ? JSON.parse(connector.authConfig) : {};
|
|
174
|
+
if (!authConfig.refreshToken) return "Google Drive not connected. Please reconnect via the Integrations tab.";
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const drive = await buildDriveClient(authConfig, db, connector.workspaceId);
|
|
178
|
+
|
|
179
|
+
if (action === "list_files") {
|
|
180
|
+
const q = args.query
|
|
181
|
+
? `${args.query} and trashed=false`
|
|
182
|
+
: "trashed=false";
|
|
183
|
+
const res = await drive.files.list({
|
|
184
|
+
q,
|
|
185
|
+
pageSize: 20,
|
|
186
|
+
fields: "files(id, name, mimeType, size, modifiedTime)",
|
|
187
|
+
});
|
|
188
|
+
const files = res.data.files || [];
|
|
189
|
+
if (!files.length) return "No files found.";
|
|
190
|
+
return JSON.stringify(files.map(f => ({
|
|
191
|
+
id: f.id, name: f.name, type: f.mimeType, modified: f.modifiedTime
|
|
192
|
+
})), null, 2);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (action === "read_file") {
|
|
196
|
+
let fileId = args.fileId;
|
|
197
|
+
let mimeType = "";
|
|
198
|
+
|
|
199
|
+
if (!fileId && args.fileName) {
|
|
200
|
+
const res = await drive.files.list({
|
|
201
|
+
q: `name contains '${args.fileName}' and trashed=false`,
|
|
202
|
+
pageSize: 5,
|
|
203
|
+
fields: "files(id, name, mimeType)",
|
|
204
|
+
});
|
|
205
|
+
const files = res.data.files || [];
|
|
206
|
+
if (!files.length) return `No file found matching "${args.fileName}".`;
|
|
207
|
+
fileId = files[0].id;
|
|
208
|
+
mimeType = files[0].mimeType;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!fileId) return "Provide fileId or fileName.";
|
|
212
|
+
|
|
213
|
+
if (!mimeType) {
|
|
214
|
+
const meta = await drive.files.get({ fileId, fields: "mimeType" });
|
|
215
|
+
mimeType = meta.data.mimeType;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let rows = [];
|
|
219
|
+
|
|
220
|
+
const isExcel = mimeType.includes("spreadsheetml") || mimeType.includes("excel") || mimeType.includes("openxmlformats");
|
|
221
|
+
const isGSheet = mimeType.includes("google-apps.spreadsheet");
|
|
222
|
+
|
|
223
|
+
if (isGSheet) {
|
|
224
|
+
// Export Google Sheet as CSV
|
|
225
|
+
const res = await drive.files.export({ fileId, mimeType: "text/csv" }, { responseType: "text" });
|
|
226
|
+
rows = parseCSV(res.data);
|
|
227
|
+
} else if (isExcel) {
|
|
228
|
+
// Download Excel file and parse with xlsx
|
|
229
|
+
const XLSX = require("xlsx");
|
|
230
|
+
const res = await drive.files.get({ fileId, alt: "media" }, { responseType: "arraybuffer" });
|
|
231
|
+
const wb = XLSX.read(res.data, { type: "buffer" });
|
|
232
|
+
const ws = wb.Sheets[wb.SheetNames[0]];
|
|
233
|
+
rows = XLSX.utils.sheet_to_json(ws, { defval: "" });
|
|
234
|
+
} else {
|
|
235
|
+
// Assume CSV
|
|
236
|
+
const res = await drive.files.get({ fileId, alt: "media" }, { responseType: "text" });
|
|
237
|
+
rows = parseCSV(res.data);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const limit = args.limit || 100;
|
|
241
|
+
const offset = args.offset || 0;
|
|
242
|
+
const page = rows.slice(offset, offset + limit);
|
|
243
|
+
|
|
244
|
+
return JSON.stringify({ total: rows.length, offset, returned: page.length, rows: page }, null, 2);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (action === "write_file") {
|
|
248
|
+
let fileId = args.fileId;
|
|
249
|
+
|
|
250
|
+
if (!fileId && args.fileName) {
|
|
251
|
+
const res = await drive.files.list({
|
|
252
|
+
q: `name contains '${args.fileName}' and trashed=false`,
|
|
253
|
+
pageSize: 5,
|
|
254
|
+
fields: "files(id, name, mimeType)",
|
|
255
|
+
});
|
|
256
|
+
const files = res.data.files || [];
|
|
257
|
+
if (!files.length) return `No file found matching "${args.fileName}".`;
|
|
258
|
+
fileId = files[0].id;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!fileId) return "Provide fileId or fileName to write.";
|
|
262
|
+
|
|
263
|
+
const csvContent = rowsToCSV(args.rows || []);
|
|
264
|
+
|
|
265
|
+
await drive.files.update({
|
|
266
|
+
fileId,
|
|
267
|
+
media: { mimeType: "text/csv", body: csvContent },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return `File updated successfully. ${(args.rows || []).length} rows written.`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (action === "update_rows") {
|
|
274
|
+
let fileId = args.fileId;
|
|
275
|
+
let mimeType = "";
|
|
276
|
+
|
|
277
|
+
if (!fileId && args.fileName) {
|
|
278
|
+
const res = await drive.files.list({
|
|
279
|
+
q: `name contains '${args.fileName}' and trashed=false`,
|
|
280
|
+
pageSize: 5, fields: "files(id, name, mimeType)",
|
|
281
|
+
});
|
|
282
|
+
const files = res.data.files || [];
|
|
283
|
+
if (!files.length) return `No file found matching "${args.fileName}".`;
|
|
284
|
+
fileId = files[0].id;
|
|
285
|
+
mimeType = files[0].mimeType;
|
|
286
|
+
}
|
|
287
|
+
if (!fileId) return "Provide fileId or fileName for update_rows.";
|
|
288
|
+
|
|
289
|
+
if (!mimeType) {
|
|
290
|
+
const meta = await drive.files.get({ fileId, fields: "mimeType" });
|
|
291
|
+
mimeType = meta.data.mimeType;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Read full file
|
|
295
|
+
let rows = [];
|
|
296
|
+
const isGSheet = mimeType.includes("google-apps.spreadsheet");
|
|
297
|
+
if (isGSheet) {
|
|
298
|
+
const res = await drive.files.export({ fileId, mimeType: "text/csv" }, { responseType: "text" });
|
|
299
|
+
rows = parseCSV(res.data);
|
|
300
|
+
} else {
|
|
301
|
+
const res = await drive.files.get({ fileId, alt: "media" }, { responseType: "text" });
|
|
302
|
+
rows = parseCSV(res.data);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const matchSet = new Set((args.matchValues || []).map(v => String(v).trim().toLowerCase()));
|
|
306
|
+
let updatedCount = 0;
|
|
307
|
+
for (const row of rows) {
|
|
308
|
+
const cellVal = String(row[args.matchColumn] ?? "").trim().toLowerCase();
|
|
309
|
+
if (matchSet.has(cellVal)) {
|
|
310
|
+
row[args.setColumn] = args.setValue;
|
|
311
|
+
updatedCount++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const csvContent = rowsToCSV(rows);
|
|
316
|
+
await drive.files.update({ fileId, media: { mimeType: "text/csv", body: csvContent } });
|
|
317
|
+
return `Updated ${updatedCount} row(s): set "${args.setColumn}" = "${args.setValue}" where "${args.matchColumn}" matched. Total rows preserved: ${rows.length}.`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return `Unknown Google Drive action: ${action}`;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return `Google Drive error: ${err.message}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { getToolDefinitions, getAnthropicToolDefinitions, executeTool, buildDriveClient };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
|
|
3
|
+
function cfg(connector) {
|
|
4
|
+
return connector.authConfig ? JSON.parse(connector.authConfig) : {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseRepo(repoUrl) {
|
|
8
|
+
if (!repoUrl) return {};
|
|
9
|
+
const m = repoUrl.replace(/\.git$/, "").match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
10
|
+
return m ? { owner: m[1], repo: m[2] } : {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function client({ personalAccessToken }) {
|
|
14
|
+
const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" };
|
|
15
|
+
if (personalAccessToken) headers.Authorization = `Bearer ${personalAccessToken}`;
|
|
16
|
+
return axios.create({ baseURL: "https://api.github.com", headers });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TOOLS = (c, defaultRepo) => [
|
|
20
|
+
{ action: "search_issues", desc: `Search GitHub issues and PRs in ${defaultRepo || c.name}.`,
|
|
21
|
+
params: { query: { type: "string", description: `Search query. Example: repo:${defaultRepo || "owner/repo"} is:open label:bug` } }, required: ["query"] },
|
|
22
|
+
{ action: "get_issue", desc: `Get a GitHub issue by number from ${defaultRepo || c.name}.`,
|
|
23
|
+
params: { owner: { type: "string", description: `Repo owner (default: ${defaultRepo?.split("/")[0] || "owner"})` },
|
|
24
|
+
repo: { type: "string", description: `Repo name (default: ${defaultRepo?.split("/")[1] || "repo"})` },
|
|
25
|
+
number: { type: "number", description: "Issue number." } }, required: ["number"] },
|
|
26
|
+
{ action: "list_files", desc: `List files/directories in ${defaultRepo || c.name} at a given path.`,
|
|
27
|
+
params: { path: { type: "string", description: 'Path inside the repo (default: "" = root).' } }, required: [] },
|
|
28
|
+
{ action: "read_file", desc: `Read the contents of a file from ${defaultRepo || c.name}.`,
|
|
29
|
+
params: { path: { type: "string", description: "File path inside the repo, e.g. src/index.js" } }, required: ["path"] },
|
|
30
|
+
{ action: "create_issue", desc: `Create a GitHub issue in ${defaultRepo || c.name}.`,
|
|
31
|
+
params: { owner: { type: "string", description: "Repo owner." },
|
|
32
|
+
repo: { type: "string", description: "Repo name." },
|
|
33
|
+
title: { type: "string", description: "Issue title." },
|
|
34
|
+
body: { type: "string", description: "Issue body/description." },
|
|
35
|
+
labels: { type: "array", items: { type: "string" }, description: "Labels to apply." } }, required: ["title"] },
|
|
36
|
+
{ action: "write_file", desc: `Create or update a file in ${defaultRepo || c.name}. Automatically handles SHA for updates.`,
|
|
37
|
+
params: { path: { type: "string", description: "File path inside the repo, e.g. blog/my-post.html" },
|
|
38
|
+
content: { type: "string", description: "Full file content as plain text." },
|
|
39
|
+
message: { type: "string", description: "Commit message, e.g. 'feat: publish blog — What is RAG?'" },
|
|
40
|
+
branch: { type: "string", description: "Branch to commit to (default: repo default branch)." } }, required: ["path", "content"] },
|
|
41
|
+
{ action: "delete_file", desc: `Delete a file from ${defaultRepo || c.name}. Automatically fetches the required SHA.`,
|
|
42
|
+
params: { path: { type: "string", description: "File path inside the repo, e.g. blog/old-post.html" },
|
|
43
|
+
message: { type: "string", description: "Commit message, e.g. 'revert: remove blog post — old-post'" },
|
|
44
|
+
branch: { type: "string", description: "Branch to delete from (default: repo default branch)." } }, required: ["path"] },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function getToolDefinitions(connector) {
|
|
48
|
+
const { repoUrl } = cfg(connector);
|
|
49
|
+
const { owner, repo } = parseRepo(repoUrl);
|
|
50
|
+
const defaultRepo = owner && repo ? `${owner}/${repo}` : null;
|
|
51
|
+
return TOOLS(connector, defaultRepo).map(t => ({
|
|
52
|
+
type: "function",
|
|
53
|
+
function: { name: `conn_${connector.id}_${t.action}`, description: t.desc,
|
|
54
|
+
parameters: { type: "object", properties: t.params, required: t.required } },
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAnthropicToolDefinitions(connector) {
|
|
59
|
+
const { repoUrl } = cfg(connector);
|
|
60
|
+
const { owner, repo } = parseRepo(repoUrl);
|
|
61
|
+
const defaultRepo = owner && repo ? `${owner}/${repo}` : null;
|
|
62
|
+
return TOOLS(connector, defaultRepo).map(t => ({
|
|
63
|
+
name: `conn_${connector.id}_${t.action}`, description: t.desc,
|
|
64
|
+
input_schema: { type: "object", properties: t.params, required: t.required },
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function executeTool(action, args, connector) {
|
|
69
|
+
const creds = cfg(connector);
|
|
70
|
+
if (!creds.repoUrl) return "GitHub connector not configured. Please add a repository URL in Integrations.";
|
|
71
|
+
|
|
72
|
+
const { owner: defOwner, repo: defRepo } = parseRepo(creds.repoUrl);
|
|
73
|
+
const api = client(creds);
|
|
74
|
+
|
|
75
|
+
const owner = args.owner || defOwner;
|
|
76
|
+
const repo = args.repo || defRepo;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
if (action === "search_issues") {
|
|
80
|
+
const res = await api.get(`/search/issues?q=${encodeURIComponent(args.query)}&per_page=15`);
|
|
81
|
+
const items = res.data.items || [];
|
|
82
|
+
if (!items.length) return "No issues found.";
|
|
83
|
+
return items.map(i => `[${i.repository_url?.split("/").slice(-2).join("/")}#${i.number}] ${i.title} (${i.state})`).join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (action === "get_issue") {
|
|
87
|
+
const res = await api.get(`/repos/${owner}/${repo}/issues/${args.number}`);
|
|
88
|
+
const i = res.data;
|
|
89
|
+
return JSON.stringify({ number: i.number, title: i.title, state: i.state, author: i.user?.login, labels: i.labels?.map(l => l.name), body: i.body?.slice(0, 1000), created: i.created_at }, null, 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (action === "list_files") {
|
|
93
|
+
const filePath = args.path || "";
|
|
94
|
+
const res = await api.get(`/repos/${owner}/${repo}/contents/${filePath}`);
|
|
95
|
+
const items = Array.isArray(res.data) ? res.data : [res.data];
|
|
96
|
+
return items.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.path}`).join("\n") || "Empty directory.";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (action === "read_file") {
|
|
100
|
+
const res = await api.get(`/repos/${owner}/${repo}/contents/${args.path}`);
|
|
101
|
+
const content = Buffer.from(res.data.content, "base64").toString("utf8");
|
|
102
|
+
return content.slice(0, 8000);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (action === "create_issue") {
|
|
106
|
+
const { title, body = "", labels = [] } = args;
|
|
107
|
+
const res = await api.post(`/repos/${owner}/${repo}/issues`, { title, body, labels });
|
|
108
|
+
return `Created issue #${res.data.number}: ${title}\n${res.data.html_url}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (action === "write_file") {
|
|
112
|
+
const { path, content, message = `Update ${args.path}`, branch } = args;
|
|
113
|
+
const encoded = Buffer.from(content).toString("base64");
|
|
114
|
+
|
|
115
|
+
// Fetch existing SHA if file already exists (required by GitHub API for updates)
|
|
116
|
+
let sha;
|
|
117
|
+
try {
|
|
118
|
+
const url = `/repos/${owner}/${repo}/contents/${path}` + (branch ? `?ref=${branch}` : "");
|
|
119
|
+
const existing = await api.get(url);
|
|
120
|
+
sha = existing.data.sha;
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// File does not exist yet — create mode
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const payload = { message, content: encoded };
|
|
126
|
+
if (sha) payload.sha = sha;
|
|
127
|
+
if (branch) payload.branch = branch;
|
|
128
|
+
|
|
129
|
+
const res = await api.put(`/repos/${owner}/${repo}/contents/${path}`, payload);
|
|
130
|
+
const verb = sha ? "Updated" : "Created";
|
|
131
|
+
return `${verb} ${path}\nCommit: ${res.data.commit.sha}\n${res.data.content.html_url}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (action === "delete_file") {
|
|
135
|
+
const { path, message = `Delete ${args.path}`, branch } = args;
|
|
136
|
+
|
|
137
|
+
// Fetch SHA — required by GitHub API to delete
|
|
138
|
+
let sha;
|
|
139
|
+
try {
|
|
140
|
+
const url = `/repos/${owner}/${repo}/contents/${path}` + (branch ? `?ref=${branch}` : "");
|
|
141
|
+
const existing = await api.get(url);
|
|
142
|
+
sha = existing.data.sha;
|
|
143
|
+
} catch (_) {
|
|
144
|
+
return `File not found: ${path}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const payload = { message, sha };
|
|
148
|
+
if (branch) payload.branch = branch;
|
|
149
|
+
|
|
150
|
+
const res = await api.delete(`/repos/${owner}/${repo}/contents/${path}`, { data: payload });
|
|
151
|
+
return `Deleted ${path}\nCommit: ${res.data.commit.sha}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `Unknown GitHub action: ${action}`;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return `GitHub error: ${err.response?.data?.message || err.message}`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function testConnection(authConfig) {
|
|
161
|
+
try {
|
|
162
|
+
const { owner, repo } = parseRepo(authConfig.repoUrl);
|
|
163
|
+
if (!owner || !repo) return false;
|
|
164
|
+
const res = await client(authConfig).get(`/repos/${owner}/${repo}`);
|
|
165
|
+
return !!res.data.id;
|
|
166
|
+
} catch { return false; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { getToolDefinitions, getAnthropicToolDefinitions, executeTool, testConnection };
|