runline 0.7.7 → 0.8.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/dist/plugins/_shared/imageFile.js +40 -0
- package/dist/plugins/_shared/microsoftAuth.js +100 -0
- package/dist/plugins/gmail/src/index.js +13 -1
- package/dist/plugins/googleAppsScript/src/index.js +203 -0
- package/dist/plugins/googleImage/src/index.js +30 -11
- package/dist/plugins/microsoftCalendar/src/index.js +46 -0
- package/dist/plugins/microsoftFiles/src/index.js +127 -0
- package/dist/plugins/microsoftMail/src/index.js +91 -0
- package/dist/plugins/openai/src/index.js +45 -20
- package/dist/plugins/parallel/src/index.js +100 -0
- package/dist/plugins/recraft/src/index.js +10 -6
- package/dist/plugins/replicate/src/index.js +17 -3
- package/dist/plugins/steel/src/index.js +378 -0
- package/dist/plugins/together/src/index.js +10 -6
- package/dist/plugins/xai/src/index.js +11 -5
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for image-generation plugins: write generated bytes to a
|
|
3
|
+
* file and return its path, instead of returning raw base64.
|
|
4
|
+
*
|
|
5
|
+
* Base64 image payloads in an action result bloat the agent context and are
|
|
6
|
+
* stripped by many hosts before reaching the model, so the agent can never
|
|
7
|
+
* actually deliver the image. A file `path` hands straight to a host
|
|
8
|
+
* send_file/attachment tool. Plugins load in-process via jiti, so node:fs is
|
|
9
|
+
* available (same pattern as googleDrive/googleSlides).
|
|
10
|
+
*/
|
|
11
|
+
import { writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
const MIME_EXT = {
|
|
15
|
+
"image/png": "png",
|
|
16
|
+
"image/jpeg": "jpg",
|
|
17
|
+
"image/jpg": "jpg",
|
|
18
|
+
"image/webp": "webp",
|
|
19
|
+
"image/gif": "gif",
|
|
20
|
+
"image/svg+xml": "svg",
|
|
21
|
+
};
|
|
22
|
+
export function extForMime(mimeType) {
|
|
23
|
+
if (!mimeType)
|
|
24
|
+
return "png";
|
|
25
|
+
return MIME_EXT[mimeType] ?? mimeType.split("/")[1]?.split("+")[0] ?? "png";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decode base64 image bytes and write them to `saveDir` (or the OS temp dir),
|
|
29
|
+
* named `<provider>-<stamp>-<index>.<ext>`. Returns the file path + size.
|
|
30
|
+
*/
|
|
31
|
+
export function writeImageFile(opts) {
|
|
32
|
+
const mimeType = opts.mimeType ?? "image/png";
|
|
33
|
+
const dir = (typeof opts.saveDir === "string" && opts.saveDir.trim()) || tmpdir();
|
|
34
|
+
const bytes = Buffer.from(opts.base64 ?? "", "base64");
|
|
35
|
+
const stamp = opts.stamp ?? Date.now();
|
|
36
|
+
const path = join(dir, `${opts.provider}-${stamp}-${opts.index}.${extForMime(mimeType)}`);
|
|
37
|
+
writeFileSync(path, bytes);
|
|
38
|
+
return { path, mimeType, byteLength: bytes.length };
|
|
39
|
+
}
|
|
40
|
+
export const SEND_FILE_NOTE = "Image(s) written to disk. Deliver each to the user with send_file using its `path`.";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
2
|
+
function authority(cfg) {
|
|
3
|
+
return `https://login.microsoftonline.com/${cfg.tenantId || "common"}/oauth2/v2.0/token`;
|
|
4
|
+
}
|
|
5
|
+
export function isAppOnly(cfg) {
|
|
6
|
+
return !cfg.refreshToken && !!(cfg.tenantId && cfg.clientId && cfg.clientSecret);
|
|
7
|
+
}
|
|
8
|
+
/** Graph path prefix for the acting principal: /me (delegated) or /users/{upn} (app-only). */
|
|
9
|
+
export function userBase(ctx) {
|
|
10
|
+
const cfg = ctx.connection.config;
|
|
11
|
+
if (cfg.refreshToken)
|
|
12
|
+
return "/me";
|
|
13
|
+
if (cfg.userUpn)
|
|
14
|
+
return `/users/${encodeURIComponent(cfg.userUpn)}`;
|
|
15
|
+
throw new Error("microsoft: app-only mode requires userUpn (target mailbox/drive). Set MS_GRAPH_USER_UPN, or connect via OAuth.");
|
|
16
|
+
}
|
|
17
|
+
export async function microsoftAccessToken(ctx, pluginName, scopes) {
|
|
18
|
+
const cfg = ctx.connection.config;
|
|
19
|
+
if (cfg.accessToken &&
|
|
20
|
+
typeof cfg.accessTokenExpiresAt === "number" &&
|
|
21
|
+
Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
|
|
22
|
+
return cfg.accessToken;
|
|
23
|
+
}
|
|
24
|
+
let body;
|
|
25
|
+
if (cfg.refreshToken) {
|
|
26
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
27
|
+
throw new Error(`${pluginName}: missing clientId/clientSecret for OAuth refresh.`);
|
|
28
|
+
}
|
|
29
|
+
body = new URLSearchParams({
|
|
30
|
+
client_id: cfg.clientId,
|
|
31
|
+
client_secret: cfg.clientSecret,
|
|
32
|
+
refresh_token: cfg.refreshToken,
|
|
33
|
+
grant_type: "refresh_token",
|
|
34
|
+
scope: [...scopes, "offline_access"].join(" "),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (isAppOnly(cfg)) {
|
|
38
|
+
body = new URLSearchParams({
|
|
39
|
+
client_id: cfg.clientId,
|
|
40
|
+
client_secret: cfg.clientSecret,
|
|
41
|
+
grant_type: "client_credentials",
|
|
42
|
+
scope: "https://graph.microsoft.com/.default",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new Error(`${pluginName}: no credentials. Connect via OAuth, or set tenantId/clientId/clientSecret (app-only).`);
|
|
47
|
+
}
|
|
48
|
+
const res = await fetch(authority(cfg), {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
51
|
+
body: body.toString(),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`${pluginName}: token request failed (${res.status}): ${await res.text()}`);
|
|
55
|
+
}
|
|
56
|
+
const data = (await res.json());
|
|
57
|
+
const patch = {
|
|
58
|
+
accessToken: data.access_token,
|
|
59
|
+
accessTokenExpiresAt: Date.now() + data.expires_in * 1000,
|
|
60
|
+
};
|
|
61
|
+
// Microsoft rotates refresh tokens — persist the new one when present.
|
|
62
|
+
if (data.refresh_token)
|
|
63
|
+
patch.refreshToken = data.refresh_token;
|
|
64
|
+
await ctx.updateConnection(patch);
|
|
65
|
+
return data.access_token;
|
|
66
|
+
}
|
|
67
|
+
/** Authenticated Graph v1.0 request. Returns parsed JSON ({success:true} for 204). */
|
|
68
|
+
export async function graphRequest(ctx, pluginName, scopes, method, path, body) {
|
|
69
|
+
const token = await microsoftAccessToken(ctx, pluginName, scopes);
|
|
70
|
+
const init = {
|
|
71
|
+
method,
|
|
72
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
73
|
+
};
|
|
74
|
+
if (body !== undefined) {
|
|
75
|
+
init.headers["Content-Type"] = "application/json";
|
|
76
|
+
init.body = JSON.stringify(body);
|
|
77
|
+
}
|
|
78
|
+
const res = await fetch(`https://graph.microsoft.com/v1.0${path}`, init);
|
|
79
|
+
if (res.status === 204)
|
|
80
|
+
return { success: true };
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw new Error(`${pluginName}: ${method} ${path} → ${res.status} ${text}`);
|
|
84
|
+
return text ? JSON.parse(text) : { success: true };
|
|
85
|
+
}
|
|
86
|
+
/** Setup help shown by the OAuth wizard for all Microsoft plugins. */
|
|
87
|
+
export function microsoftSetupHelp(apiName) {
|
|
88
|
+
return [
|
|
89
|
+
`You need a Microsoft Entra (Azure AD) app registration. One-time, ~5 minutes.`,
|
|
90
|
+
"",
|
|
91
|
+
"1. Register an app: https://entra.microsoft.com → App registrations → New registration.",
|
|
92
|
+
" Supported account types: your org (single tenant) is fine.",
|
|
93
|
+
"2. Add a Web redirect URI (Authentication → Add platform → Web):",
|
|
94
|
+
" {{redirectUri}}",
|
|
95
|
+
"3. Certificates & secrets → New client secret → copy the VALUE (not the Secret ID).",
|
|
96
|
+
`4. API permissions → Add → Microsoft Graph → Delegated → add the ${apiName} scopes,`,
|
|
97
|
+
" then 'Grant admin consent'.",
|
|
98
|
+
"5. Paste the Application (client) ID and the client secret VALUE below.",
|
|
99
|
+
];
|
|
100
|
+
}
|
|
@@ -108,6 +108,18 @@ function header(name, value) {
|
|
|
108
108
|
function foldedBase64ByteLength(length) {
|
|
109
109
|
return length === 0 ? 0 : length + Math.floor((length - 1) / 76) * CRLF.length;
|
|
110
110
|
}
|
|
111
|
+
function normalizeMimeBase64(value, index) {
|
|
112
|
+
const compact = value.replace(/\s+/g, "");
|
|
113
|
+
if (!/^[A-Za-z0-9+/=_-]*$/.test(compact)) {
|
|
114
|
+
throw new Error(`gmail: attachment ${index} contentBase64 contains invalid base64 characters`);
|
|
115
|
+
}
|
|
116
|
+
const standard = compact.replace(/-/g, "+").replace(/_/g, "/");
|
|
117
|
+
const remainder = standard.length % 4;
|
|
118
|
+
if (remainder === 1) {
|
|
119
|
+
throw new Error(`gmail: attachment ${index} contentBase64 has invalid base64 length`);
|
|
120
|
+
}
|
|
121
|
+
return remainder === 0 ? standard : `${standard}${"=".repeat(4 - remainder)}`;
|
|
122
|
+
}
|
|
111
123
|
function foldBase64(encoded) {
|
|
112
124
|
let folded = "";
|
|
113
125
|
for (let i = 0; i < encoded.length; i += 76) {
|
|
@@ -147,7 +159,7 @@ function normalizeAttachment(input, index) {
|
|
|
147
159
|
return {
|
|
148
160
|
name: input.name ?? input.filename ?? `attachment-${index + 1}`,
|
|
149
161
|
mimeType: input.mimeType ?? "application/octet-stream",
|
|
150
|
-
contentBase64,
|
|
162
|
+
contentBase64: normalizeMimeBase64(contentBase64, index),
|
|
151
163
|
};
|
|
152
164
|
}
|
|
153
165
|
function attachmentPart(att) {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { googleAccessToken } from "../../_shared/googleAuth.js";
|
|
2
|
+
const SCRIPT_API = "https://script.googleapis.com/v1";
|
|
3
|
+
const DRIVE_API = "https://www.googleapis.com/drive/v3";
|
|
4
|
+
const SCOPES = [
|
|
5
|
+
"https://www.googleapis.com/auth/script.projects",
|
|
6
|
+
"https://www.googleapis.com/auth/script.deployments",
|
|
7
|
+
"https://www.googleapis.com/auth/script.processes",
|
|
8
|
+
"https://www.googleapis.com/auth/drive.metadata.readonly",
|
|
9
|
+
];
|
|
10
|
+
function accessToken(ctx) {
|
|
11
|
+
return googleAccessToken(ctx, "googleAppsScript", SCOPES);
|
|
12
|
+
}
|
|
13
|
+
async function call(ctx, method, url, payload) {
|
|
14
|
+
const headers = { Authorization: `Bearer ${await accessToken(ctx)}` };
|
|
15
|
+
if (payload !== undefined)
|
|
16
|
+
headers["Content-Type"] = "application/json";
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
method,
|
|
19
|
+
headers,
|
|
20
|
+
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
|
21
|
+
});
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
let json;
|
|
24
|
+
try {
|
|
25
|
+
json = text ? JSON.parse(text) : {};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
json = { raw: text };
|
|
29
|
+
}
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const msg = json?.error?.message || json?.error_description || text.slice(0, 300) || res.status;
|
|
32
|
+
throw new Error(`googleAppsScript: ${method} ${res.status}: ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
return json;
|
|
35
|
+
}
|
|
36
|
+
export default function googleAppsScript(rl) {
|
|
37
|
+
rl.setName("googleAppsScript");
|
|
38
|
+
rl.setVersion("1.0.0");
|
|
39
|
+
rl.setConnectionSchema({
|
|
40
|
+
clientId: { type: "string", required: false, description: "OAuth client ID", env: "GOOGLE_APPS_SCRIPT_CLIENT_ID" },
|
|
41
|
+
clientSecret: { type: "string", required: false, description: "OAuth client secret", env: "GOOGLE_APPS_SCRIPT_CLIENT_SECRET" },
|
|
42
|
+
refreshToken: { type: "string", required: false, description: "OAuth refresh token", env: "GOOGLE_APPS_SCRIPT_REFRESH_TOKEN" },
|
|
43
|
+
serviceAccountJson: { type: "string", required: false, description: "Service-account key JSON", env: "GOOGLE_APPS_SCRIPT_SERVICE_ACCOUNT_JSON" },
|
|
44
|
+
serviceAccountEmail: { type: "string", required: false, description: "Service-account email", env: "GOOGLE_APPS_SCRIPT_SERVICE_ACCOUNT_EMAIL" },
|
|
45
|
+
serviceAccountPrivateKey: { type: "string", required: false, description: "Service-account private key", env: "GOOGLE_APPS_SCRIPT_SERVICE_ACCOUNT_PRIVATE_KEY" },
|
|
46
|
+
serviceAccountSubject: { type: "string", required: false, description: "User to impersonate (domain-wide delegation)", env: "GOOGLE_APPS_SCRIPT_SERVICE_ACCOUNT_SUBJECT" },
|
|
47
|
+
});
|
|
48
|
+
rl.registerAction("script.list", {
|
|
49
|
+
description: "List Apps Script projects in Drive (standalone scripts; bound scripts live inside their container and don't appear here).",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
query: { type: "string", required: false, description: "Case-insensitive name substring filter." },
|
|
52
|
+
pageSize: { type: "number", required: false, description: "Max results (default 50)." },
|
|
53
|
+
},
|
|
54
|
+
async execute(input, ctx) {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
q: "mimeType='application/vnd.google-apps.script' and trashed=false",
|
|
57
|
+
fields: "files(id,name,modifiedTime)",
|
|
58
|
+
pageSize: String(input.pageSize ?? 50),
|
|
59
|
+
orderBy: "modifiedTime desc",
|
|
60
|
+
});
|
|
61
|
+
const res = await call(ctx, "GET", `${DRIVE_API}/files?${params}`);
|
|
62
|
+
let files = res.files ?? [];
|
|
63
|
+
if (input.query) {
|
|
64
|
+
const q = String(input.query).toLowerCase();
|
|
65
|
+
files = files.filter((f) => (f.name || "").toLowerCase().includes(q));
|
|
66
|
+
}
|
|
67
|
+
return { count: files.length, scripts: files };
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
rl.registerAction("project.getContent", {
|
|
71
|
+
description: "Get all files of an Apps Script project (name, type, source).",
|
|
72
|
+
inputSchema: { scriptId: { type: "string", required: true } },
|
|
73
|
+
async execute(input, ctx) {
|
|
74
|
+
const res = await call(ctx, "GET", `${SCRIPT_API}/projects/${input.scriptId}/content`);
|
|
75
|
+
return { scriptId: input.scriptId, files: res.files ?? [] };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
rl.registerAction("project.readFile", {
|
|
79
|
+
description: "Read one file's source from a project.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
scriptId: { type: "string", required: true },
|
|
82
|
+
name: { type: "string", required: true, description: "File name without extension (e.g. 'Code', 'appsscript')." },
|
|
83
|
+
},
|
|
84
|
+
async execute(input, ctx) {
|
|
85
|
+
const res = await call(ctx, "GET", `${SCRIPT_API}/projects/${input.scriptId}/content`);
|
|
86
|
+
const file = (res.files ?? []).find((f) => f.name === input.name);
|
|
87
|
+
if (!file)
|
|
88
|
+
throw new Error(`No file "${input.name}". Available: ${(res.files ?? []).map((f) => f.name).join(", ")}`);
|
|
89
|
+
return { name: file.name, type: file.type, source: file.source };
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
rl.registerAction("file.edit", {
|
|
93
|
+
description: "Replace (or add) a single file's source, leaving other files untouched. Read-modify-write — the safe way to change code.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
scriptId: { type: "string", required: true },
|
|
96
|
+
name: { type: "string", required: true },
|
|
97
|
+
source: { type: "string", required: true },
|
|
98
|
+
type: { type: "string", required: false, description: "SERVER_JS (default), HTML, or JSON (for appsscript)." },
|
|
99
|
+
},
|
|
100
|
+
async execute(input, ctx) {
|
|
101
|
+
const cur = await call(ctx, "GET", `${SCRIPT_API}/projects/${input.scriptId}/content`);
|
|
102
|
+
const files = cur.files ?? [];
|
|
103
|
+
const idx = files.findIndex((f) => f.name === input.name);
|
|
104
|
+
const type = input.type || (input.name === "appsscript" ? "JSON" : files[idx]?.type || "SERVER_JS");
|
|
105
|
+
const entry = { name: input.name, type, source: input.source };
|
|
106
|
+
if (idx >= 0)
|
|
107
|
+
files[idx] = entry;
|
|
108
|
+
else
|
|
109
|
+
files.push(entry);
|
|
110
|
+
await call(ctx, "PUT", `${SCRIPT_API}/projects/${input.scriptId}/content`, { files });
|
|
111
|
+
return { scriptId: input.scriptId, updated: input.name, fileCount: files.length };
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
rl.registerAction("project.updateContent", {
|
|
115
|
+
description: "Replace the entire project file set. files = [{name, type, source}], must include the appsscript JSON manifest. Prefer file.edit for single changes.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
scriptId: { type: "string", required: true },
|
|
118
|
+
files: { type: "array", required: true, description: "[{name, type: SERVER_JS|HTML|JSON, source}]" },
|
|
119
|
+
},
|
|
120
|
+
async execute(input, ctx) {
|
|
121
|
+
const res = await call(ctx, "PUT", `${SCRIPT_API}/projects/${input.scriptId}/content`, { files: input.files });
|
|
122
|
+
return { scriptId: input.scriptId, fileCount: (res.files ?? []).length };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
rl.registerAction("project.create", {
|
|
126
|
+
description: "Create a new Apps Script project. Pass parentId (a Drive file id, e.g. a Sheet) to bind it to that container.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
title: { type: "string", required: true },
|
|
129
|
+
parentId: { type: "string", required: false, description: "Container Drive file id for a bound script." },
|
|
130
|
+
},
|
|
131
|
+
async execute(input, ctx) {
|
|
132
|
+
const payload = { title: input.title };
|
|
133
|
+
if (input.parentId)
|
|
134
|
+
payload.parentId = input.parentId;
|
|
135
|
+
const res = await call(ctx, "POST", `${SCRIPT_API}/projects`, payload);
|
|
136
|
+
return { scriptId: res.scriptId, title: res.title, parentId: res.parentId };
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
rl.registerAction("version.create", {
|
|
140
|
+
description: "Create an immutable version of the project (needed before deploying).",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
scriptId: { type: "string", required: true },
|
|
143
|
+
description: { type: "string", required: false },
|
|
144
|
+
},
|
|
145
|
+
async execute(input, ctx) {
|
|
146
|
+
const res = await call(ctx, "POST", `${SCRIPT_API}/projects/${input.scriptId}/versions`, { description: input.description || "" });
|
|
147
|
+
return { scriptId: input.scriptId, versionNumber: res.versionNumber, description: res.description };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
rl.registerAction("deployment.create", {
|
|
151
|
+
description: "Deploy a version. For function.run, deploy with an API-executable manifest (executionApi access).",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
scriptId: { type: "string", required: true },
|
|
154
|
+
versionNumber: { type: "number", required: true },
|
|
155
|
+
description: { type: "string", required: false },
|
|
156
|
+
manifestFileName: { type: "string", required: false, description: "Defaults to 'appsscript'." },
|
|
157
|
+
},
|
|
158
|
+
async execute(input, ctx) {
|
|
159
|
+
const res = await call(ctx, "POST", `${SCRIPT_API}/projects/${input.scriptId}/deployments`, {
|
|
160
|
+
versionNumber: input.versionNumber,
|
|
161
|
+
manifestFileName: input.manifestFileName || "appsscript",
|
|
162
|
+
description: input.description || "",
|
|
163
|
+
});
|
|
164
|
+
return { deploymentId: res.deploymentId, entryPoints: res.entryPoints };
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
rl.registerAction("function.run", {
|
|
168
|
+
description: "Run a function via scripts.run. Requires the project linked to a standard GCP project, the Apps Script API enabled, and an API-executable deployment (or devMode for the owner).",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
scriptId: { type: "string", required: true },
|
|
171
|
+
functionName: { type: "string", required: true },
|
|
172
|
+
parameters: { type: "array", required: false, description: "Positional args for the function." },
|
|
173
|
+
devMode: { type: "boolean", required: false, description: "Run latest saved code (owner only). Default true." },
|
|
174
|
+
},
|
|
175
|
+
async execute(input, ctx) {
|
|
176
|
+
const res = await call(ctx, "POST", `${SCRIPT_API}/scripts/${input.scriptId}:run`, {
|
|
177
|
+
function: input.functionName,
|
|
178
|
+
parameters: input.parameters ?? [],
|
|
179
|
+
devMode: input.devMode === undefined ? true : input.devMode,
|
|
180
|
+
});
|
|
181
|
+
if (res.error) {
|
|
182
|
+
const d = res.error.details?.[0];
|
|
183
|
+
throw new Error(`Function error: ${d?.errorMessage || res.error.message}`);
|
|
184
|
+
}
|
|
185
|
+
return { done: res.done, result: res.response?.result ?? null };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
rl.registerAction("process.list", {
|
|
189
|
+
description: "Recent executions for a project (status, function, times) — a log view.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
scriptId: { type: "string", required: true },
|
|
192
|
+
pageSize: { type: "number", required: false, description: "Default 20." },
|
|
193
|
+
},
|
|
194
|
+
async execute(input, ctx) {
|
|
195
|
+
const params = new URLSearchParams({
|
|
196
|
+
"userProcessFilter.scriptId": input.scriptId,
|
|
197
|
+
pageSize: String(input.pageSize ?? 20),
|
|
198
|
+
});
|
|
199
|
+
const res = await call(ctx, "GET", `${SCRIPT_API}/processes?${params}`);
|
|
200
|
+
return { count: (res.processes ?? []).length, processes: res.processes ?? [] };
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
* API key. Kept under the `googleImage` namespace so it doesn't
|
|
7
7
|
* collide with `googleDrive`, `googleDocs`, etc.
|
|
8
8
|
*
|
|
9
|
-
* await googleImage.image.create({ prompt: "a watercolor fox" })
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* const { images } = await googleImage.image.create({ prompt: "a watercolor fox" })
|
|
10
|
+
* // images[0].path -> "/tmp/googleImage-….png"
|
|
11
|
+
*
|
|
12
|
+
* Generated images are written to disk and the action returns their file
|
|
13
|
+
* `path`s — never raw base64, which bloats the agent context and is
|
|
14
|
+
* stripped before delivery. Hand each `path` to the host's file-sending
|
|
15
|
+
* tool (e.g. send_file) to deliver the image.
|
|
14
16
|
*
|
|
15
17
|
* Nano Banana supports conversational editing — chain prompts in
|
|
16
18
|
* follow-up calls and it'll keep iterating on the last image.
|
|
17
19
|
*/
|
|
20
|
+
import { writeFileSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
18
23
|
const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
19
24
|
export default function googleImage(rl) {
|
|
20
25
|
rl.setName("googleImage");
|
|
@@ -28,7 +33,7 @@ export default function googleImage(rl) {
|
|
|
28
33
|
},
|
|
29
34
|
});
|
|
30
35
|
rl.registerAction("image.create", {
|
|
31
|
-
description: "Generate an image with Google's Gemini image models (Nano Banana / Imagen).
|
|
36
|
+
description: "Generate an image with Google's Gemini image models (Nano Banana / Imagen). Writes the image(s) to disk and returns their file `path`s — not base64. Deliver each image to the user with send_file using its `path`.",
|
|
32
37
|
inputSchema: {
|
|
33
38
|
prompt: {
|
|
34
39
|
type: "string",
|
|
@@ -40,6 +45,11 @@ export default function googleImage(rl) {
|
|
|
40
45
|
required: false,
|
|
41
46
|
description: "gemini-2.5-flash-image (Nano Banana, default) | gemini-3-pro-image-preview | gemini-3.1-flash-image-preview",
|
|
42
47
|
},
|
|
48
|
+
saveDir: {
|
|
49
|
+
type: "string",
|
|
50
|
+
required: false,
|
|
51
|
+
description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
|
|
52
|
+
},
|
|
43
53
|
},
|
|
44
54
|
async execute(input, ctx) {
|
|
45
55
|
const p = (input ?? {});
|
|
@@ -62,18 +72,27 @@ export default function googleImage(rl) {
|
|
|
62
72
|
throw new Error(`Google API error ${res.status}: ${await res.text()}`);
|
|
63
73
|
}
|
|
64
74
|
const data = (await res.json());
|
|
75
|
+
const dir = (typeof p.saveDir === "string" && p.saveDir.trim()) || tmpdir();
|
|
76
|
+
const stamp = Date.now();
|
|
65
77
|
const images = [];
|
|
66
78
|
for (const candidate of data.candidates ?? []) {
|
|
67
79
|
for (const part of candidate.content?.parts ?? []) {
|
|
68
80
|
if (part.inlineData?.data) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
81
|
+
const mimeType = part.inlineData.mimeType ?? "image/png";
|
|
82
|
+
const ext = mimeType.includes("jpeg") ? "jpg" : mimeType.split("/")[1] || "png";
|
|
83
|
+
const bytes = Buffer.from(part.inlineData.data, "base64");
|
|
84
|
+
const path = join(dir, `googleImage-${stamp}-${images.length}.${ext}`);
|
|
85
|
+
writeFileSync(path, bytes);
|
|
86
|
+
images.push({ path, mimeType, byteLength: bytes.length });
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
provider: "googleImage",
|
|
92
|
+
model,
|
|
93
|
+
images,
|
|
94
|
+
note: "Image(s) written to disk. Deliver each to the user with send_file using its `path`.",
|
|
95
|
+
};
|
|
77
96
|
},
|
|
78
97
|
});
|
|
79
98
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { graphRequest, microsoftSetupHelp, userBase } from "../../_shared/microsoftAuth.js";
|
|
2
|
+
const NAME = "microsoftCalendar";
|
|
3
|
+
const SCOPES = ["https://graph.microsoft.com/Calendars.Read"];
|
|
4
|
+
export default function microsoftCalendar(rl) {
|
|
5
|
+
rl.setName(NAME);
|
|
6
|
+
rl.setVersion("1.0.0");
|
|
7
|
+
rl.setConnectionSchema({
|
|
8
|
+
tenantId: { type: "string", required: false, env: "MS_GRAPH_TENANT_ID", description: "Entra tenant id (app-only) or omit for OAuth /common" },
|
|
9
|
+
clientId: { type: "string", required: false, env: "MS_GRAPH_CLIENT_ID", description: "App (client) id" },
|
|
10
|
+
clientSecret: { type: "string", required: false, env: "MS_GRAPH_CLIENT_SECRET", description: "Client secret VALUE" },
|
|
11
|
+
refreshToken: { type: "string", required: false, env: "MICROSOFTCALENDAR_REFRESH_TOKEN", description: "OAuth2 refresh token (set by the login flow)" },
|
|
12
|
+
userUpn: { type: "string", required: false, env: "MS_GRAPH_USER_UPN", description: "App-only only: target user UPN" },
|
|
13
|
+
});
|
|
14
|
+
rl.setOAuth({
|
|
15
|
+
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
16
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
17
|
+
scopes: [...SCOPES, "offline_access"],
|
|
18
|
+
setupHelp: microsoftSetupHelp("Calendars.Read"),
|
|
19
|
+
});
|
|
20
|
+
rl.registerAction("calendar.list", {
|
|
21
|
+
description: "List calendar events in a date range. Returns [{id,subject,start,end,location,organizer,attendees}].",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
start: { type: "string", required: true, description: "ISO start datetime, e.g. 2026-05-01T00:00:00Z" },
|
|
24
|
+
end: { type: "string", required: true, description: "ISO end datetime" },
|
|
25
|
+
top: { type: "number", required: false, default: 50 },
|
|
26
|
+
},
|
|
27
|
+
async execute(input, ctx) {
|
|
28
|
+
const qs = new URLSearchParams({
|
|
29
|
+
startDateTime: input.start,
|
|
30
|
+
endDateTime: input.end,
|
|
31
|
+
$top: String(input.top ?? 50),
|
|
32
|
+
$select: "id,subject,start,end,location,organizer,attendees,isAllDay,webLink",
|
|
33
|
+
$orderby: "start/dateTime",
|
|
34
|
+
});
|
|
35
|
+
const r = await graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/calendarView?${qs}`);
|
|
36
|
+
return r.value;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
rl.registerAction("event.get", {
|
|
40
|
+
description: "Get one calendar event by id (full details incl. body).",
|
|
41
|
+
inputSchema: { id: { type: "string", required: true } },
|
|
42
|
+
async execute(input, ctx) {
|
|
43
|
+
return graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/events/${input.id}`);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { graphRequest, microsoftAccessToken, microsoftSetupHelp, userBase, } from "../../_shared/microsoftAuth.js";
|
|
2
|
+
const NAME = "microsoftFiles";
|
|
3
|
+
const SCOPES = [
|
|
4
|
+
"https://graph.microsoft.com/Files.ReadWrite.All",
|
|
5
|
+
"https://graph.microsoft.com/Sites.ReadWrite.All",
|
|
6
|
+
];
|
|
7
|
+
/** Resolve the drive root path: explicit drive/site, else the user's default drive. */
|
|
8
|
+
function driveBase(ctx) {
|
|
9
|
+
const cfg = ctx.connection.config;
|
|
10
|
+
if (cfg.driveId)
|
|
11
|
+
return `/drives/${cfg.driveId}`;
|
|
12
|
+
if (cfg.siteId)
|
|
13
|
+
return `/sites/${cfg.siteId}/drive`;
|
|
14
|
+
return `${userBase(ctx)}/drive`;
|
|
15
|
+
}
|
|
16
|
+
async function binaryFetch(ctx, method, path, body) {
|
|
17
|
+
const token = await microsoftAccessToken(ctx, NAME, SCOPES);
|
|
18
|
+
const init = {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bearer ${token}`,
|
|
22
|
+
...(body ? { "Content-Type": "application/octet-stream" } : {}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
if (body)
|
|
26
|
+
init.body = body;
|
|
27
|
+
return fetch(`https://graph.microsoft.com/v1.0${path}`, init);
|
|
28
|
+
}
|
|
29
|
+
export default function microsoftFiles(rl) {
|
|
30
|
+
rl.setName(NAME);
|
|
31
|
+
rl.setVersion("1.0.0");
|
|
32
|
+
rl.setConnectionSchema({
|
|
33
|
+
tenantId: { type: "string", required: false, env: "MS_GRAPH_TENANT_ID", description: "Entra tenant id (app-only) or omit for OAuth /common" },
|
|
34
|
+
clientId: { type: "string", required: false, env: "MS_GRAPH_CLIENT_ID", description: "App (client) id" },
|
|
35
|
+
clientSecret: { type: "string", required: false, env: "MS_GRAPH_CLIENT_SECRET", description: "Client secret VALUE" },
|
|
36
|
+
refreshToken: { type: "string", required: false, env: "MICROSOFTFILES_REFRESH_TOKEN", description: "OAuth2 refresh token (set by the login flow)" },
|
|
37
|
+
userUpn: { type: "string", required: false, env: "MS_GRAPH_USER_UPN", description: "App-only only: target user UPN (their OneDrive)" },
|
|
38
|
+
siteId: { type: "string", required: false, env: "MS_SHAREPOINT_SITE_ID", description: "Optional SharePoint site id (use its default drive)" },
|
|
39
|
+
driveId: { type: "string", required: false, env: "MS_GRAPH_DRIVE_ID", description: "Optional explicit drive id (overrides site/user)" },
|
|
40
|
+
});
|
|
41
|
+
rl.setOAuth({
|
|
42
|
+
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
43
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
44
|
+
scopes: [...SCOPES, "offline_access"],
|
|
45
|
+
setupHelp: microsoftSetupHelp("Files.ReadWrite.All, Sites.ReadWrite.All"),
|
|
46
|
+
});
|
|
47
|
+
rl.registerAction("files.search", {
|
|
48
|
+
description: "Search the drive for files/folders by text. Returns [{id,name,size,lastModifiedDateTime,webUrl,folder?}].",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
query: { type: "string", required: true },
|
|
51
|
+
top: { type: "number", required: false, default: 25 },
|
|
52
|
+
},
|
|
53
|
+
async execute(input, ctx) {
|
|
54
|
+
const q = encodeURIComponent(String(input.query).replace(/'/g, "''"));
|
|
55
|
+
const qs = new URLSearchParams({
|
|
56
|
+
$top: String(input.top ?? 25),
|
|
57
|
+
$select: "id,name,size,lastModifiedDateTime,webUrl,folder,file",
|
|
58
|
+
});
|
|
59
|
+
const r = await graphRequest(ctx, NAME, SCOPES, "GET", `${driveBase(ctx)}/root/search(q='${q}')?${qs}`);
|
|
60
|
+
return r.value;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
rl.registerAction("files.list", {
|
|
64
|
+
description: "List children of a folder (default the drive root, or pass folderId).",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
folderId: { type: "string", required: false, description: "Folder item id; omit for root" },
|
|
67
|
+
top: { type: "number", required: false, default: 100 },
|
|
68
|
+
},
|
|
69
|
+
async execute(input, ctx) {
|
|
70
|
+
const where = input.folderId ? `/items/${input.folderId}/children` : "/root/children";
|
|
71
|
+
const qs = new URLSearchParams({
|
|
72
|
+
$top: String(input.top ?? 100),
|
|
73
|
+
$select: "id,name,size,lastModifiedDateTime,webUrl,folder,file",
|
|
74
|
+
});
|
|
75
|
+
const r = await graphRequest(ctx, NAME, SCOPES, "GET", `${driveBase(ctx)}${where}?${qs}`);
|
|
76
|
+
return r.value;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
rl.registerAction("files.get", {
|
|
80
|
+
description: "Download a file by id. Returns {id,name,size,contentType,base64}. base64 is the file bytes.",
|
|
81
|
+
inputSchema: { id: { type: "string", required: true } },
|
|
82
|
+
async execute(input, ctx) {
|
|
83
|
+
const meta = await graphRequest(ctx, NAME, SCOPES, "GET", `${driveBase(ctx)}/items/${input.id}`);
|
|
84
|
+
const res = await binaryFetch(ctx, "GET", `${driveBase(ctx)}/items/${input.id}/content`);
|
|
85
|
+
if (!res.ok)
|
|
86
|
+
throw new Error(`${NAME}: download ${input.id} → ${res.status} ${await res.text()}`);
|
|
87
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
88
|
+
return {
|
|
89
|
+
id: meta.id,
|
|
90
|
+
name: meta.name,
|
|
91
|
+
size: meta.size,
|
|
92
|
+
contentType: meta.file?.mimeType,
|
|
93
|
+
base64: buf.toString("base64"),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
rl.registerAction("files.upload", {
|
|
98
|
+
description: "Upload a file (base64) to a path in the drive, e.g. a dedicated output folder. Returns the created item. For files up to ~4MB.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
path: { type: "string", required: true, description: "Drive-relative path incl. filename, e.g. 'Agent Output/report.docx'" },
|
|
101
|
+
base64: { type: "string", required: true, description: "File content, base64-encoded" },
|
|
102
|
+
},
|
|
103
|
+
async execute(input, ctx) {
|
|
104
|
+
const bytes = Buffer.from(input.base64, "base64");
|
|
105
|
+
const p = input.path.split("/").map(encodeURIComponent).join("/");
|
|
106
|
+
const res = await binaryFetch(ctx, "PUT", `${driveBase(ctx)}/root:/${p}:/content`, bytes);
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
throw new Error(`${NAME}: upload ${input.path} → ${res.status} ${await res.text()}`);
|
|
109
|
+
return JSON.parse(await res.text());
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
rl.registerAction("folder.create", {
|
|
113
|
+
description: "Create a folder (e.g. a dedicated agent output folder) under the drive root or a parent.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
name: { type: "string", required: true },
|
|
116
|
+
parentId: { type: "string", required: false, description: "Parent folder id; omit for root" },
|
|
117
|
+
},
|
|
118
|
+
async execute(input, ctx) {
|
|
119
|
+
const where = input.parentId ? `/items/${input.parentId}/children` : "/root/children";
|
|
120
|
+
return graphRequest(ctx, NAME, SCOPES, "POST", `${driveBase(ctx)}${where}`, {
|
|
121
|
+
name: input.name,
|
|
122
|
+
folder: {},
|
|
123
|
+
"@microsoft.graph.conflictBehavior": "rename",
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|