runline 0.7.5 → 0.7.7

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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Code mode for agents — turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",