runline 0.7.6 → 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/googleCalendar/src/index.js +167 -0
- package/dist/plugins/googleDocs/src/index.js +420 -0
- package/dist/plugins/googleDrive/src/index.js +669 -0
- package/dist/plugins/googleImage/src/index.js +30 -11
- package/dist/plugins/googleSheets/src/index.js +275 -0
- 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
|
+
}
|
|
@@ -780,4 +780,171 @@ export default function googleCalendar(rl) {
|
|
|
780
780
|
return calRequest(ctx, "POST", `/calendars/${encodeCalendarId(p.calendarId)}/events/${p.eventId}/move`, undefined, qs);
|
|
781
781
|
},
|
|
782
782
|
});
|
|
783
|
+
// ─── FreeBusy ────────────────────────────────────────────────────
|
|
784
|
+
rl.registerAction("freeBusy.query", {
|
|
785
|
+
description: "Query free/busy windows across one or more calendars in a time range. Returns an array of busy intervals per calendar id, plus any errors.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
calendarIds: { type: "array", required: true, description: "Calendar ids to query (use 'primary' for the user's own)." },
|
|
788
|
+
timeMin: { type: "string", required: true, description: "RFC 3339 lower bound." },
|
|
789
|
+
timeMax: { type: "string", required: true, description: "RFC 3339 upper bound." },
|
|
790
|
+
timeZone: { type: "string", required: false },
|
|
791
|
+
groupExpansionMax: { type: "number", required: false },
|
|
792
|
+
calendarExpansionMax: { type: "number", required: false },
|
|
793
|
+
},
|
|
794
|
+
async execute(input, ctx) {
|
|
795
|
+
const p = (input ?? {});
|
|
796
|
+
const body = {
|
|
797
|
+
timeMin: p.timeMin,
|
|
798
|
+
timeMax: p.timeMax,
|
|
799
|
+
timeZone: p.timeZone,
|
|
800
|
+
groupExpansionMax: p.groupExpansionMax,
|
|
801
|
+
calendarExpansionMax: p.calendarExpansionMax,
|
|
802
|
+
items: p.calendarIds.map((id) => ({ id })),
|
|
803
|
+
};
|
|
804
|
+
return calRequest(ctx, "POST", `/freeBusy`, body);
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
// ─── Calendar list management ───────────────────────────────────
|
|
808
|
+
rl.registerAction("calendarList.list", {
|
|
809
|
+
description: "List calendars on the user's calendar list (the user's left sidebar).",
|
|
810
|
+
inputSchema: {
|
|
811
|
+
minAccessRole: { type: "string", required: false, description: "freeBusyReader | reader | writer | owner" },
|
|
812
|
+
showDeleted: { type: "boolean", required: false },
|
|
813
|
+
showHidden: { type: "boolean", required: false },
|
|
814
|
+
returnAll: { type: "boolean", required: false, default: true },
|
|
815
|
+
},
|
|
816
|
+
async execute(input, ctx) {
|
|
817
|
+
const p = (input ?? {});
|
|
818
|
+
const qs = {
|
|
819
|
+
minAccessRole: p.minAccessRole,
|
|
820
|
+
showDeleted: p.showDeleted,
|
|
821
|
+
showHidden: p.showHidden,
|
|
822
|
+
maxResults: 250,
|
|
823
|
+
};
|
|
824
|
+
if (p.returnAll ?? true)
|
|
825
|
+
return paginateAll(ctx, "/users/me/calendarList", "items", qs);
|
|
826
|
+
const res = (await calRequest(ctx, "GET", "/users/me/calendarList", undefined, qs));
|
|
827
|
+
return res.items ?? [];
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
rl.registerAction("calendarList.insert", {
|
|
831
|
+
description: "Add a calendar (by id) to the user's calendar list.",
|
|
832
|
+
inputSchema: {
|
|
833
|
+
calendarId: { type: "string", required: true },
|
|
834
|
+
colorRgbFormat: { type: "boolean", required: false },
|
|
835
|
+
defaultReminders: { type: "array", required: false },
|
|
836
|
+
summaryOverride: { type: "string", required: false },
|
|
837
|
+
},
|
|
838
|
+
async execute(input, ctx) {
|
|
839
|
+
const p = (input ?? {});
|
|
840
|
+
const body = { id: p.calendarId };
|
|
841
|
+
if (p.summaryOverride)
|
|
842
|
+
body.summaryOverride = p.summaryOverride;
|
|
843
|
+
if (Array.isArray(p.defaultReminders))
|
|
844
|
+
body.defaultReminders = p.defaultReminders;
|
|
845
|
+
return calRequest(ctx, "POST", "/users/me/calendarList", body, { colorRgbFormat: p.colorRgbFormat });
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
rl.registerAction("calendarList.patch", {
|
|
849
|
+
description: "Patch a calendar entry on the user's calendar list (colors, summary override, reminders, selected).",
|
|
850
|
+
inputSchema: {
|
|
851
|
+
calendarId: { type: "string", required: true },
|
|
852
|
+
colorId: { type: "string", required: false },
|
|
853
|
+
backgroundColor: { type: "string", required: false },
|
|
854
|
+
foregroundColor: { type: "string", required: false },
|
|
855
|
+
summaryOverride: { type: "string", required: false },
|
|
856
|
+
selected: { type: "boolean", required: false },
|
|
857
|
+
hidden: { type: "boolean", required: false },
|
|
858
|
+
defaultReminders: { type: "array", required: false },
|
|
859
|
+
},
|
|
860
|
+
async execute(input, ctx) {
|
|
861
|
+
const p = (input ?? {});
|
|
862
|
+
const body = {};
|
|
863
|
+
for (const k of ["colorId", "backgroundColor", "foregroundColor", "summaryOverride", "selected", "hidden", "defaultReminders"]) {
|
|
864
|
+
if (p[k] !== undefined)
|
|
865
|
+
body[k] = p[k];
|
|
866
|
+
}
|
|
867
|
+
return calRequest(ctx, "PATCH", `/users/me/calendarList/${encodeCalendarId(p.calendarId)}`, body, { colorRgbFormat: p.backgroundColor || p.foregroundColor ? true : undefined });
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
rl.registerAction("calendarList.delete", {
|
|
871
|
+
description: "Remove a calendar from the user's calendar list (does not delete the underlying calendar).",
|
|
872
|
+
inputSchema: { calendarId: { type: "string", required: true } },
|
|
873
|
+
async execute(input, ctx) {
|
|
874
|
+
const p = (input ?? {});
|
|
875
|
+
await calRequest(ctx, "DELETE", `/users/me/calendarList/${encodeCalendarId(p.calendarId)}`);
|
|
876
|
+
return { success: true };
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
// ─── ACL (calendar sharing) ──────────────────────────────────────
|
|
880
|
+
rl.registerAction("acl.list", {
|
|
881
|
+
description: "List ACL rules on a calendar.",
|
|
882
|
+
inputSchema: {
|
|
883
|
+
calendarId: { type: "string", required: true },
|
|
884
|
+
returnAll: { type: "boolean", required: false, default: true },
|
|
885
|
+
},
|
|
886
|
+
async execute(input, ctx) {
|
|
887
|
+
const p = (input ?? {});
|
|
888
|
+
const path = `/calendars/${encodeCalendarId(p.calendarId)}/acl`;
|
|
889
|
+
if (p.returnAll ?? true)
|
|
890
|
+
return paginateAll(ctx, path, "items", { maxResults: 250 });
|
|
891
|
+
const res = (await calRequest(ctx, "GET", path));
|
|
892
|
+
return res.items ?? [];
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
rl.registerAction("acl.insert", {
|
|
896
|
+
description: "Add an ACL rule to a calendar. Roles: 'none' | 'freeBusyReader' | 'reader' | 'writer' | 'owner'.",
|
|
897
|
+
inputSchema: {
|
|
898
|
+
calendarId: { type: "string", required: true },
|
|
899
|
+
role: { type: "string", required: true },
|
|
900
|
+
scopeType: { type: "string", required: true, description: "'default' | 'user' | 'group' | 'domain'" },
|
|
901
|
+
scopeValue: { type: "string", required: false, description: "Email / domain depending on scopeType." },
|
|
902
|
+
sendNotifications: { type: "boolean", required: false },
|
|
903
|
+
},
|
|
904
|
+
async execute(input, ctx) {
|
|
905
|
+
const p = (input ?? {});
|
|
906
|
+
const body = { role: p.role, scope: { type: p.scopeType, value: p.scopeValue } };
|
|
907
|
+
return calRequest(ctx, "POST", `/calendars/${encodeCalendarId(p.calendarId)}/acl`, body, { sendNotifications: p.sendNotifications });
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
rl.registerAction("acl.update", {
|
|
911
|
+
description: "Patch an ACL rule's role.",
|
|
912
|
+
inputSchema: {
|
|
913
|
+
calendarId: { type: "string", required: true },
|
|
914
|
+
ruleId: { type: "string", required: true },
|
|
915
|
+
role: { type: "string", required: true },
|
|
916
|
+
},
|
|
917
|
+
async execute(input, ctx) {
|
|
918
|
+
const p = (input ?? {});
|
|
919
|
+
return calRequest(ctx, "PATCH", `/calendars/${encodeCalendarId(p.calendarId)}/acl/${encodeURIComponent(p.ruleId)}`, { role: p.role });
|
|
920
|
+
},
|
|
921
|
+
});
|
|
922
|
+
rl.registerAction("acl.delete", {
|
|
923
|
+
description: "Remove an ACL rule.",
|
|
924
|
+
inputSchema: {
|
|
925
|
+
calendarId: { type: "string", required: true },
|
|
926
|
+
ruleId: { type: "string", required: true },
|
|
927
|
+
},
|
|
928
|
+
async execute(input, ctx) {
|
|
929
|
+
const p = (input ?? {});
|
|
930
|
+
await calRequest(ctx, "DELETE", `/calendars/${encodeCalendarId(p.calendarId)}/acl/${encodeURIComponent(p.ruleId)}`);
|
|
931
|
+
return { success: true };
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
// ─── Settings ────────────────────────────────────────────────────
|
|
935
|
+
rl.registerAction("settings.list", {
|
|
936
|
+
description: "List the user's calendar settings (timezone, week start, working location, etc.).",
|
|
937
|
+
inputSchema: { returnAll: { type: "boolean", required: false, default: true } },
|
|
938
|
+
async execute(_input, ctx) {
|
|
939
|
+
return paginateAll(ctx, "/users/me/settings", "items", { maxResults: 100 });
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
rl.registerAction("settings.get", {
|
|
943
|
+
description: "Get a single setting by key (e.g. 'timezone', 'weekStart', 'locale').",
|
|
944
|
+
inputSchema: { setting: { type: "string", required: true } },
|
|
945
|
+
async execute(input, ctx) {
|
|
946
|
+
const p = (input ?? {});
|
|
947
|
+
return calRequest(ctx, "GET", `/users/me/settings/${encodeURIComponent(p.setting)}`);
|
|
948
|
+
},
|
|
949
|
+
});
|
|
783
950
|
}
|