flake-update-20260505
1#!/usr/bin/env node
2
3import {
4 DEFAULT_VERSIONS,
5 authorize,
6 formatScopes,
7 getGoogleApis,
8} from "./common.js";
9
10function printHelp() {
11 console.log(`Google Workspace API helper
12
13Usage:
14 node workspace.js call <service> <method.path> [params-json] [--version vX] [--scopes s1,s2]
15 node workspace.js calendar-today [calendarId]
16 node workspace.js calendar-upcoming [days] [calendarId]
17 node workspace.js drive-search <query>
18 node workspace.js gmail-search <query>
19 node workspace.js gmail-read <messageId>
20
21Examples:
22 node workspace.js call drive files.list '{"pageSize":5,"fields":"files(id,name)"}'
23 node workspace.js call calendar events.list '{"calendarId":"primary","maxResults":10,"singleEvents":true,"orderBy":"startTime"}'
24 node workspace.js drive-search "name contains 'Roadmap' and trashed=false"
25 node workspace.js gmail-search "from:alice@example.com newer_than:7d"
26 node workspace.js gmail-read 18e1234abcd5678
27 node workspace.js calendar-upcoming 3
28`);
29}
30
31function parseOptions(argv) {
32 const positional = [];
33 const options = {
34 version: undefined,
35 scopes: undefined,
36 };
37
38 for (let i = 0; i < argv.length; i += 1) {
39 const arg = argv[i];
40 if (arg === "--version") {
41 options.version = argv[i + 1];
42 i += 1;
43 continue;
44 }
45 if (arg === "--scopes") {
46 options.scopes = formatScopes(argv[i + 1]);
47 i += 1;
48 continue;
49 }
50 positional.push(arg);
51 }
52
53 return { positional, options };
54}
55
56function resolveMethod(root, methodPath) {
57 const parts = methodPath.split(".").filter(Boolean);
58 if (parts.length === 0) {
59 throw new Error("method.path is empty");
60 }
61
62 let parent = root;
63 for (let i = 0; i < parts.length - 1; i += 1) {
64 parent = parent?.[parts[i]];
65 if (!parent) {
66 throw new Error(`Invalid method path (missing segment: ${parts[i]})`);
67 }
68 }
69
70 const methodName = parts[parts.length - 1];
71 const method = parent?.[methodName];
72
73 if (typeof method !== "function") {
74 throw new Error(
75 `Invalid method path: ${methodPath}. Final segment is not callable.`
76 );
77 }
78
79 return { parent, method };
80}
81
82function parseJsonObject(raw, label) {
83 if (!raw) {
84 return {};
85 }
86
87 let parsed;
88 try {
89 parsed = JSON.parse(raw);
90 } catch (error) {
91 throw new Error(`${label} is not valid JSON: ${error.message}`);
92 }
93
94 if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
95 throw new Error(`${label} must be a JSON object`);
96 }
97
98 return parsed;
99}
100
101async function callApi({ service, methodPath, params, version, scopes }) {
102 const google = getGoogleApis();
103 const factory = google[service];
104 if (typeof factory !== "function") {
105 throw new Error(`Unknown Google API service: ${service}`);
106 }
107
108 const auth = await authorize({
109 interactive: true,
110 scopes: scopes && scopes.length > 0 ? scopes : undefined,
111 });
112
113 const api = factory({
114 version: version || DEFAULT_VERSIONS[service] || "v1",
115 auth,
116 });
117
118 const { parent, method } = resolveMethod(api, methodPath);
119 const response = await method.call(parent, params);
120 return response?.data ?? response;
121}
122
123// --- Convenience commands ---
124
125async function cmdCall(args, options) {
126 const [service, methodPath, paramsRaw] = args;
127
128 if (!service || !methodPath) {
129 throw new Error("Usage: call <service> <method.path> [params-json]");
130 }
131
132 const params = parseJsonObject(paramsRaw, "params-json");
133 const data = await callApi({
134 service,
135 methodPath,
136 params,
137 version: options.version,
138 scopes: options.scopes,
139 });
140
141 console.log(JSON.stringify(data, null, 2));
142}
143
144async function cmdCalendarToday(args, options) {
145 const calendarId = args[0] || "primary";
146
147 const start = new Date();
148 start.setHours(0, 0, 0, 0);
149
150 const end = new Date(start);
151 end.setDate(end.getDate() + 1);
152
153 const data = await callApi({
154 service: "calendar",
155 methodPath: "events.list",
156 version: options.version,
157 scopes: options.scopes,
158 params: {
159 calendarId,
160 timeMin: start.toISOString(),
161 timeMax: end.toISOString(),
162 singleEvents: true,
163 orderBy: "startTime",
164 },
165 });
166
167 console.log(JSON.stringify(data, null, 2));
168}
169
170async function cmdCalendarUpcoming(args, options) {
171 const days = parseInt(args[0], 10) || 7;
172 const calendarId = args[1] || "primary";
173
174 const start = new Date();
175 const end = new Date(start);
176 end.setDate(end.getDate() + days);
177
178 const data = await callApi({
179 service: "calendar",
180 methodPath: "events.list",
181 version: options.version,
182 scopes: options.scopes,
183 params: {
184 calendarId,
185 timeMin: start.toISOString(),
186 timeMax: end.toISOString(),
187 singleEvents: true,
188 orderBy: "startTime",
189 },
190 });
191
192 console.log(JSON.stringify(data, null, 2));
193}
194
195async function cmdDriveSearch(args, options) {
196 const query = args.join(" ").trim();
197 if (!query) {
198 throw new Error("Usage: drive-search <query>");
199 }
200
201 const data = await callApi({
202 service: "drive",
203 methodPath: "files.list",
204 version: options.version,
205 scopes: options.scopes,
206 params: {
207 q: query,
208 pageSize: 20,
209 fields:
210 "nextPageToken, files(id,name,mimeType,modifiedTime,webViewLink)",
211 },
212 });
213
214 console.log(JSON.stringify(data, null, 2));
215}
216
217async function cmdGmailSearch(args, options) {
218 const query = args.join(" ").trim();
219 if (!query) {
220 throw new Error("Usage: gmail-search <query>");
221 }
222
223 const list = await callApi({
224 service: "gmail",
225 methodPath: "users.messages.list",
226 version: options.version,
227 scopes: options.scopes,
228 params: {
229 userId: "me",
230 q: query,
231 maxResults: 20,
232 },
233 });
234
235 const messages = list.messages || [];
236 const details = [];
237
238 for (const message of messages.slice(0, 10)) {
239 const full = await callApi({
240 service: "gmail",
241 methodPath: "users.messages.get",
242 version: options.version,
243 scopes: options.scopes,
244 params: {
245 userId: "me",
246 id: message.id,
247 format: "metadata",
248 metadataHeaders: ["From", "To", "Subject", "Date"],
249 },
250 });
251
252 details.push({
253 id: message.id,
254 threadId: message.threadId,
255 snippet: full.snippet,
256 payload: full.payload,
257 });
258 }
259
260 console.log(
261 JSON.stringify(
262 {
263 resultCount: messages.length,
264 messages: details,
265 },
266 null,
267 2
268 )
269 );
270}
271
272async function cmdGmailRead(args, options) {
273 const messageId = args[0];
274 if (!messageId) {
275 throw new Error("Usage: gmail-read <messageId>");
276 }
277
278 const data = await callApi({
279 service: "gmail",
280 methodPath: "users.messages.get",
281 version: options.version,
282 scopes: options.scopes,
283 params: {
284 userId: "me",
285 id: messageId,
286 format: "full",
287 },
288 });
289
290 // Extract headers
291 const headers = {};
292 if (data.payload && data.payload.headers) {
293 for (const h of data.payload.headers) {
294 headers[h.name] = h.value;
295 }
296 }
297
298 // Extract body text
299 let body = "";
300 function extractText(part) {
301 if (part.mimeType === "text/plain" && part.body && part.body.data) {
302 body += Buffer.from(part.body.data, "base64url").toString("utf8");
303 }
304 if (part.parts) {
305 for (const p of part.parts) {
306 extractText(p);
307 }
308 }
309 }
310
311 if (data.payload) {
312 extractText(data.payload);
313 }
314
315 console.log(
316 JSON.stringify(
317 {
318 id: data.id,
319 threadId: data.threadId,
320 from: headers["From"],
321 to: headers["To"],
322 subject: headers["Subject"],
323 date: headers["Date"],
324 snippet: data.snippet,
325 body: body || "(no plain text body)",
326 },
327 null,
328 2
329 )
330 );
331}
332
333// --- Main ---
334
335async function main() {
336 const { positional, options } = parseOptions(process.argv.slice(2));
337 const [command, ...args] = positional;
338
339 if (
340 !command ||
341 command === "help" ||
342 command === "--help" ||
343 command === "-h"
344 ) {
345 printHelp();
346 return;
347 }
348
349 if (command === "call") {
350 await cmdCall(args, options);
351 return;
352 }
353
354 if (command === "calendar-today") {
355 await cmdCalendarToday(args, options);
356 return;
357 }
358
359 if (command === "calendar-upcoming") {
360 await cmdCalendarUpcoming(args, options);
361 return;
362 }
363
364 if (command === "drive-search") {
365 await cmdDriveSearch(args, options);
366 return;
367 }
368
369 if (command === "gmail-search") {
370 await cmdGmailSearch(args, options);
371 return;
372 }
373
374 if (command === "gmail-read") {
375 await cmdGmailRead(args, options);
376 return;
377 }
378
379 throw new Error(`Unknown command: ${command}`);
380}
381
382main().catch((error) => {
383 console.error(`❌ ${error.message}`);
384 process.exit(1);
385});