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.
|
@@ -18,6 +18,24 @@
|
|
|
18
18
|
* drive.create / drive.get / drive.list / drive.update / drive.delete
|
|
19
19
|
* (Shared Drives / Team Drives)
|
|
20
20
|
*
|
|
21
|
+
* comment.list / comment.get / comment.create / comment.update /
|
|
22
|
+
* comment.delete / comment.resolve / comment.reopen
|
|
23
|
+
* reply.list / reply.create / reply.update / reply.delete
|
|
24
|
+
*
|
|
25
|
+
* revision.list / revision.get / revision.download /
|
|
26
|
+
* revision.update / revision.delete / revision.restore
|
|
27
|
+
* (Office-file comments live inside the bytes; revision.* is the
|
|
28
|
+
* recovery path when file.update overwrites a working draft.)
|
|
29
|
+
*
|
|
30
|
+
* changes.getStartPageToken / changes.list / changes.watch / changes.stop
|
|
31
|
+
* (Drive change feed.)
|
|
32
|
+
*
|
|
33
|
+
* permission.update — patch an existing share role / expiration.
|
|
34
|
+
* accessProposal.list / accessProposal.resolve — Drive's "Request access" flow.
|
|
35
|
+
* about.get — current user, quota, export formats.
|
|
36
|
+
* file.export — native-doc export wrapper.
|
|
37
|
+
* file.list — raw files.list (the wrapper is fileFolder.search).
|
|
38
|
+
*
|
|
21
39
|
* Binary content conventions — every upload/download surface
|
|
22
40
|
* speaks base64 or filesystem paths:
|
|
23
41
|
*
|
|
@@ -1146,4 +1164,655 @@ export default function googleDrive(rl) {
|
|
|
1146
1164
|
return { success: true };
|
|
1147
1165
|
},
|
|
1148
1166
|
});
|
|
1167
|
+
// ─── Comments ────────────────────────────────────────────────────
|
|
1168
|
+
//
|
|
1169
|
+
// Drive's `comments` and `replies` resources. The comments live on
|
|
1170
|
+
// the Drive side for both Google-native docs and Office files, and
|
|
1171
|
+
// are distinct from in-document comments stored inside .docx /
|
|
1172
|
+
// .xlsx / .pptx bytes (those round-trip through `revisions`).
|
|
1173
|
+
const COMMENT_FIELDS = "kind,id,createdTime,modifiedTime,resolved,deleted," +
|
|
1174
|
+
"author(displayName,emailAddress)," +
|
|
1175
|
+
"quotedFileContent(value,mimeType)," +
|
|
1176
|
+
"anchor,content,htmlContent," +
|
|
1177
|
+
"replies(kind,id,createdTime,modifiedTime,deleted,author(displayName,emailAddress),action,content,htmlContent)";
|
|
1178
|
+
const COMMENT_LIST_FIELDS = `kind,nextPageToken,comments(${COMMENT_FIELDS})`;
|
|
1179
|
+
const REPLY_FIELDS = "kind,id,createdTime,modifiedTime,deleted," +
|
|
1180
|
+
"author(displayName,emailAddress),action,content,htmlContent";
|
|
1181
|
+
const REPLY_LIST_FIELDS = `kind,nextPageToken,replies(${REPLY_FIELDS})`;
|
|
1182
|
+
rl.registerAction("comment.list", {
|
|
1183
|
+
description: "List all comments on a Drive file, including each comment's replies. Returns an array sorted by Drive's default (most recent first).",
|
|
1184
|
+
inputSchema: {
|
|
1185
|
+
fileId: { type: "string", required: true },
|
|
1186
|
+
includeDeleted: {
|
|
1187
|
+
type: "boolean",
|
|
1188
|
+
required: false,
|
|
1189
|
+
description: "Include deleted comments. Default false.",
|
|
1190
|
+
},
|
|
1191
|
+
pageSize: {
|
|
1192
|
+
type: "number",
|
|
1193
|
+
required: false,
|
|
1194
|
+
description: "Max comments per page (Drive caps at 100).",
|
|
1195
|
+
},
|
|
1196
|
+
startModifiedTime: {
|
|
1197
|
+
type: "string",
|
|
1198
|
+
required: false,
|
|
1199
|
+
description: "RFC 3339 timestamp; only return comments modified at or after this time.",
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
async execute(input, ctx) {
|
|
1203
|
+
const p = (input ?? {});
|
|
1204
|
+
const fileId = p.fileId;
|
|
1205
|
+
const out = [];
|
|
1206
|
+
const query = {
|
|
1207
|
+
fields: COMMENT_LIST_FIELDS,
|
|
1208
|
+
includeDeleted: p.includeDeleted ?? false,
|
|
1209
|
+
pageSize: p.pageSize ?? 100,
|
|
1210
|
+
startModifiedTime: p.startModifiedTime,
|
|
1211
|
+
};
|
|
1212
|
+
let pageToken;
|
|
1213
|
+
do {
|
|
1214
|
+
if (pageToken)
|
|
1215
|
+
query.pageToken = pageToken;
|
|
1216
|
+
const page = (await driveRequest(ctx, "GET", `/drive/v3/files/${fileId}/comments`, undefined, query));
|
|
1217
|
+
if (Array.isArray(page.comments))
|
|
1218
|
+
out.push(...page.comments);
|
|
1219
|
+
pageToken = page.nextPageToken;
|
|
1220
|
+
} while (pageToken);
|
|
1221
|
+
return { fileId, count: out.length, comments: out };
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
rl.registerAction("comment.get", {
|
|
1225
|
+
description: "Fetch a single comment (with its replies) by ID.",
|
|
1226
|
+
inputSchema: {
|
|
1227
|
+
fileId: { type: "string", required: true },
|
|
1228
|
+
commentId: { type: "string", required: true },
|
|
1229
|
+
includeDeleted: { type: "boolean", required: false },
|
|
1230
|
+
},
|
|
1231
|
+
async execute(input, ctx) {
|
|
1232
|
+
const p = (input ?? {});
|
|
1233
|
+
return driveRequest(ctx, "GET", `/drive/v3/files/${p.fileId}/comments/${p.commentId}`, undefined, { fields: COMMENT_FIELDS, includeDeleted: p.includeDeleted ?? false });
|
|
1234
|
+
},
|
|
1235
|
+
});
|
|
1236
|
+
rl.registerAction("comment.create", {
|
|
1237
|
+
description: "Create a new top-level comment on a file. Pass quotedFileContent.value (and optionally mimeType) to anchor the comment to a specific snippet.",
|
|
1238
|
+
inputSchema: {
|
|
1239
|
+
fileId: { type: "string", required: true },
|
|
1240
|
+
content: { type: "string", required: true, description: "Comment body (plain text)." },
|
|
1241
|
+
quotedFileContent: {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
required: false,
|
|
1244
|
+
description: "{ value: string, mimeType?: string }",
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
async execute(input, ctx) {
|
|
1248
|
+
const p = (input ?? {});
|
|
1249
|
+
const body = { content: p.content };
|
|
1250
|
+
if (p.quotedFileContent)
|
|
1251
|
+
body.quotedFileContent = p.quotedFileContent;
|
|
1252
|
+
return driveRequest(ctx, "POST", `/drive/v3/files/${p.fileId}/comments`, body, { fields: COMMENT_FIELDS });
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
rl.registerAction("comment.update", {
|
|
1256
|
+
description: "Edit the content of an existing comment. Caller must be the author or have edit rights.",
|
|
1257
|
+
inputSchema: {
|
|
1258
|
+
fileId: { type: "string", required: true },
|
|
1259
|
+
commentId: { type: "string", required: true },
|
|
1260
|
+
content: { type: "string", required: true },
|
|
1261
|
+
},
|
|
1262
|
+
async execute(input, ctx) {
|
|
1263
|
+
const p = (input ?? {});
|
|
1264
|
+
return driveRequest(ctx, "PATCH", `/drive/v3/files/${p.fileId}/comments/${p.commentId}`, { content: p.content }, { fields: COMMENT_FIELDS });
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
rl.registerAction("comment.delete", {
|
|
1268
|
+
description: "Soft-delete a comment.",
|
|
1269
|
+
inputSchema: {
|
|
1270
|
+
fileId: { type: "string", required: true },
|
|
1271
|
+
commentId: { type: "string", required: true },
|
|
1272
|
+
},
|
|
1273
|
+
async execute(input, ctx) {
|
|
1274
|
+
const p = (input ?? {});
|
|
1275
|
+
await driveRequest(ctx, "DELETE", `/drive/v3/files/${p.fileId}/comments/${p.commentId}`);
|
|
1276
|
+
return { success: true };
|
|
1277
|
+
},
|
|
1278
|
+
});
|
|
1279
|
+
rl.registerAction("comment.resolve", {
|
|
1280
|
+
description: "Resolve a comment thread by posting a resolution reply. `resolved` on a Comment is computed from replies; this is the canonical way to mark a thread done.",
|
|
1281
|
+
inputSchema: {
|
|
1282
|
+
fileId: { type: "string", required: true },
|
|
1283
|
+
commentId: { type: "string", required: true },
|
|
1284
|
+
content: {
|
|
1285
|
+
type: "string",
|
|
1286
|
+
required: false,
|
|
1287
|
+
description: "Optional reply body. Defaults to 'Resolved.'",
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
async execute(input, ctx) {
|
|
1291
|
+
const p = (input ?? {});
|
|
1292
|
+
return driveRequest(ctx, "POST", `/drive/v3/files/${p.fileId}/comments/${p.commentId}/replies`, { action: "resolve", content: p.content ?? "Resolved." }, { fields: REPLY_FIELDS });
|
|
1293
|
+
},
|
|
1294
|
+
});
|
|
1295
|
+
rl.registerAction("comment.reopen", {
|
|
1296
|
+
description: "Re-open a previously resolved comment by posting a reopen reply.",
|
|
1297
|
+
inputSchema: {
|
|
1298
|
+
fileId: { type: "string", required: true },
|
|
1299
|
+
commentId: { type: "string", required: true },
|
|
1300
|
+
content: { type: "string", required: false },
|
|
1301
|
+
},
|
|
1302
|
+
async execute(input, ctx) {
|
|
1303
|
+
const p = (input ?? {});
|
|
1304
|
+
return driveRequest(ctx, "POST", `/drive/v3/files/${p.fileId}/comments/${p.commentId}/replies`, { action: "reopen", content: p.content ?? "Reopened." }, { fields: REPLY_FIELDS });
|
|
1305
|
+
},
|
|
1306
|
+
});
|
|
1307
|
+
rl.registerAction("reply.list", {
|
|
1308
|
+
description: "List replies on a specific comment.",
|
|
1309
|
+
inputSchema: {
|
|
1310
|
+
fileId: { type: "string", required: true },
|
|
1311
|
+
commentId: { type: "string", required: true },
|
|
1312
|
+
includeDeleted: { type: "boolean", required: false },
|
|
1313
|
+
pageSize: { type: "number", required: false },
|
|
1314
|
+
},
|
|
1315
|
+
async execute(input, ctx) {
|
|
1316
|
+
const p = (input ?? {});
|
|
1317
|
+
const out = [];
|
|
1318
|
+
const query = {
|
|
1319
|
+
fields: REPLY_LIST_FIELDS,
|
|
1320
|
+
includeDeleted: p.includeDeleted ?? false,
|
|
1321
|
+
pageSize: p.pageSize ?? 100,
|
|
1322
|
+
};
|
|
1323
|
+
let pageToken;
|
|
1324
|
+
do {
|
|
1325
|
+
if (pageToken)
|
|
1326
|
+
query.pageToken = pageToken;
|
|
1327
|
+
const page = (await driveRequest(ctx, "GET", `/drive/v3/files/${p.fileId}/comments/${p.commentId}/replies`, undefined, query));
|
|
1328
|
+
if (Array.isArray(page.replies))
|
|
1329
|
+
out.push(...page.replies);
|
|
1330
|
+
pageToken = page.nextPageToken;
|
|
1331
|
+
} while (pageToken);
|
|
1332
|
+
return { fileId: p.fileId, commentId: p.commentId, count: out.length, replies: out };
|
|
1333
|
+
},
|
|
1334
|
+
});
|
|
1335
|
+
rl.registerAction("reply.create", {
|
|
1336
|
+
description: "Post a reply to a comment. Pass action: 'resolve' | 'reopen' to also flip the comment state.",
|
|
1337
|
+
inputSchema: {
|
|
1338
|
+
fileId: { type: "string", required: true },
|
|
1339
|
+
commentId: { type: "string", required: true },
|
|
1340
|
+
content: { type: "string", required: true },
|
|
1341
|
+
action: {
|
|
1342
|
+
type: "string",
|
|
1343
|
+
required: false,
|
|
1344
|
+
description: "'resolve' | 'reopen'. Omit for a plain reply.",
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
async execute(input, ctx) {
|
|
1348
|
+
const p = (input ?? {});
|
|
1349
|
+
const body = { content: p.content };
|
|
1350
|
+
if (p.action === "resolve" || p.action === "reopen")
|
|
1351
|
+
body.action = p.action;
|
|
1352
|
+
return driveRequest(ctx, "POST", `/drive/v3/files/${p.fileId}/comments/${p.commentId}/replies`, body, { fields: REPLY_FIELDS });
|
|
1353
|
+
},
|
|
1354
|
+
});
|
|
1355
|
+
rl.registerAction("reply.update", {
|
|
1356
|
+
description: "Edit the content of a reply.",
|
|
1357
|
+
inputSchema: {
|
|
1358
|
+
fileId: { type: "string", required: true },
|
|
1359
|
+
commentId: { type: "string", required: true },
|
|
1360
|
+
replyId: { type: "string", required: true },
|
|
1361
|
+
content: { type: "string", required: true },
|
|
1362
|
+
},
|
|
1363
|
+
async execute(input, ctx) {
|
|
1364
|
+
const p = (input ?? {});
|
|
1365
|
+
return driveRequest(ctx, "PATCH", `/drive/v3/files/${p.fileId}/comments/${p.commentId}/replies/${p.replyId}`, { content: p.content }, { fields: REPLY_FIELDS });
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
rl.registerAction("reply.delete", {
|
|
1369
|
+
description: "Soft-delete a reply.",
|
|
1370
|
+
inputSchema: {
|
|
1371
|
+
fileId: { type: "string", required: true },
|
|
1372
|
+
commentId: { type: "string", required: true },
|
|
1373
|
+
replyId: { type: "string", required: true },
|
|
1374
|
+
},
|
|
1375
|
+
async execute(input, ctx) {
|
|
1376
|
+
const p = (input ?? {});
|
|
1377
|
+
await driveRequest(ctx, "DELETE", `/drive/v3/files/${p.fileId}/comments/${p.commentId}/replies/${p.replyId}`);
|
|
1378
|
+
return { success: true };
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
// ─── Revisions ──────────────────────────────────────────────────
|
|
1382
|
+
//
|
|
1383
|
+
// Drive retains prior bytes of every file. For Google-native docs
|
|
1384
|
+
// that means snapshots of the doc state; for Office files (.docx,
|
|
1385
|
+
// .xlsx, .pptx) it means the actual byte history. Exposing this
|
|
1386
|
+
// matters because in-document review comments on Office files live
|
|
1387
|
+
// *inside* the file bytes (`word/comments.xml`), not in Drive's
|
|
1388
|
+
// `comments` resource. A call to `file.update({ contentPath })`
|
|
1389
|
+
// silently destroys those comments; the only programmatic recovery
|
|
1390
|
+
// path is reading prior revision bytes through these actions.
|
|
1391
|
+
//
|
|
1392
|
+
// Default retention is ~30 days / 100 revisions for Office files;
|
|
1393
|
+
// set `keepForever: true` via `revision.update` on a known-good
|
|
1394
|
+
// revision before any risky in-place update.
|
|
1395
|
+
const REVISION_FIELDS = "id,mimeType,modifiedTime,keepForever,originalFilename,size,md5Checksum," +
|
|
1396
|
+
"lastModifyingUser(displayName,emailAddress)," +
|
|
1397
|
+
"published,publishAuto,publishedOutsideDomain,publishedLink";
|
|
1398
|
+
const REVISION_LIST_FIELDS = `kind,nextPageToken,revisions(${REVISION_FIELDS})`;
|
|
1399
|
+
rl.registerAction("revision.list", {
|
|
1400
|
+
description: "List every revision Drive retains for a file, oldest first. For Office files the per-revision bytes are what `revision.download` returns.",
|
|
1401
|
+
inputSchema: {
|
|
1402
|
+
fileId: { type: "string", required: true },
|
|
1403
|
+
pageSize: { type: "number", required: false, description: "Default 200; Drive caps at 1000." },
|
|
1404
|
+
},
|
|
1405
|
+
async execute(input, ctx) {
|
|
1406
|
+
const p = (input ?? {});
|
|
1407
|
+
const fileId = p.fileId;
|
|
1408
|
+
const out = [];
|
|
1409
|
+
const query = {
|
|
1410
|
+
fields: REVISION_LIST_FIELDS,
|
|
1411
|
+
pageSize: p.pageSize ?? 200,
|
|
1412
|
+
};
|
|
1413
|
+
let pageToken;
|
|
1414
|
+
do {
|
|
1415
|
+
if (pageToken)
|
|
1416
|
+
query.pageToken = pageToken;
|
|
1417
|
+
const page = (await driveRequest(ctx, "GET", `/drive/v3/files/${fileId}/revisions`, undefined, query));
|
|
1418
|
+
if (Array.isArray(page.revisions))
|
|
1419
|
+
out.push(...page.revisions);
|
|
1420
|
+
pageToken = page.nextPageToken;
|
|
1421
|
+
} while (pageToken);
|
|
1422
|
+
return { fileId, count: out.length, revisions: out };
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
rl.registerAction("revision.get", {
|
|
1426
|
+
description: "Fetch metadata for a single revision.",
|
|
1427
|
+
inputSchema: {
|
|
1428
|
+
fileId: { type: "string", required: true },
|
|
1429
|
+
revisionId: { type: "string", required: true },
|
|
1430
|
+
},
|
|
1431
|
+
async execute(input, ctx) {
|
|
1432
|
+
const p = (input ?? {});
|
|
1433
|
+
return driveRequest(ctx, "GET", `/drive/v3/files/${p.fileId}/revisions/${p.revisionId}`, undefined, { fields: REVISION_FIELDS });
|
|
1434
|
+
},
|
|
1435
|
+
});
|
|
1436
|
+
rl.registerAction("revision.download", {
|
|
1437
|
+
description: "Download the bytes of a specific revision. Pass savePath to write to disk and get back the path; otherwise returns { contentBase64, mimeType, size }. This is the recovery path when an in-place file.update has overwritten in-document comments on an Office file.",
|
|
1438
|
+
inputSchema: {
|
|
1439
|
+
fileId: { type: "string", required: true },
|
|
1440
|
+
revisionId: { type: "string", required: true },
|
|
1441
|
+
savePath: {
|
|
1442
|
+
type: "string",
|
|
1443
|
+
required: false,
|
|
1444
|
+
description: "Filesystem path to write the bytes to. If omitted, returns base64.",
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
async execute(input, ctx) {
|
|
1448
|
+
const p = (input ?? {});
|
|
1449
|
+
const fileId = p.fileId;
|
|
1450
|
+
const revisionId = p.revisionId;
|
|
1451
|
+
const token = await accessToken(ctx);
|
|
1452
|
+
const url = new URL(`${API_BASE}/drive/v3/files/${fileId}/revisions/${revisionId}`);
|
|
1453
|
+
url.searchParams.set("alt", "media");
|
|
1454
|
+
const res = await fetch(url.toString(), {
|
|
1455
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1456
|
+
});
|
|
1457
|
+
if (!res.ok) {
|
|
1458
|
+
throw new Error(`googleDrive: revision download failed (${res.status}): ${await res.text()}`);
|
|
1459
|
+
}
|
|
1460
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
1461
|
+
const mimeType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
1462
|
+
if (typeof p.savePath === "string") {
|
|
1463
|
+
writeFileSync(p.savePath, bytes);
|
|
1464
|
+
return { fileId, revisionId, mimeType, size: bytes.byteLength, savedTo: p.savePath };
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
fileId,
|
|
1468
|
+
revisionId,
|
|
1469
|
+
mimeType,
|
|
1470
|
+
size: bytes.byteLength,
|
|
1471
|
+
contentBase64: bytes.toString("base64"),
|
|
1472
|
+
};
|
|
1473
|
+
},
|
|
1474
|
+
});
|
|
1475
|
+
rl.registerAction("revision.update", {
|
|
1476
|
+
description: "Patch revision metadata. The most useful flag is keepForever — without it Drive can garbage-collect revisions after 30 days / 100 versions on Office files. Set keepForever=true on the head revision before any risky file.update so prior bytes are guaranteed recoverable.",
|
|
1477
|
+
inputSchema: {
|
|
1478
|
+
fileId: { type: "string", required: true },
|
|
1479
|
+
revisionId: { type: "string", required: true },
|
|
1480
|
+
keepForever: { type: "boolean", required: false },
|
|
1481
|
+
published: { type: "boolean", required: false },
|
|
1482
|
+
publishAuto: { type: "boolean", required: false },
|
|
1483
|
+
publishedOutsideDomain: { type: "boolean", required: false },
|
|
1484
|
+
},
|
|
1485
|
+
async execute(input, ctx) {
|
|
1486
|
+
const p = (input ?? {});
|
|
1487
|
+
const body = {};
|
|
1488
|
+
for (const k of [
|
|
1489
|
+
"keepForever",
|
|
1490
|
+
"published",
|
|
1491
|
+
"publishAuto",
|
|
1492
|
+
"publishedOutsideDomain",
|
|
1493
|
+
]) {
|
|
1494
|
+
if (p[k] !== undefined)
|
|
1495
|
+
body[k] = p[k];
|
|
1496
|
+
}
|
|
1497
|
+
if (Object.keys(body).length === 0) {
|
|
1498
|
+
throw new Error("googleDrive.revision.update: pass at least one of keepForever / published / publishAuto / publishedOutsideDomain.");
|
|
1499
|
+
}
|
|
1500
|
+
return driveRequest(ctx, "PATCH", `/drive/v3/files/${p.fileId}/revisions/${p.revisionId}`, body, { fields: REVISION_FIELDS });
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
rl.registerAction("revision.delete", {
|
|
1504
|
+
description: "Permanently delete a revision (head revision cannot be deleted).",
|
|
1505
|
+
inputSchema: {
|
|
1506
|
+
fileId: { type: "string", required: true },
|
|
1507
|
+
revisionId: { type: "string", required: true },
|
|
1508
|
+
},
|
|
1509
|
+
async execute(input, ctx) {
|
|
1510
|
+
const p = (input ?? {});
|
|
1511
|
+
await driveRequest(ctx, "DELETE", `/drive/v3/files/${p.fileId}/revisions/${p.revisionId}`);
|
|
1512
|
+
return { success: true };
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
rl.registerAction("revision.restore", {
|
|
1516
|
+
description: "Restore an older revision as the head of the file. Downloads the revision bytes and re-uploads them via multipart so the head moves to that content. Drive's REST API has no native restore endpoint for binary files; this performs the equivalent in two calls. Returns the new head file resource.",
|
|
1517
|
+
inputSchema: {
|
|
1518
|
+
fileId: { type: "string", required: true },
|
|
1519
|
+
revisionId: { type: "string", required: true, description: "Revision to restore as head." },
|
|
1520
|
+
mimeType: {
|
|
1521
|
+
type: "string",
|
|
1522
|
+
required: false,
|
|
1523
|
+
description: "Override mime type for the re-upload. Defaults to the revision's mime type.",
|
|
1524
|
+
},
|
|
1525
|
+
},
|
|
1526
|
+
async execute(input, ctx) {
|
|
1527
|
+
const p = (input ?? {});
|
|
1528
|
+
const fileId = p.fileId;
|
|
1529
|
+
const revisionId = p.revisionId;
|
|
1530
|
+
const token = await accessToken(ctx);
|
|
1531
|
+
// 1. Pull the chosen revision's bytes.
|
|
1532
|
+
const dl = await fetch(`${API_BASE}/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`, { headers: { Authorization: `Bearer ${token}` } });
|
|
1533
|
+
if (!dl.ok) {
|
|
1534
|
+
throw new Error(`googleDrive: revision restore download failed (${dl.status}): ${await dl.text()}`);
|
|
1535
|
+
}
|
|
1536
|
+
const bytes = Buffer.from(await dl.arrayBuffer());
|
|
1537
|
+
const mime = p.mimeType ??
|
|
1538
|
+
dl.headers.get("content-type") ??
|
|
1539
|
+
"application/octet-stream";
|
|
1540
|
+
// 2. Multipart-PATCH them as the new head of the same file.
|
|
1541
|
+
const url = new URL(`${API_BASE}/upload/drive/v3/files/${fileId}`);
|
|
1542
|
+
url.searchParams.set("uploadType", "multipart");
|
|
1543
|
+
url.searchParams.set("supportsAllDrives", "true");
|
|
1544
|
+
url.searchParams.set("fields", "id,name,mimeType,modifiedTime,size,headRevisionId,webViewLink");
|
|
1545
|
+
const body = buildMultipart({ mimeType: mime }, bytes, mime);
|
|
1546
|
+
const res = await fetch(url.toString(), {
|
|
1547
|
+
method: "PATCH",
|
|
1548
|
+
headers: {
|
|
1549
|
+
Authorization: `Bearer ${token}`,
|
|
1550
|
+
"Content-Type": `multipart/related; boundary=${MULTIPART_BOUNDARY}`,
|
|
1551
|
+
"Content-Length": String(body.byteLength),
|
|
1552
|
+
},
|
|
1553
|
+
body: new Uint8Array(body),
|
|
1554
|
+
});
|
|
1555
|
+
if (!res.ok) {
|
|
1556
|
+
throw new Error(`googleDrive: revision restore upload failed (${res.status}): ${await res.text()}`);
|
|
1557
|
+
}
|
|
1558
|
+
const head = (await res.json());
|
|
1559
|
+
return { fileId, restoredFromRevisionId: revisionId, head };
|
|
1560
|
+
},
|
|
1561
|
+
});
|
|
1562
|
+
// ─── Changes feed ───────────────────────────────────────────────
|
|
1563
|
+
//
|
|
1564
|
+
// Drive's change feed. Without these the agent has to poll comment.list
|
|
1565
|
+
// per file. With them, a sensor can wake on any file change in the user's
|
|
1566
|
+
// corpus or in a specific shared drive.
|
|
1567
|
+
rl.registerAction("changes.getStartPageToken", {
|
|
1568
|
+
description: "Get the current Drive change-feed start page token. Use as the seed `pageToken` for the first `changes.list` call.",
|
|
1569
|
+
inputSchema: {
|
|
1570
|
+
driveId: { type: "string", required: false, description: "Shared-drive id; omit for the user's My Drive corpus." },
|
|
1571
|
+
supportsAllDrives: { type: "boolean", required: false, default: true },
|
|
1572
|
+
},
|
|
1573
|
+
async execute(input, ctx) {
|
|
1574
|
+
const p = (input ?? {});
|
|
1575
|
+
return driveRequest(ctx, "GET", `/drive/v3/changes/startPageToken`, undefined, {
|
|
1576
|
+
driveId: p.driveId,
|
|
1577
|
+
supportsAllDrives: p.supportsAllDrives ?? true,
|
|
1578
|
+
});
|
|
1579
|
+
},
|
|
1580
|
+
});
|
|
1581
|
+
rl.registerAction("changes.list", {
|
|
1582
|
+
description: "List changes to files and Shared Drives since the given `pageToken`. Returns `{ changes, newStartPageToken, nextPageToken? }`. Drive does not surface comment-level changes here — use this for file metadata/content changes; pair with comments.list for review activity.",
|
|
1583
|
+
inputSchema: {
|
|
1584
|
+
pageToken: { type: "string", required: true, description: "Token from `changes.getStartPageToken` or a prior `nextPageToken`." },
|
|
1585
|
+
driveId: { type: "string", required: false },
|
|
1586
|
+
spaces: { type: "string", required: false, description: "drive | photos | appDataFolder. Default drive." },
|
|
1587
|
+
includeRemoved: { type: "boolean", required: false },
|
|
1588
|
+
includeItemsFromAllDrives: { type: "boolean", required: false, default: true },
|
|
1589
|
+
supportsAllDrives: { type: "boolean", required: false, default: true },
|
|
1590
|
+
restrictToMyDrive: { type: "boolean", required: false },
|
|
1591
|
+
pageSize: { type: "number", required: false },
|
|
1592
|
+
fields: { type: "string", required: false },
|
|
1593
|
+
},
|
|
1594
|
+
async execute(input, ctx) {
|
|
1595
|
+
const p = (input ?? {});
|
|
1596
|
+
return driveRequest(ctx, "GET", `/drive/v3/changes`, undefined, {
|
|
1597
|
+
pageToken: p.pageToken,
|
|
1598
|
+
driveId: p.driveId,
|
|
1599
|
+
spaces: p.spaces,
|
|
1600
|
+
includeRemoved: p.includeRemoved,
|
|
1601
|
+
includeItemsFromAllDrives: p.includeItemsFromAllDrives ?? true,
|
|
1602
|
+
supportsAllDrives: p.supportsAllDrives ?? true,
|
|
1603
|
+
restrictToMyDrive: p.restrictToMyDrive,
|
|
1604
|
+
pageSize: p.pageSize ?? 100,
|
|
1605
|
+
fields: p.fields ??
|
|
1606
|
+
"kind,nextPageToken,newStartPageToken,changes(kind,removed,fileId,driveId,changeType,time,file(id,name,mimeType,modifiedTime,trashed,parents))",
|
|
1607
|
+
});
|
|
1608
|
+
},
|
|
1609
|
+
});
|
|
1610
|
+
rl.registerAction("changes.watch", {
|
|
1611
|
+
description: "Subscribe to push notifications on the change feed. Drive will POST to `address` whenever a change in this corpus is recorded. Returns a channel resource; pair with `channels.stop` (`changes.stop`) when done.",
|
|
1612
|
+
inputSchema: {
|
|
1613
|
+
pageToken: { type: "string", required: true },
|
|
1614
|
+
address: { type: "string", required: true, description: "HTTPS URL Drive will POST notifications to." },
|
|
1615
|
+
channelId: { type: "string", required: false, description: "Caller-chosen UUID. Auto-generated when omitted." },
|
|
1616
|
+
token: { type: "string", required: false, description: "Optional opaque token Drive echoes on each delivery." },
|
|
1617
|
+
expiration: { type: "number", required: false, description: "Unix ms at which Drive should expire the channel. Defaults ~1 hour." },
|
|
1618
|
+
driveId: { type: "string", required: false },
|
|
1619
|
+
supportsAllDrives: { type: "boolean", required: false, default: true },
|
|
1620
|
+
includeItemsFromAllDrives: { type: "boolean", required: false, default: true },
|
|
1621
|
+
},
|
|
1622
|
+
async execute(input, ctx) {
|
|
1623
|
+
const p = (input ?? {});
|
|
1624
|
+
const body = {
|
|
1625
|
+
id: p.channelId ?? uuid(),
|
|
1626
|
+
type: "web_hook",
|
|
1627
|
+
address: p.address,
|
|
1628
|
+
};
|
|
1629
|
+
if (p.token)
|
|
1630
|
+
body.token = p.token;
|
|
1631
|
+
if (p.expiration)
|
|
1632
|
+
body.expiration = String(p.expiration);
|
|
1633
|
+
return driveRequest(ctx, "POST", `/drive/v3/changes/watch`, body, {
|
|
1634
|
+
pageToken: p.pageToken,
|
|
1635
|
+
driveId: p.driveId,
|
|
1636
|
+
supportsAllDrives: p.supportsAllDrives ?? true,
|
|
1637
|
+
includeItemsFromAllDrives: p.includeItemsFromAllDrives ?? true,
|
|
1638
|
+
});
|
|
1639
|
+
},
|
|
1640
|
+
});
|
|
1641
|
+
rl.registerAction("changes.stop", {
|
|
1642
|
+
description: "Stop a previously-subscribed change channel. Pass the same `channelId` and `resourceId` returned by `changes.watch`.",
|
|
1643
|
+
inputSchema: {
|
|
1644
|
+
channelId: { type: "string", required: true },
|
|
1645
|
+
resourceId: { type: "string", required: true },
|
|
1646
|
+
},
|
|
1647
|
+
async execute(input, ctx) {
|
|
1648
|
+
const p = (input ?? {});
|
|
1649
|
+
await driveRequest(ctx, "POST", `/drive/v3/channels/stop`, { id: p.channelId, resourceId: p.resourceId });
|
|
1650
|
+
return { success: true };
|
|
1651
|
+
},
|
|
1652
|
+
});
|
|
1653
|
+
// ─── Permission update ──────────────────────────────────────────
|
|
1654
|
+
//
|
|
1655
|
+
// Patch an existing permission. The bundled `file.share` creates a new
|
|
1656
|
+
// permission, and `file.deletePermission` removes one — but to promote a
|
|
1657
|
+
// commenter to writer (or expire a permission) without re-sharing, you
|
|
1658
|
+
// need the PATCH endpoint.
|
|
1659
|
+
rl.registerAction("permission.update", {
|
|
1660
|
+
description: "Patch an existing file/folder permission. Use to change the role on an existing share, set an expiration time, or transfer ownership.",
|
|
1661
|
+
inputSchema: {
|
|
1662
|
+
fileId: { type: "string", required: true },
|
|
1663
|
+
permissionId: { type: "string", required: true },
|
|
1664
|
+
role: { type: "string", required: false, description: "owner | organizer | fileOrganizer | writer | commenter | reader" },
|
|
1665
|
+
expirationTime: { type: "string", required: false, description: "RFC 3339; only valid for writer/commenter/reader on My Drive files." },
|
|
1666
|
+
transferOwnership: { type: "boolean", required: false },
|
|
1667
|
+
removeExpiration: { type: "boolean", required: false },
|
|
1668
|
+
useDomainAdminAccess: { type: "boolean", required: false },
|
|
1669
|
+
supportsAllDrives: { type: "boolean", required: false, default: true },
|
|
1670
|
+
},
|
|
1671
|
+
async execute(input, ctx) {
|
|
1672
|
+
const p = (input ?? {});
|
|
1673
|
+
const body = {};
|
|
1674
|
+
if (p.role)
|
|
1675
|
+
body.role = p.role;
|
|
1676
|
+
if (p.expirationTime)
|
|
1677
|
+
body.expirationTime = p.expirationTime;
|
|
1678
|
+
const qs = { supportsAllDrives: p.supportsAllDrives ?? true };
|
|
1679
|
+
if (p.transferOwnership)
|
|
1680
|
+
qs.transferOwnership = true;
|
|
1681
|
+
if (p.removeExpiration)
|
|
1682
|
+
qs.removeExpiration = true;
|
|
1683
|
+
if (p.useDomainAdminAccess)
|
|
1684
|
+
qs.useDomainAdminAccess = true;
|
|
1685
|
+
if (Object.keys(body).length === 0 && !qs.removeExpiration) {
|
|
1686
|
+
throw new Error("googleDrive.permission.update: pass at least one of role / expirationTime / removeExpiration.");
|
|
1687
|
+
}
|
|
1688
|
+
return driveRequest(ctx, "PATCH", `/drive/v3/files/${p.fileId}/permissions/${p.permissionId}`, body, qs);
|
|
1689
|
+
},
|
|
1690
|
+
});
|
|
1691
|
+
// ─── Access proposals ───────────────────────────────────────────
|
|
1692
|
+
//
|
|
1693
|
+
// Drive's "Request access" flow. When a user clicks "Request access" on a
|
|
1694
|
+
// file they can't open, Drive records an access proposal which the file's
|
|
1695
|
+
// owner can resolve (accept and grant, or deny). These actions let the
|
|
1696
|
+
// agent triage and resolve those requests programmatically.
|
|
1697
|
+
rl.registerAction("accessProposal.list", {
|
|
1698
|
+
description: "List access proposals (Request-access entries) on a file.",
|
|
1699
|
+
inputSchema: {
|
|
1700
|
+
fileId: { type: "string", required: true },
|
|
1701
|
+
pageSize: { type: "number", required: false },
|
|
1702
|
+
},
|
|
1703
|
+
async execute(input, ctx) {
|
|
1704
|
+
const p = (input ?? {});
|
|
1705
|
+
const out = [];
|
|
1706
|
+
let pageToken;
|
|
1707
|
+
do {
|
|
1708
|
+
const page = (await driveRequest(ctx, "GET", `/drive/v3/files/${p.fileId}/accessproposals`, undefined, { pageSize: p.pageSize ?? 100, pageToken }));
|
|
1709
|
+
if (Array.isArray(page.accessProposals))
|
|
1710
|
+
out.push(...page.accessProposals);
|
|
1711
|
+
pageToken = page.nextPageToken;
|
|
1712
|
+
} while (pageToken);
|
|
1713
|
+
return { fileId: p.fileId, count: out.length, accessProposals: out };
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
rl.registerAction("accessProposal.resolve", {
|
|
1717
|
+
description: "Resolve an access proposal. `action` is one of 'ACCEPT' or 'DENY'. When accepting, pass `role` (default 'reader') to grant.",
|
|
1718
|
+
inputSchema: {
|
|
1719
|
+
fileId: { type: "string", required: true },
|
|
1720
|
+
proposalId: { type: "string", required: true },
|
|
1721
|
+
action: { type: "string", required: true, description: "'ACCEPT' | 'DENY'" },
|
|
1722
|
+
role: { type: "string", required: false, description: "Role to grant on ACCEPT. Default 'reader'." },
|
|
1723
|
+
view: { type: "string", required: false, description: "Optional Drive scope view (e.g. 'published')." },
|
|
1724
|
+
sendNotification: { type: "boolean", required: false },
|
|
1725
|
+
},
|
|
1726
|
+
async execute(input, ctx) {
|
|
1727
|
+
const p = (input ?? {});
|
|
1728
|
+
const body = { action: p.action };
|
|
1729
|
+
if (p.action === "ACCEPT") {
|
|
1730
|
+
body.role = [p.role ?? "reader"];
|
|
1731
|
+
if (p.view)
|
|
1732
|
+
body.view = p.view;
|
|
1733
|
+
}
|
|
1734
|
+
if (p.sendNotification !== undefined)
|
|
1735
|
+
body.sendNotification = p.sendNotification;
|
|
1736
|
+
return driveRequest(ctx, "POST", `/drive/v3/files/${p.fileId}/accessproposals/${p.proposalId}:resolve`, body);
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
// ─── About ───────────────────────────────────────────────────────
|
|
1740
|
+
rl.registerAction("about.get", {
|
|
1741
|
+
description: "Current user info: storage quota, export formats, max upload size, importable mime types. Useful for healthchecks and conversion planning.",
|
|
1742
|
+
inputSchema: {
|
|
1743
|
+
fields: { type: "string", required: false },
|
|
1744
|
+
},
|
|
1745
|
+
async execute(input, ctx) {
|
|
1746
|
+
const p = (input ?? {});
|
|
1747
|
+
return driveRequest(ctx, "GET", `/drive/v3/about`, undefined, {
|
|
1748
|
+
fields: p.fields ??
|
|
1749
|
+
"user(displayName,emailAddress,permissionId,photoLink),storageQuota,maxUploadSize,canCreateDrives,exportFormats,importFormats",
|
|
1750
|
+
});
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
// ─── File export (ergonomic wrapper) ────────────────────────────
|
|
1754
|
+
rl.registerAction("file.export", {
|
|
1755
|
+
description: "Export a Google-native file (Doc/Sheet/Slide/Drawing/Form) to a non-native mimeType. Wrapper around the export endpoint; pass `savePath` to write to disk and get the path back, otherwise returns base64.",
|
|
1756
|
+
inputSchema: {
|
|
1757
|
+
fileId: { type: "string", required: true },
|
|
1758
|
+
mimeType: {
|
|
1759
|
+
type: "string",
|
|
1760
|
+
required: true,
|
|
1761
|
+
description: "Target export mime, e.g. application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/pdf, text/plain, text/csv.",
|
|
1762
|
+
},
|
|
1763
|
+
savePath: { type: "string", required: false },
|
|
1764
|
+
},
|
|
1765
|
+
async execute(input, ctx) {
|
|
1766
|
+
const p = (input ?? {});
|
|
1767
|
+
const token = await accessToken(ctx);
|
|
1768
|
+
const u = new URL(`${API_BASE}/drive/v3/files/${p.fileId}/export`);
|
|
1769
|
+
u.searchParams.set("mimeType", p.mimeType);
|
|
1770
|
+
const res = await fetch(u.toString(), { headers: { Authorization: `Bearer ${token}` } });
|
|
1771
|
+
if (!res.ok) {
|
|
1772
|
+
throw new Error(`googleDrive: export failed (${res.status}): ${await res.text()}`);
|
|
1773
|
+
}
|
|
1774
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
1775
|
+
const contentType = res.headers.get("content-type") ?? p.mimeType;
|
|
1776
|
+
if (typeof p.savePath === "string") {
|
|
1777
|
+
writeFileSync(p.savePath, bytes);
|
|
1778
|
+
return { path: p.savePath, mimeType: contentType, size: bytes.byteLength };
|
|
1779
|
+
}
|
|
1780
|
+
return { mimeType: contentType, size: bytes.byteLength, contentBase64: bytes.toString("base64") };
|
|
1781
|
+
},
|
|
1782
|
+
});
|
|
1783
|
+
// ─── Raw file.list ──────────────────────────────────────────────
|
|
1784
|
+
rl.registerAction("file.list", {
|
|
1785
|
+
description: "Raw Drive `files.list`. Pass Drive search-syntax `q` and any combination of corpora / driveId / spaces / orderBy. `fileFolder.search` is the friendlier wrapper; reach for `file.list` only when you need the unwrapped surface.",
|
|
1786
|
+
inputSchema: {
|
|
1787
|
+
q: { type: "string", required: false },
|
|
1788
|
+
corpora: { type: "string", required: false, description: "user | drive | allDrives" },
|
|
1789
|
+
driveId: { type: "string", required: false },
|
|
1790
|
+
spaces: { type: "string", required: false },
|
|
1791
|
+
orderBy: { type: "string", required: false },
|
|
1792
|
+
pageSize: { type: "number", required: false },
|
|
1793
|
+
pageToken: { type: "string", required: false },
|
|
1794
|
+
includeItemsFromAllDrives: { type: "boolean", required: false, default: true },
|
|
1795
|
+
supportsAllDrives: { type: "boolean", required: false, default: true },
|
|
1796
|
+
fields: { type: "string", required: false },
|
|
1797
|
+
returnAll: { type: "boolean", required: false, description: "If true, follows pageToken until exhausted and returns the concatenated file list." },
|
|
1798
|
+
},
|
|
1799
|
+
async execute(input, ctx) {
|
|
1800
|
+
const p = (input ?? {});
|
|
1801
|
+
const baseQs = {
|
|
1802
|
+
q: p.q,
|
|
1803
|
+
corpora: p.corpora,
|
|
1804
|
+
driveId: p.driveId,
|
|
1805
|
+
spaces: p.spaces,
|
|
1806
|
+
orderBy: p.orderBy,
|
|
1807
|
+
pageSize: p.pageSize ?? 100,
|
|
1808
|
+
includeItemsFromAllDrives: p.includeItemsFromAllDrives ?? true,
|
|
1809
|
+
supportsAllDrives: p.supportsAllDrives ?? true,
|
|
1810
|
+
fields: p.fields ?? "kind,nextPageToken,files(id,name,mimeType,parents,modifiedTime,size,owners(emailAddress),driveId,webViewLink)",
|
|
1811
|
+
};
|
|
1812
|
+
if (p.returnAll) {
|
|
1813
|
+
return paginateAll(ctx, "/drive/v3/files", "files", baseQs);
|
|
1814
|
+
}
|
|
1815
|
+
return driveRequest(ctx, "GET", `/drive/v3/files`, undefined, { ...baseQs, pageToken: p.pageToken });
|
|
1816
|
+
},
|
|
1817
|
+
});
|
|
1149
1818
|
}
|