runline 0.3.3 → 0.5.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/core/engine.js +139 -7
- package/dist/main.js +0 -0
- package/dist/plugins/gmail/src/index.js +13 -13
- package/dist/plugins/googleCalendar/src/index.js +795 -0
- package/dist/plugins/googleContacts/src/index.js +691 -0
- package/dist/plugins/googleDocs/src/index.js +669 -0
- package/dist/plugins/googleDrive/src/index.js +1161 -0
- package/dist/plugins/googleSheets/src/index.js +913 -0
- package/dist/plugins/googleSlides/src/index.js +319 -0
- package/dist/plugins/googleTasks/src/index.js +419 -0
- package/package.json +4 -2
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Docs plugin for runline.
|
|
3
|
+
*
|
|
4
|
+
* OAuth2 user flow, same shape as the rest of the Google plugins.
|
|
5
|
+
* Scopes: `auth/documents` for docs; `auth/drive.file` is added
|
|
6
|
+
* because `document.create` goes through Drive's files endpoint
|
|
7
|
+
* — the Docs API itself only creates blank documents without a
|
|
8
|
+
* target folder.
|
|
9
|
+
*
|
|
10
|
+
* Surface area:
|
|
11
|
+
*
|
|
12
|
+
* document.create
|
|
13
|
+
* document.get (optional `simple=true` returns flat text)
|
|
14
|
+
* document.batchUpdate (raw request list)
|
|
15
|
+
*
|
|
16
|
+
* Plus convenience helpers that wrap the most common batchUpdate
|
|
17
|
+
* shapes as first-class actions, addressable without constructing
|
|
18
|
+
* the nested request objects by hand:
|
|
19
|
+
*
|
|
20
|
+
* document.insertText
|
|
21
|
+
* document.replaceAllText
|
|
22
|
+
* document.deleteContentRange
|
|
23
|
+
* document.insertTable
|
|
24
|
+
* document.insertPageBreak
|
|
25
|
+
* document.createParagraphBullets
|
|
26
|
+
* document.deleteParagraphBullets
|
|
27
|
+
* document.createNamedRange
|
|
28
|
+
* document.deleteNamedRange
|
|
29
|
+
* document.createHeader / document.deleteHeader
|
|
30
|
+
* document.createFooter / document.deleteFooter
|
|
31
|
+
* document.deletePositionedObject
|
|
32
|
+
* document.insertTableRow / document.deleteTableRow
|
|
33
|
+
* document.insertTableColumn / document.deleteTableColumn
|
|
34
|
+
*
|
|
35
|
+
* Every helper ultimately hits `POST /v1/documents/{id}:batchUpdate`
|
|
36
|
+
* with a single request; callers who need to chain multiple edits
|
|
37
|
+
* atomically can compose them via `document.batchUpdate`.
|
|
38
|
+
*/
|
|
39
|
+
// ─── OAuth ───────────────────────────────────────────────────────
|
|
40
|
+
const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
41
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
42
|
+
async function refreshAccessToken(ctx) {
|
|
43
|
+
const cfg = ctx.connection.config;
|
|
44
|
+
const { clientId, clientSecret, refreshToken } = cfg;
|
|
45
|
+
if (!clientId || !clientSecret || !refreshToken) {
|
|
46
|
+
throw new Error("googleDocs: missing clientId/clientSecret/refreshToken. Run the Docs OAuth helper to seed these.");
|
|
47
|
+
}
|
|
48
|
+
const body = new URLSearchParams({
|
|
49
|
+
client_id: clientId,
|
|
50
|
+
client_secret: clientSecret,
|
|
51
|
+
refresh_token: refreshToken,
|
|
52
|
+
grant_type: "refresh_token",
|
|
53
|
+
});
|
|
54
|
+
const res = await fetch(TOKEN_ENDPOINT, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
57
|
+
body: body.toString(),
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(`googleDocs: token refresh failed (${res.status}): ${await res.text()}`);
|
|
61
|
+
}
|
|
62
|
+
const data = (await res.json());
|
|
63
|
+
const expiresAt = Date.now() + data.expires_in * 1000;
|
|
64
|
+
await ctx.updateConnection({
|
|
65
|
+
accessToken: data.access_token,
|
|
66
|
+
accessTokenExpiresAt: expiresAt,
|
|
67
|
+
});
|
|
68
|
+
return data.access_token;
|
|
69
|
+
}
|
|
70
|
+
async function accessToken(ctx) {
|
|
71
|
+
const cfg = ctx.connection.config;
|
|
72
|
+
if (cfg.accessToken &&
|
|
73
|
+
typeof cfg.accessTokenExpiresAt === "number" &&
|
|
74
|
+
Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
|
|
75
|
+
return cfg.accessToken;
|
|
76
|
+
}
|
|
77
|
+
return refreshAccessToken(ctx);
|
|
78
|
+
}
|
|
79
|
+
// ─── Request ─────────────────────────────────────────────────────
|
|
80
|
+
const DOCS_BASE = "https://docs.googleapis.com/v1";
|
|
81
|
+
const DRIVE_BASE = "https://www.googleapis.com/drive/v3";
|
|
82
|
+
async function docsRequest(ctx, method, path, body, qs, baseOverride) {
|
|
83
|
+
const token = await accessToken(ctx);
|
|
84
|
+
const url = new URL(`${baseOverride ?? DOCS_BASE}${path}`);
|
|
85
|
+
if (qs) {
|
|
86
|
+
for (const [k, v] of Object.entries(qs)) {
|
|
87
|
+
if (v === undefined || v === null)
|
|
88
|
+
continue;
|
|
89
|
+
url.searchParams.set(k, String(v));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const init = {
|
|
93
|
+
method,
|
|
94
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
95
|
+
};
|
|
96
|
+
if (body && Object.keys(body).length > 0) {
|
|
97
|
+
init.headers["Content-Type"] = "application/json";
|
|
98
|
+
init.body = JSON.stringify(body);
|
|
99
|
+
}
|
|
100
|
+
const res = await fetch(url.toString(), init);
|
|
101
|
+
if (res.status === 204)
|
|
102
|
+
return { success: true };
|
|
103
|
+
const text = await res.text();
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(`googleDocs: ${method} ${path} → ${res.status} ${text}`);
|
|
106
|
+
}
|
|
107
|
+
return text ? JSON.parse(text) : { success: true };
|
|
108
|
+
}
|
|
109
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
110
|
+
const DOC_URL_REGEX = /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9-_]+)/;
|
|
111
|
+
/**
|
|
112
|
+
* Accept a bare document ID or a full docs.google.com URL and return
|
|
113
|
+
* the ID. Falls through to the input unchanged if no URL is detected.
|
|
114
|
+
*/
|
|
115
|
+
function extractDocumentId(input) {
|
|
116
|
+
if (!input)
|
|
117
|
+
throw new Error("googleDocs: documentId or URL is required");
|
|
118
|
+
const m = input.match(DOC_URL_REGEX);
|
|
119
|
+
return m ? m[1] : input;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build a `Location` object for Docs insert requests. When
|
|
123
|
+
* `segmentId` is "body" or missing, send an empty segmentId — Docs
|
|
124
|
+
* treats that as "the document body". `index` is required for
|
|
125
|
+
* `location`; `endOfSegmentLocation` doesn't take one.
|
|
126
|
+
*/
|
|
127
|
+
function buildLocation(kind, segmentId, index) {
|
|
128
|
+
const seg = segmentId && segmentId !== "body" ? segmentId : "";
|
|
129
|
+
if (kind === "endOfSegmentLocation") {
|
|
130
|
+
return { endOfSegmentLocation: { segmentId: seg } };
|
|
131
|
+
}
|
|
132
|
+
if (index === undefined || index === null) {
|
|
133
|
+
throw new Error("googleDocs: `index` is required when location kind is 'location'");
|
|
134
|
+
}
|
|
135
|
+
return { location: { segmentId: seg, index } };
|
|
136
|
+
}
|
|
137
|
+
async function runBatchUpdate(ctx, documentId, request, writeControl) {
|
|
138
|
+
const body = { requests: [request] };
|
|
139
|
+
if (writeControl)
|
|
140
|
+
body.writeControl = writeControl;
|
|
141
|
+
const res = (await docsRequest(ctx, "POST", `/documents/${documentId}:batchUpdate`, body));
|
|
142
|
+
// Flatten single-request replies so callers don't have to drill in.
|
|
143
|
+
const reply = res.replies?.[0] ?? {};
|
|
144
|
+
const key = Object.keys(reply)[0];
|
|
145
|
+
return { documentId, ...(key ? { [key]: reply[key] } : {}) };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Walk a `document.body.content` tree and concatenate every
|
|
149
|
+
* `textRun.content` we find — the `simple=true` output on
|
|
150
|
+
* `document.get`. Intentionally ignores tables, headers, footers,
|
|
151
|
+
* and inline objects.
|
|
152
|
+
*/
|
|
153
|
+
function flattenBodyText(body) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
const content = body?.content ?? [];
|
|
156
|
+
for (const item of content) {
|
|
157
|
+
const para = item.paragraph;
|
|
158
|
+
if (!para?.elements)
|
|
159
|
+
continue;
|
|
160
|
+
for (const el of para.elements) {
|
|
161
|
+
const tr = el.textRun;
|
|
162
|
+
if (tr?.content)
|
|
163
|
+
parts.push(tr.content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return parts.join("");
|
|
167
|
+
}
|
|
168
|
+
// ─── Plugin ──────────────────────────────────────────────────────
|
|
169
|
+
const SCOPES = [
|
|
170
|
+
"https://www.googleapis.com/auth/documents",
|
|
171
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
172
|
+
];
|
|
173
|
+
export default function googleDocs(rl) {
|
|
174
|
+
rl.setName("googleDocs");
|
|
175
|
+
rl.setVersion("0.1.0");
|
|
176
|
+
rl.setOAuth({
|
|
177
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
178
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
179
|
+
scopes: SCOPES,
|
|
180
|
+
authParams: { access_type: "offline", prompt: "consent" },
|
|
181
|
+
setupHelp: [
|
|
182
|
+
"You need a Google Cloud OAuth client. Takes ~5 minutes, one time.",
|
|
183
|
+
"",
|
|
184
|
+
"1. Create or pick a Google Cloud project:",
|
|
185
|
+
" https://console.cloud.google.com/projectcreate",
|
|
186
|
+
"",
|
|
187
|
+
"2. Enable the Google Docs API (and Drive API for document.create):",
|
|
188
|
+
" https://console.cloud.google.com/apis/library/docs.googleapis.com",
|
|
189
|
+
" https://console.cloud.google.com/apis/library/drive.googleapis.com",
|
|
190
|
+
"",
|
|
191
|
+
"3. Configure the OAuth consent screen:",
|
|
192
|
+
" https://console.cloud.google.com/apis/credentials/consent",
|
|
193
|
+
" • Audience: External",
|
|
194
|
+
"",
|
|
195
|
+
"4. Add yourself as a test user:",
|
|
196
|
+
" https://console.cloud.google.com/auth/audience",
|
|
197
|
+
"",
|
|
198
|
+
"5. Create the OAuth client:",
|
|
199
|
+
" https://console.cloud.google.com/apis/credentials",
|
|
200
|
+
" • + Create credentials → OAuth client ID",
|
|
201
|
+
" • Application type: Web application",
|
|
202
|
+
" • Authorized redirect URIs → + Add URI: {{redirectUri}}",
|
|
203
|
+
"",
|
|
204
|
+
"6. Paste the Client ID and Client Secret below, or export",
|
|
205
|
+
" GOOGLE_DOCS_CLIENT_ID and GOOGLE_DOCS_CLIENT_SECRET.",
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
rl.setConnectionSchema({
|
|
209
|
+
clientId: { type: "string", required: true, env: "GOOGLE_DOCS_CLIENT_ID" },
|
|
210
|
+
clientSecret: { type: "string", required: true, env: "GOOGLE_DOCS_CLIENT_SECRET" },
|
|
211
|
+
refreshToken: { type: "string", required: true, env: "GOOGLE_DOCS_REFRESH_TOKEN" },
|
|
212
|
+
accessToken: { type: "string", required: false },
|
|
213
|
+
accessTokenExpiresAt: { type: "number", required: false },
|
|
214
|
+
});
|
|
215
|
+
// ── Document lifecycle ────────────────────────────────
|
|
216
|
+
rl.registerAction("document.create", {
|
|
217
|
+
description: "Create a new Google Doc, optionally in a specific Drive folder (goes through the Drive API; needs drive.file scope).",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
title: { type: "string", required: true },
|
|
220
|
+
folderId: {
|
|
221
|
+
type: "string",
|
|
222
|
+
required: false,
|
|
223
|
+
description: "Parent folder in Drive. Omit to place in My Drive root.",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
async execute(input, ctx) {
|
|
227
|
+
const p = (input ?? {});
|
|
228
|
+
const body = {
|
|
229
|
+
name: p.title,
|
|
230
|
+
mimeType: "application/vnd.google-apps.document",
|
|
231
|
+
};
|
|
232
|
+
if (p.folderId) {
|
|
233
|
+
body.parents = [p.folderId];
|
|
234
|
+
}
|
|
235
|
+
return docsRequest(ctx, "POST", "/files", body, undefined, DRIVE_BASE);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
rl.registerAction("document.get", {
|
|
239
|
+
description: "Get a document. Accepts a bare ID or a docs.google.com URL. `simple=true` collapses the body to plain text.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
document: { type: "string", required: true, description: "Document ID or URL" },
|
|
242
|
+
simple: { type: "boolean", required: false },
|
|
243
|
+
suggestionsViewMode: {
|
|
244
|
+
type: "string",
|
|
245
|
+
required: false,
|
|
246
|
+
description: "DEFAULT_FOR_CURRENT_ACCESS | SUGGESTIONS_INLINE | PREVIEW_SUGGESTIONS_ACCEPTED | PREVIEW_WITHOUT_SUGGESTIONS",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
async execute(input, ctx) {
|
|
250
|
+
const p = (input ?? {});
|
|
251
|
+
const documentId = extractDocumentId(p.document);
|
|
252
|
+
const qs = {};
|
|
253
|
+
if (p.suggestionsViewMode)
|
|
254
|
+
qs.suggestionsViewMode = p.suggestionsViewMode;
|
|
255
|
+
const res = (await docsRequest(ctx, "GET", `/documents/${documentId}`, undefined, qs));
|
|
256
|
+
if (!p.simple)
|
|
257
|
+
return res;
|
|
258
|
+
return { documentId, content: flattenBodyText(res.body) };
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
rl.registerAction("document.batchUpdate", {
|
|
262
|
+
description: "Raw passthrough to documents.batchUpdate — pass a full `requests` array for atomic multi-edit operations.",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
document: { type: "string", required: true },
|
|
265
|
+
requests: { type: "array", required: true },
|
|
266
|
+
writeControl: {
|
|
267
|
+
type: "object",
|
|
268
|
+
required: false,
|
|
269
|
+
description: "{requiredRevisionId} | {targetRevisionId}",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
async execute(input, ctx) {
|
|
273
|
+
const p = (input ?? {});
|
|
274
|
+
const documentId = extractDocumentId(p.document);
|
|
275
|
+
const body = {
|
|
276
|
+
requests: p.requests,
|
|
277
|
+
};
|
|
278
|
+
if (p.writeControl)
|
|
279
|
+
body.writeControl = p.writeControl;
|
|
280
|
+
return docsRequest(ctx, "POST", `/documents/${documentId}:batchUpdate`, body);
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
// ── Text edits ────────────────────────────────────────
|
|
284
|
+
rl.registerAction("document.insertText", {
|
|
285
|
+
description: "Insert text at a specific index, or at the end of a segment (body/header/footer/footnote).",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
document: { type: "string", required: true },
|
|
288
|
+
text: { type: "string", required: true },
|
|
289
|
+
locationKind: {
|
|
290
|
+
type: "string",
|
|
291
|
+
required: false,
|
|
292
|
+
description: "location (default; requires index) | endOfSegmentLocation",
|
|
293
|
+
},
|
|
294
|
+
index: { type: "number", required: false, description: "Required for locationKind=location" },
|
|
295
|
+
segmentId: {
|
|
296
|
+
type: "string",
|
|
297
|
+
required: false,
|
|
298
|
+
description: 'Segment ID, or "body" / empty for the main body',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
async execute(input, ctx) {
|
|
302
|
+
const p = (input ?? {});
|
|
303
|
+
const documentId = extractDocumentId(p.document);
|
|
304
|
+
const kind = p.locationKind ?? "location";
|
|
305
|
+
const locObj = buildLocation(kind, p.segmentId, p.index);
|
|
306
|
+
return runBatchUpdate(ctx, documentId, {
|
|
307
|
+
insertText: { text: p.text, ...locObj },
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
rl.registerAction("document.replaceAllText", {
|
|
312
|
+
description: "Replace every occurrence of a text string throughout the document.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
document: { type: "string", required: true },
|
|
315
|
+
findText: { type: "string", required: true },
|
|
316
|
+
replaceText: { type: "string", required: true },
|
|
317
|
+
matchCase: { type: "boolean", required: false },
|
|
318
|
+
},
|
|
319
|
+
async execute(input, ctx) {
|
|
320
|
+
const p = (input ?? {});
|
|
321
|
+
const documentId = extractDocumentId(p.document);
|
|
322
|
+
return runBatchUpdate(ctx, documentId, {
|
|
323
|
+
replaceAllText: {
|
|
324
|
+
replaceText: p.replaceText,
|
|
325
|
+
containsText: { text: p.findText, matchCase: p.matchCase === true },
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
rl.registerAction("document.deleteContentRange", {
|
|
331
|
+
description: "Delete text between two indices in a segment.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
document: { type: "string", required: true },
|
|
334
|
+
startIndex: { type: "number", required: true },
|
|
335
|
+
endIndex: { type: "number", required: true },
|
|
336
|
+
segmentId: { type: "string", required: false },
|
|
337
|
+
},
|
|
338
|
+
async execute(input, ctx) {
|
|
339
|
+
const p = (input ?? {});
|
|
340
|
+
const documentId = extractDocumentId(p.document);
|
|
341
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
342
|
+
return runBatchUpdate(ctx, documentId, {
|
|
343
|
+
deleteContentRange: {
|
|
344
|
+
range: {
|
|
345
|
+
segmentId: seg,
|
|
346
|
+
startIndex: p.startIndex,
|
|
347
|
+
endIndex: p.endIndex,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
// ── Structural inserts ────────────────────────────────
|
|
354
|
+
rl.registerAction("document.insertPageBreak", {
|
|
355
|
+
description: "Insert a page break at an index or at the end of a segment.",
|
|
356
|
+
inputSchema: {
|
|
357
|
+
document: { type: "string", required: true },
|
|
358
|
+
locationKind: { type: "string", required: false },
|
|
359
|
+
index: { type: "number", required: false },
|
|
360
|
+
segmentId: { type: "string", required: false },
|
|
361
|
+
},
|
|
362
|
+
async execute(input, ctx) {
|
|
363
|
+
const p = (input ?? {});
|
|
364
|
+
const documentId = extractDocumentId(p.document);
|
|
365
|
+
const kind = p.locationKind ?? "location";
|
|
366
|
+
return runBatchUpdate(ctx, documentId, {
|
|
367
|
+
insertPageBreak: buildLocation(kind, p.segmentId, p.index),
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
rl.registerAction("document.insertTable", {
|
|
372
|
+
description: "Insert an empty table with the given dimensions.",
|
|
373
|
+
inputSchema: {
|
|
374
|
+
document: { type: "string", required: true },
|
|
375
|
+
rows: { type: "number", required: true },
|
|
376
|
+
columns: { type: "number", required: true },
|
|
377
|
+
locationKind: { type: "string", required: false },
|
|
378
|
+
index: { type: "number", required: false },
|
|
379
|
+
segmentId: { type: "string", required: false },
|
|
380
|
+
},
|
|
381
|
+
async execute(input, ctx) {
|
|
382
|
+
const p = (input ?? {});
|
|
383
|
+
const documentId = extractDocumentId(p.document);
|
|
384
|
+
const kind = p.locationKind ?? "location";
|
|
385
|
+
return runBatchUpdate(ctx, documentId, {
|
|
386
|
+
insertTable: {
|
|
387
|
+
rows: p.rows,
|
|
388
|
+
columns: p.columns,
|
|
389
|
+
...buildLocation(kind, p.segmentId, p.index),
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
rl.registerAction("document.insertTableRow", {
|
|
395
|
+
description: "Insert a table row above or below a cell in an existing table.",
|
|
396
|
+
inputSchema: {
|
|
397
|
+
document: { type: "string", required: true },
|
|
398
|
+
tableStartIndex: {
|
|
399
|
+
type: "number",
|
|
400
|
+
required: true,
|
|
401
|
+
description: "Document index where the table begins",
|
|
402
|
+
},
|
|
403
|
+
rowIndex: { type: "number", required: true },
|
|
404
|
+
columnIndex: { type: "number", required: true },
|
|
405
|
+
insertBelow: { type: "boolean", required: false, description: "default: false (insert above)" },
|
|
406
|
+
segmentId: { type: "string", required: false },
|
|
407
|
+
},
|
|
408
|
+
async execute(input, ctx) {
|
|
409
|
+
const p = (input ?? {});
|
|
410
|
+
const documentId = extractDocumentId(p.document);
|
|
411
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
412
|
+
return runBatchUpdate(ctx, documentId, {
|
|
413
|
+
insertTableRow: {
|
|
414
|
+
insertBelow: p.insertBelow === true,
|
|
415
|
+
tableCellLocation: {
|
|
416
|
+
rowIndex: p.rowIndex,
|
|
417
|
+
columnIndex: p.columnIndex,
|
|
418
|
+
tableStartLocation: { segmentId: seg, index: p.tableStartIndex },
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
rl.registerAction("document.deleteTableRow", {
|
|
425
|
+
description: "Delete a specific row from a table.",
|
|
426
|
+
inputSchema: {
|
|
427
|
+
document: { type: "string", required: true },
|
|
428
|
+
tableStartIndex: { type: "number", required: true },
|
|
429
|
+
rowIndex: { type: "number", required: true },
|
|
430
|
+
columnIndex: { type: "number", required: true },
|
|
431
|
+
segmentId: { type: "string", required: false },
|
|
432
|
+
},
|
|
433
|
+
async execute(input, ctx) {
|
|
434
|
+
const p = (input ?? {});
|
|
435
|
+
const documentId = extractDocumentId(p.document);
|
|
436
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
437
|
+
return runBatchUpdate(ctx, documentId, {
|
|
438
|
+
deleteTableRow: {
|
|
439
|
+
tableCellLocation: {
|
|
440
|
+
rowIndex: p.rowIndex,
|
|
441
|
+
columnIndex: p.columnIndex,
|
|
442
|
+
tableStartLocation: { segmentId: seg, index: p.tableStartIndex },
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
rl.registerAction("document.insertTableColumn", {
|
|
449
|
+
description: "Insert a column left or right of a cell.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
document: { type: "string", required: true },
|
|
452
|
+
tableStartIndex: { type: "number", required: true },
|
|
453
|
+
rowIndex: { type: "number", required: true },
|
|
454
|
+
columnIndex: { type: "number", required: true },
|
|
455
|
+
insertRight: { type: "boolean", required: false, description: "default: false (insert left)" },
|
|
456
|
+
segmentId: { type: "string", required: false },
|
|
457
|
+
},
|
|
458
|
+
async execute(input, ctx) {
|
|
459
|
+
const p = (input ?? {});
|
|
460
|
+
const documentId = extractDocumentId(p.document);
|
|
461
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
462
|
+
return runBatchUpdate(ctx, documentId, {
|
|
463
|
+
insertTableColumn: {
|
|
464
|
+
insertRight: p.insertRight === true,
|
|
465
|
+
tableCellLocation: {
|
|
466
|
+
rowIndex: p.rowIndex,
|
|
467
|
+
columnIndex: p.columnIndex,
|
|
468
|
+
tableStartLocation: { segmentId: seg, index: p.tableStartIndex },
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
rl.registerAction("document.deleteTableColumn", {
|
|
475
|
+
description: "Delete a specific column from a table.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
document: { type: "string", required: true },
|
|
478
|
+
tableStartIndex: { type: "number", required: true },
|
|
479
|
+
rowIndex: { type: "number", required: true },
|
|
480
|
+
columnIndex: { type: "number", required: true },
|
|
481
|
+
segmentId: { type: "string", required: false },
|
|
482
|
+
},
|
|
483
|
+
async execute(input, ctx) {
|
|
484
|
+
const p = (input ?? {});
|
|
485
|
+
const documentId = extractDocumentId(p.document);
|
|
486
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
487
|
+
return runBatchUpdate(ctx, documentId, {
|
|
488
|
+
deleteTableColumn: {
|
|
489
|
+
tableCellLocation: {
|
|
490
|
+
rowIndex: p.rowIndex,
|
|
491
|
+
columnIndex: p.columnIndex,
|
|
492
|
+
tableStartLocation: { segmentId: seg, index: p.tableStartIndex },
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
// ── Bullets ───────────────────────────────────────────
|
|
499
|
+
rl.registerAction("document.createParagraphBullets", {
|
|
500
|
+
description: "Apply a bullet preset to paragraphs spanning a range. Presets: BULLET_DISC_CIRCLE_SQUARE, BULLET_DIAMONDX_ARROW3D_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN, NUMBERED_DECIMAL_NESTED, etc.",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
document: { type: "string", required: true },
|
|
503
|
+
bulletPreset: { type: "string", required: true },
|
|
504
|
+
startIndex: { type: "number", required: true },
|
|
505
|
+
endIndex: { type: "number", required: true },
|
|
506
|
+
segmentId: { type: "string", required: false },
|
|
507
|
+
},
|
|
508
|
+
async execute(input, ctx) {
|
|
509
|
+
const p = (input ?? {});
|
|
510
|
+
const documentId = extractDocumentId(p.document);
|
|
511
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
512
|
+
return runBatchUpdate(ctx, documentId, {
|
|
513
|
+
createParagraphBullets: {
|
|
514
|
+
bulletPreset: p.bulletPreset,
|
|
515
|
+
range: { segmentId: seg, startIndex: p.startIndex, endIndex: p.endIndex },
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
rl.registerAction("document.deleteParagraphBullets", {
|
|
521
|
+
description: "Remove bullets from paragraphs in a range.",
|
|
522
|
+
inputSchema: {
|
|
523
|
+
document: { type: "string", required: true },
|
|
524
|
+
startIndex: { type: "number", required: true },
|
|
525
|
+
endIndex: { type: "number", required: true },
|
|
526
|
+
segmentId: { type: "string", required: false },
|
|
527
|
+
},
|
|
528
|
+
async execute(input, ctx) {
|
|
529
|
+
const p = (input ?? {});
|
|
530
|
+
const documentId = extractDocumentId(p.document);
|
|
531
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
532
|
+
return runBatchUpdate(ctx, documentId, {
|
|
533
|
+
deleteParagraphBullets: {
|
|
534
|
+
range: { segmentId: seg, startIndex: p.startIndex, endIndex: p.endIndex },
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
// ── Named ranges ──────────────────────────────────────
|
|
540
|
+
rl.registerAction("document.createNamedRange", {
|
|
541
|
+
description: "Create a named range over a span of text (useful for later programmatic edits).",
|
|
542
|
+
inputSchema: {
|
|
543
|
+
document: { type: "string", required: true },
|
|
544
|
+
name: { type: "string", required: true },
|
|
545
|
+
startIndex: { type: "number", required: true },
|
|
546
|
+
endIndex: { type: "number", required: true },
|
|
547
|
+
segmentId: { type: "string", required: false },
|
|
548
|
+
},
|
|
549
|
+
async execute(input, ctx) {
|
|
550
|
+
const p = (input ?? {});
|
|
551
|
+
const documentId = extractDocumentId(p.document);
|
|
552
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
553
|
+
return runBatchUpdate(ctx, documentId, {
|
|
554
|
+
createNamedRange: {
|
|
555
|
+
name: p.name,
|
|
556
|
+
range: { segmentId: seg, startIndex: p.startIndex, endIndex: p.endIndex },
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
rl.registerAction("document.deleteNamedRange", {
|
|
562
|
+
description: "Delete named range(s). Pass one of `namedRangeId` or `name`; the latter deletes every range sharing that name.",
|
|
563
|
+
inputSchema: {
|
|
564
|
+
document: { type: "string", required: true },
|
|
565
|
+
namedRangeId: { type: "string", required: false },
|
|
566
|
+
name: { type: "string", required: false },
|
|
567
|
+
},
|
|
568
|
+
async execute(input, ctx) {
|
|
569
|
+
const p = (input ?? {});
|
|
570
|
+
const documentId = extractDocumentId(p.document);
|
|
571
|
+
if (!p.namedRangeId && !p.name) {
|
|
572
|
+
throw new Error("googleDocs: provide namedRangeId or name");
|
|
573
|
+
}
|
|
574
|
+
const req = p.namedRangeId
|
|
575
|
+
? { namedRangeId: p.namedRangeId }
|
|
576
|
+
: { name: p.name };
|
|
577
|
+
return runBatchUpdate(ctx, documentId, { deleteNamedRange: req });
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
// ── Header / footer / positioned object ──────────────
|
|
581
|
+
rl.registerAction("document.createHeader", {
|
|
582
|
+
description: "Create a DEFAULT header attached to a SectionBreak.",
|
|
583
|
+
inputSchema: {
|
|
584
|
+
document: { type: "string", required: true },
|
|
585
|
+
locationKind: { type: "string", required: false },
|
|
586
|
+
index: { type: "number", required: false },
|
|
587
|
+
segmentId: { type: "string", required: false },
|
|
588
|
+
},
|
|
589
|
+
async execute(input, ctx) {
|
|
590
|
+
const p = (input ?? {});
|
|
591
|
+
const documentId = extractDocumentId(p.document);
|
|
592
|
+
const kind = p.locationKind ?? "location";
|
|
593
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
594
|
+
const sectionBreakLocation = { segmentId: seg };
|
|
595
|
+
if (kind === "location") {
|
|
596
|
+
if (p.index === undefined) {
|
|
597
|
+
throw new Error("googleDocs: `index` is required when locationKind=location");
|
|
598
|
+
}
|
|
599
|
+
sectionBreakLocation.index = p.index;
|
|
600
|
+
}
|
|
601
|
+
return runBatchUpdate(ctx, documentId, {
|
|
602
|
+
createHeader: { type: "DEFAULT", sectionBreakLocation },
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
rl.registerAction("document.deleteHeader", {
|
|
607
|
+
description: "Delete a header by ID.",
|
|
608
|
+
inputSchema: {
|
|
609
|
+
document: { type: "string", required: true },
|
|
610
|
+
headerId: { type: "string", required: true },
|
|
611
|
+
},
|
|
612
|
+
async execute(input, ctx) {
|
|
613
|
+
const p = (input ?? {});
|
|
614
|
+
const documentId = extractDocumentId(p.document);
|
|
615
|
+
return runBatchUpdate(ctx, documentId, { deleteHeader: { headerId: p.headerId } });
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
rl.registerAction("document.createFooter", {
|
|
619
|
+
description: "Create a DEFAULT footer attached to a SectionBreak.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
document: { type: "string", required: true },
|
|
622
|
+
locationKind: { type: "string", required: false },
|
|
623
|
+
index: { type: "number", required: false },
|
|
624
|
+
segmentId: { type: "string", required: false },
|
|
625
|
+
},
|
|
626
|
+
async execute(input, ctx) {
|
|
627
|
+
const p = (input ?? {});
|
|
628
|
+
const documentId = extractDocumentId(p.document);
|
|
629
|
+
const kind = p.locationKind ?? "location";
|
|
630
|
+
const seg = p.segmentId && p.segmentId !== "body" ? p.segmentId : "";
|
|
631
|
+
const sectionBreakLocation = { segmentId: seg };
|
|
632
|
+
if (kind === "location") {
|
|
633
|
+
if (p.index === undefined) {
|
|
634
|
+
throw new Error("googleDocs: `index` is required when locationKind=location");
|
|
635
|
+
}
|
|
636
|
+
sectionBreakLocation.index = p.index;
|
|
637
|
+
}
|
|
638
|
+
return runBatchUpdate(ctx, documentId, {
|
|
639
|
+
createFooter: { type: "DEFAULT", sectionBreakLocation },
|
|
640
|
+
});
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
rl.registerAction("document.deleteFooter", {
|
|
644
|
+
description: "Delete a footer by ID.",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
document: { type: "string", required: true },
|
|
647
|
+
footerId: { type: "string", required: true },
|
|
648
|
+
},
|
|
649
|
+
async execute(input, ctx) {
|
|
650
|
+
const p = (input ?? {});
|
|
651
|
+
const documentId = extractDocumentId(p.document);
|
|
652
|
+
return runBatchUpdate(ctx, documentId, { deleteFooter: { footerId: p.footerId } });
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
rl.registerAction("document.deletePositionedObject", {
|
|
656
|
+
description: "Delete a positioned object (inline image, floating image, etc.) by its objectId.",
|
|
657
|
+
inputSchema: {
|
|
658
|
+
document: { type: "string", required: true },
|
|
659
|
+
objectId: { type: "string", required: true },
|
|
660
|
+
},
|
|
661
|
+
async execute(input, ctx) {
|
|
662
|
+
const p = (input ?? {});
|
|
663
|
+
const documentId = extractDocumentId(p.document);
|
|
664
|
+
return runBatchUpdate(ctx, documentId, {
|
|
665
|
+
deletePositionedObject: { objectId: p.objectId },
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
}
|