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
|
@@ -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
|
}
|
|
@@ -313,6 +313,17 @@ const SCOPES = [
|
|
|
313
313
|
"https://www.googleapis.com/auth/spreadsheets",
|
|
314
314
|
"https://www.googleapis.com/auth/drive.file",
|
|
315
315
|
];
|
|
316
|
+
function a1RangeToGridRange(a1, sheetId) {
|
|
317
|
+
// Strip optional 'Sheet!' prefix
|
|
318
|
+
const m = a1.match(/^(?:[^!]+!)?([A-Z]+)(\d+):([A-Z]+)(\d+)$/i);
|
|
319
|
+
if (!m)
|
|
320
|
+
throw new Error(`a1RangeToGridRange: cannot parse ${a1}; expected A1 form like A1:D20`);
|
|
321
|
+
const startCol = columnLetterToNumber(m[1]) - 1;
|
|
322
|
+
const startRow = parseInt(m[2], 10) - 1;
|
|
323
|
+
const endCol = columnLetterToNumber(m[3]);
|
|
324
|
+
const endRow = parseInt(m[4], 10);
|
|
325
|
+
return { sheetId, startRowIndex: startRow, endRowIndex: endRow, startColumnIndex: startCol, endColumnIndex: endCol };
|
|
326
|
+
}
|
|
316
327
|
export default function googleSheets(rl) {
|
|
317
328
|
rl.setName("googleSheets");
|
|
318
329
|
rl.setVersion("0.1.0");
|
|
@@ -899,4 +910,268 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
|
|
|
899
910
|
appended: prepared.appendData.length,
|
|
900
911
|
newColumns: prepared.newColumns,
|
|
901
912
|
};
|
|
913
|
+
// ─── Charts ─────────────────────────────────────────────────────
|
|
914
|
+
rl.registerAction("chart.add", {
|
|
915
|
+
description: "Embed a chart on a sheet. Minimal signature: type ('COLUMN'|'BAR'|'LINE'|'AREA'|'PIE'|'SCATTER'|'COMBO'), data source (sheet name + A1 range), and target anchor cell. For full control pass a raw `chartSpec` object instead.",
|
|
916
|
+
inputSchema: {
|
|
917
|
+
spreadsheetId: { type: "string", required: true },
|
|
918
|
+
title: { type: "string", required: false },
|
|
919
|
+
type: { type: "string", required: false, description: "Basic chart type. Default COLUMN. Ignored when `chartSpec` is provided." },
|
|
920
|
+
sourceSheet: { type: "string", required: false, description: "Sheet name holding the source range. Ignored when `chartSpec` is provided." },
|
|
921
|
+
sourceRange: { type: "string", required: false, description: "A1 range on `sourceSheet`, e.g. 'A1:D20'. Ignored when `chartSpec` is provided." },
|
|
922
|
+
legendPosition: { type: "string", required: false, description: "BOTTOM_LEGEND | TOP_LEGEND | RIGHT_LEGEND | LEFT_LEGEND | NO_LEGEND. Default BOTTOM_LEGEND." },
|
|
923
|
+
stackedType: { type: "string", required: false, description: "STACKED | PERCENT_STACKED — for COLUMN/BAR/AREA." },
|
|
924
|
+
anchorSheet: { type: "string", required: true, description: "Sheet to place the chart on." },
|
|
925
|
+
anchorRow: { type: "number", required: false, default: 0 },
|
|
926
|
+
anchorColumn: { type: "number", required: false, default: 0 },
|
|
927
|
+
widthPx: { type: "number", required: false, default: 600 },
|
|
928
|
+
heightPx: { type: "number", required: false, default: 371 },
|
|
929
|
+
chartSpec: { type: "object", required: false, description: "Raw ChartSpec object; bypasses the simple type+source builder." },
|
|
930
|
+
},
|
|
931
|
+
async execute(input, ctx) {
|
|
932
|
+
const p = (input ?? {});
|
|
933
|
+
const spreadsheetId = p.spreadsheetId;
|
|
934
|
+
const anchorSheetId = await resolveSheetId(ctx, spreadsheetId, p.anchorSheet);
|
|
935
|
+
let spec;
|
|
936
|
+
if (p.chartSpec) {
|
|
937
|
+
spec = p.chartSpec;
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
const sourceSheetId = await resolveSheetId(ctx, spreadsheetId, p.sourceSheet);
|
|
941
|
+
const grid = a1RangeToGridRange(p.sourceRange, sourceSheetId);
|
|
942
|
+
spec = {
|
|
943
|
+
title: p.title,
|
|
944
|
+
basicChart: {
|
|
945
|
+
chartType: p.type ?? "COLUMN",
|
|
946
|
+
legendPosition: p.legendPosition ?? "BOTTOM_LEGEND",
|
|
947
|
+
stackedType: p.stackedType,
|
|
948
|
+
headerCount: 1,
|
|
949
|
+
domains: [{ domain: { sourceRange: { sources: [{
|
|
950
|
+
sheetId: grid.sheetId, startRowIndex: grid.startRowIndex, endRowIndex: grid.endRowIndex,
|
|
951
|
+
startColumnIndex: grid.startColumnIndex, endColumnIndex: grid.startColumnIndex + 1,
|
|
952
|
+
}] } } }],
|
|
953
|
+
series: [{ series: { sourceRange: { sources: [{
|
|
954
|
+
sheetId: grid.sheetId, startRowIndex: grid.startRowIndex, endRowIndex: grid.endRowIndex,
|
|
955
|
+
startColumnIndex: grid.startColumnIndex + 1, endColumnIndex: grid.endColumnIndex,
|
|
956
|
+
}] } }, targetAxis: "LEFT_AXIS" }],
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const req = {
|
|
961
|
+
addChart: {
|
|
962
|
+
chart: {
|
|
963
|
+
spec,
|
|
964
|
+
position: {
|
|
965
|
+
overlayPosition: {
|
|
966
|
+
anchorCell: { sheetId: anchorSheetId, rowIndex: p.anchorRow ?? 0, columnIndex: p.anchorColumn ?? 0 },
|
|
967
|
+
widthPixels: p.widthPx ?? 600,
|
|
968
|
+
heightPixels: p.heightPx ?? 371,
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
return spreadsheetBatchUpdate(ctx, spreadsheetId, [req]);
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
rl.registerAction("chart.update", {
|
|
978
|
+
description: "Update an existing chart's spec by chartId.",
|
|
979
|
+
inputSchema: {
|
|
980
|
+
spreadsheetId: { type: "string", required: true },
|
|
981
|
+
chartId: { type: "number", required: true },
|
|
982
|
+
chartSpec: { type: "object", required: true },
|
|
983
|
+
fields: { type: "string", required: false, default: "*" },
|
|
984
|
+
},
|
|
985
|
+
async execute(input, ctx) {
|
|
986
|
+
const p = (input ?? {});
|
|
987
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
988
|
+
{
|
|
989
|
+
updateChartSpec: {
|
|
990
|
+
chartId: p.chartId,
|
|
991
|
+
spec: p.chartSpec,
|
|
992
|
+
fields: p.fields ?? "*",
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
]);
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
rl.registerAction("chart.delete", {
|
|
999
|
+
description: "Delete a chart by its embedded-object id.",
|
|
1000
|
+
inputSchema: {
|
|
1001
|
+
spreadsheetId: { type: "string", required: true },
|
|
1002
|
+
chartId: { type: "number", required: true },
|
|
1003
|
+
},
|
|
1004
|
+
async execute(input, ctx) {
|
|
1005
|
+
const p = (input ?? {});
|
|
1006
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1007
|
+
{ deleteEmbeddedObject: { objectId: p.chartId } },
|
|
1008
|
+
]);
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
// ─── Named ranges ───────────────────────────────────────────────
|
|
1012
|
+
rl.registerAction("namedRange.add", {
|
|
1013
|
+
description: "Create a named range on a sheet (so formulas can reference it by name).",
|
|
1014
|
+
inputSchema: {
|
|
1015
|
+
spreadsheetId: { type: "string", required: true },
|
|
1016
|
+
name: { type: "string", required: true },
|
|
1017
|
+
sheet: { type: "string", required: true },
|
|
1018
|
+
range: { type: "string", required: true, description: "A1 range on `sheet`, e.g. 'A1:D20'." },
|
|
1019
|
+
},
|
|
1020
|
+
async execute(input, ctx) {
|
|
1021
|
+
const p = (input ?? {});
|
|
1022
|
+
const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
|
|
1023
|
+
const grid = a1RangeToGridRange(p.range, sheetId);
|
|
1024
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1025
|
+
{ addNamedRange: { namedRange: { name: p.name, range: grid } } },
|
|
1026
|
+
]);
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
rl.registerAction("namedRange.delete", {
|
|
1030
|
+
description: "Delete a named range by its id.",
|
|
1031
|
+
inputSchema: {
|
|
1032
|
+
spreadsheetId: { type: "string", required: true },
|
|
1033
|
+
namedRangeId: { type: "string", required: true },
|
|
1034
|
+
},
|
|
1035
|
+
async execute(input, ctx) {
|
|
1036
|
+
const p = (input ?? {});
|
|
1037
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1038
|
+
{ deleteNamedRange: { namedRangeId: p.namedRangeId } },
|
|
1039
|
+
]);
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
// ─── Protected ranges ───────────────────────────────────────────
|
|
1043
|
+
rl.registerAction("protectedRange.add", {
|
|
1044
|
+
description: "Protect a range from edits. Pass `editorEmails` to restrict who can still edit; without it, only the sheet owner can edit.",
|
|
1045
|
+
inputSchema: {
|
|
1046
|
+
spreadsheetId: { type: "string", required: true },
|
|
1047
|
+
sheet: { type: "string", required: true },
|
|
1048
|
+
range: { type: "string", required: false, description: "A1 range. Omit to protect the whole sheet." },
|
|
1049
|
+
description: { type: "string", required: false },
|
|
1050
|
+
warningOnly: { type: "boolean", required: false, description: "When true, edits are allowed but show a warning." },
|
|
1051
|
+
editorEmails: { type: "array", required: false },
|
|
1052
|
+
},
|
|
1053
|
+
async execute(input, ctx) {
|
|
1054
|
+
const p = (input ?? {});
|
|
1055
|
+
const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
|
|
1056
|
+
const protectedRange = { description: p.description, warningOnly: p.warningOnly };
|
|
1057
|
+
if (p.range)
|
|
1058
|
+
protectedRange.range = a1RangeToGridRange(p.range, sheetId);
|
|
1059
|
+
else
|
|
1060
|
+
protectedRange.range = { sheetId };
|
|
1061
|
+
if (Array.isArray(p.editorEmails)) {
|
|
1062
|
+
protectedRange.editors = { users: p.editorEmails };
|
|
1063
|
+
}
|
|
1064
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1065
|
+
{ addProtectedRange: { protectedRange } },
|
|
1066
|
+
]);
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
rl.registerAction("protectedRange.delete", {
|
|
1070
|
+
description: "Delete a protected range by its id.",
|
|
1071
|
+
inputSchema: {
|
|
1072
|
+
spreadsheetId: { type: "string", required: true },
|
|
1073
|
+
protectedRangeId: { type: "number", required: true },
|
|
1074
|
+
},
|
|
1075
|
+
async execute(input, ctx) {
|
|
1076
|
+
const p = (input ?? {});
|
|
1077
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1078
|
+
{ deleteProtectedRange: { protectedRangeId: p.protectedRangeId } },
|
|
1079
|
+
]);
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
1082
|
+
// ─── Conditional formatting ─────────────────────────────────────
|
|
1083
|
+
rl.registerAction("conditionalFormat.add", {
|
|
1084
|
+
description: "Add a single-condition conditional-format rule. For complex rules pass a raw `rule` object instead of the simple builder.",
|
|
1085
|
+
inputSchema: {
|
|
1086
|
+
spreadsheetId: { type: "string", required: true },
|
|
1087
|
+
sheet: { type: "string", required: true },
|
|
1088
|
+
range: { type: "string", required: true },
|
|
1089
|
+
condition: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
required: false,
|
|
1092
|
+
description: "BooleanCondition, e.g. { type: 'NUMBER_GREATER', values: [{ userEnteredValue: '100' }] }.",
|
|
1093
|
+
},
|
|
1094
|
+
backgroundColorHex: { type: "string", required: false },
|
|
1095
|
+
foregroundColorHex: { type: "string", required: false },
|
|
1096
|
+
bold: { type: "boolean", required: false },
|
|
1097
|
+
italic: { type: "boolean", required: false },
|
|
1098
|
+
rule: { type: "object", required: false, description: "Raw ConditionalFormatRule; bypasses the simple builder." },
|
|
1099
|
+
},
|
|
1100
|
+
async execute(input, ctx) {
|
|
1101
|
+
const p = (input ?? {});
|
|
1102
|
+
const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
|
|
1103
|
+
const grid = a1RangeToGridRange(p.range, sheetId);
|
|
1104
|
+
let rule;
|
|
1105
|
+
if (p.rule)
|
|
1106
|
+
rule = p.rule;
|
|
1107
|
+
else {
|
|
1108
|
+
const fmt = {};
|
|
1109
|
+
if (p.backgroundColorHex)
|
|
1110
|
+
fmt.backgroundColor = hexToRgb(p.backgroundColorHex);
|
|
1111
|
+
if (p.foregroundColorHex || p.bold || p.italic) {
|
|
1112
|
+
const ts = {};
|
|
1113
|
+
if (p.foregroundColorHex)
|
|
1114
|
+
ts.foregroundColor = hexToRgb(p.foregroundColorHex);
|
|
1115
|
+
if (p.bold !== undefined)
|
|
1116
|
+
ts.bold = p.bold;
|
|
1117
|
+
if (p.italic !== undefined)
|
|
1118
|
+
ts.italic = p.italic;
|
|
1119
|
+
fmt.textFormat = ts;
|
|
1120
|
+
}
|
|
1121
|
+
rule = { ranges: [grid], booleanRule: { condition: p.condition, format: fmt } };
|
|
1122
|
+
}
|
|
1123
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [{ addConditionalFormatRule: { rule, index: 0 } }]);
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
rl.registerAction("conditionalFormat.delete", {
|
|
1127
|
+
description: "Delete a conditional-format rule by its index within the sheet's rule list.",
|
|
1128
|
+
inputSchema: {
|
|
1129
|
+
spreadsheetId: { type: "string", required: true },
|
|
1130
|
+
sheet: { type: "string", required: true },
|
|
1131
|
+
index: { type: "number", required: true },
|
|
1132
|
+
},
|
|
1133
|
+
async execute(input, ctx) {
|
|
1134
|
+
const p = (input ?? {});
|
|
1135
|
+
const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
|
|
1136
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1137
|
+
{ deleteConditionalFormatRule: { sheetId, index: p.index } },
|
|
1138
|
+
]);
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
// ─── Data validation ────────────────────────────────────────────
|
|
1142
|
+
rl.registerAction("dataValidation.set", {
|
|
1143
|
+
description: "Apply a data-validation rule to a range. Pass either `condition` (BooleanCondition) or `oneOfList` (array of values for ONE_OF_LIST). Use `clear: true` to remove validation.",
|
|
1144
|
+
inputSchema: {
|
|
1145
|
+
spreadsheetId: { type: "string", required: true },
|
|
1146
|
+
sheet: { type: "string", required: true },
|
|
1147
|
+
range: { type: "string", required: true },
|
|
1148
|
+
condition: { type: "object", required: false },
|
|
1149
|
+
oneOfList: { type: "array", required: false },
|
|
1150
|
+
showCustomUi: { type: "boolean", required: false, default: true },
|
|
1151
|
+
strict: { type: "boolean", required: false, default: true },
|
|
1152
|
+
inputMessage: { type: "string", required: false },
|
|
1153
|
+
clear: { type: "boolean", required: false },
|
|
1154
|
+
},
|
|
1155
|
+
async execute(input, ctx) {
|
|
1156
|
+
const p = (input ?? {});
|
|
1157
|
+
const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
|
|
1158
|
+
const grid = a1RangeToGridRange(p.range, sheetId);
|
|
1159
|
+
let rule = null;
|
|
1160
|
+
if (!p.clear) {
|
|
1161
|
+
let cond = p.condition;
|
|
1162
|
+
if (!cond && Array.isArray(p.oneOfList)) {
|
|
1163
|
+
cond = {
|
|
1164
|
+
type: "ONE_OF_LIST",
|
|
1165
|
+
values: p.oneOfList.map((v) => ({ userEnteredValue: String(v) })),
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
if (!cond)
|
|
1169
|
+
throw new Error("googleSheets.dataValidation.set: pass condition or oneOfList, or set clear=true");
|
|
1170
|
+
rule = { condition: cond, strict: p.strict ?? true, showCustomUi: p.showCustomUi ?? true, inputMessage: p.inputMessage };
|
|
1171
|
+
}
|
|
1172
|
+
return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
|
|
1173
|
+
{ setDataValidation: { range: grid, rule: rule ?? undefined } },
|
|
1174
|
+
]);
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
902
1177
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { graphRequest, microsoftSetupHelp, userBase } from "../../_shared/microsoftAuth.js";
|
|
2
|
+
const NAME = "microsoftMail";
|
|
3
|
+
const SCOPES = [
|
|
4
|
+
"https://graph.microsoft.com/Mail.Send",
|
|
5
|
+
"https://graph.microsoft.com/Mail.ReadWrite",
|
|
6
|
+
"https://graph.microsoft.com/Mail.Read",
|
|
7
|
+
];
|
|
8
|
+
const recipients = (addrs) => (Array.isArray(addrs) ? addrs : addrs ? [addrs] : []).map((a) => ({
|
|
9
|
+
emailAddress: { address: a },
|
|
10
|
+
}));
|
|
11
|
+
function toMessage(input) {
|
|
12
|
+
return {
|
|
13
|
+
subject: input.subject,
|
|
14
|
+
body: { contentType: input.html ? "HTML" : "Text", content: input.body ?? "" },
|
|
15
|
+
toRecipients: recipients(input.to),
|
|
16
|
+
ccRecipients: recipients(input.cc),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export default function microsoftMail(rl) {
|
|
20
|
+
rl.setName(NAME);
|
|
21
|
+
rl.setVersion("1.0.0");
|
|
22
|
+
rl.setConnectionSchema({
|
|
23
|
+
tenantId: { type: "string", required: false, env: "MS_GRAPH_TENANT_ID", description: "Entra tenant id (app-only) or omit for OAuth /common" },
|
|
24
|
+
clientId: { type: "string", required: false, env: "MS_GRAPH_CLIENT_ID", description: "App (client) id" },
|
|
25
|
+
clientSecret: { type: "string", required: false, env: "MS_GRAPH_CLIENT_SECRET", description: "Client secret VALUE" },
|
|
26
|
+
refreshToken: { type: "string", required: false, env: "MICROSOFTMAIL_REFRESH_TOKEN", description: "OAuth2 refresh token (set by the login flow)" },
|
|
27
|
+
userUpn: { type: "string", required: false, env: "MS_GRAPH_USER_UPN", description: "App-only only: target mailbox UPN (e.g. agent@contoso.com)" },
|
|
28
|
+
});
|
|
29
|
+
rl.setOAuth({
|
|
30
|
+
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
31
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
32
|
+
scopes: [...SCOPES, "offline_access"],
|
|
33
|
+
setupHelp: microsoftSetupHelp("Mail.Send, Mail.ReadWrite, Mail.Read"),
|
|
34
|
+
});
|
|
35
|
+
rl.registerAction("mail.send", {
|
|
36
|
+
description: "Send an email as the connected mailbox. Returns {success}. Get user approval before sending external mail.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
to: { type: "array", required: true, description: "Recipient address(es)" },
|
|
39
|
+
subject: { type: "string", required: true },
|
|
40
|
+
body: { type: "string", required: true },
|
|
41
|
+
cc: { type: "array", required: false },
|
|
42
|
+
html: { type: "boolean", required: false, description: "Body is HTML (default plain text)" },
|
|
43
|
+
},
|
|
44
|
+
async execute(input, ctx) {
|
|
45
|
+
await graphRequest(ctx, NAME, SCOPES, "POST", `${userBase(ctx)}/sendMail`, {
|
|
46
|
+
message: toMessage(input),
|
|
47
|
+
saveToSentItems: true,
|
|
48
|
+
});
|
|
49
|
+
return { success: true };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
rl.registerAction("mail.draft", {
|
|
53
|
+
description: "Create a draft email (not sent). Returns {id, webLink}.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
to: { type: "array", required: false },
|
|
56
|
+
subject: { type: "string", required: true },
|
|
57
|
+
body: { type: "string", required: true },
|
|
58
|
+
cc: { type: "array", required: false },
|
|
59
|
+
html: { type: "boolean", required: false },
|
|
60
|
+
},
|
|
61
|
+
async execute(input, ctx) {
|
|
62
|
+
const r = await graphRequest(ctx, NAME, SCOPES, "POST", `${userBase(ctx)}/messages`, toMessage(input));
|
|
63
|
+
return { id: r.id, webLink: r.webLink };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
rl.registerAction("mail.list", {
|
|
67
|
+
description: "List recent messages. Optional KQL search. Returns [{id,subject,from,receivedDateTime,bodyPreview,hasAttachments}].",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
search: { type: "string", required: false, description: "KQL search across the mailbox" },
|
|
70
|
+
top: { type: "number", required: false, default: 20 },
|
|
71
|
+
},
|
|
72
|
+
async execute(input, ctx) {
|
|
73
|
+
const qs = new URLSearchParams({
|
|
74
|
+
$top: String(input.top ?? 20),
|
|
75
|
+
$select: "id,subject,from,receivedDateTime,bodyPreview,hasAttachments",
|
|
76
|
+
$orderby: "receivedDateTime desc",
|
|
77
|
+
});
|
|
78
|
+
if (input.search)
|
|
79
|
+
qs.set("$search", `"${input.search}"`);
|
|
80
|
+
const r = await graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/messages?${qs}`);
|
|
81
|
+
return r.value;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
rl.registerAction("mail.get", {
|
|
85
|
+
description: "Get one message with full body by id.",
|
|
86
|
+
inputSchema: { id: { type: "string", required: true } },
|
|
87
|
+
async execute(input, ctx) {
|
|
88
|
+
return graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/messages/${input.id}`);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|