Add Google integrations and documentation

This commit is contained in:
Stefano Fiorini
2026-03-08 21:29:37 -05:00
parent 976888f002
commit 7361b31c7c
8 changed files with 1641 additions and 1 deletions

View File

@@ -0,0 +1,248 @@
#!/usr/bin/env node
/*
Google Workspace helper CLI
Commands:
whoami
send --to <email> --subject <text> --body <text> [--html]
search-mail --query <gmail query> [--max 10]
search-calendar --query <text> [--max 10] [--timeMin ISO] [--timeMax ISO] [--calendar primary]
create-event --summary <text> --start <ISO> --end <ISO> [--timeZone America/Chicago] [--description <text>] [--location <text>] [--calendar primary]
*/
const fs = require('fs');
const path = require('path');
const { google } = require('googleapis');
const DEFAULT_SUBJECT = process.env.GW_IMPERSONATE || 'stefano@fiorinis.com';
const DEFAULT_KEY_CANDIDATES = [
process.env.GW_KEY_PATH,
path.join(process.env.HOME || '', '.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json'),
path.join(process.env.HOME || '', '.clawdbot/credentials/google-workspace/service-account.json'),
].filter(Boolean);
const SCOPES = [
'https://www.googleapis.com/auth/gmail.send',
'https://www.googleapis.com/auth/gmail.compose',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
];
function resolveKeyPath() {
for (const p of DEFAULT_KEY_CANDIDATES) {
if (p && fs.existsSync(p)) return p;
}
return null;
}
function parseArgs(argv) {
const out = { _: [] };
for (let i = 0; i < argv.length; i++) {
const token = argv[i];
if (token.startsWith('--')) {
const key = token.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith('--')) {
out[key] = true;
} else {
out[key] = next;
i++;
}
} else {
out._.push(token);
}
}
return out;
}
function usage() {
console.log(`Google Workspace CLI\n
Env (optional):
GW_IMPERSONATE user to impersonate (default: ${DEFAULT_SUBJECT})
GW_KEY_PATH service-account key path
Commands:
whoami
send --to <email> --subject <text> --body <text> [--html]
search-mail --query <gmail query> [--max 10]
search-calendar --query <text> [--max 10] [--timeMin ISO] [--timeMax ISO] [--calendar primary]
create-event --summary <text> --start <ISO> --end <ISO> [--timeZone America/Chicago] [--description <text>] [--location <text>] [--calendar primary]
`);
}
function assertRequired(opts, required) {
const missing = required.filter((k) => !opts[k]);
if (missing.length) {
throw new Error(`Missing required options: ${missing.map((m) => `--${m}`).join(', ')}`);
}
}
function makeRawEmail({ from, to, subject, body, isHtml = false }) {
const contentType = isHtml ? 'text/html; charset="UTF-8"' : 'text/plain; charset="UTF-8"';
const msg = [
`From: ${from}`,
`To: ${to}`,
`Subject: ${subject}`,
'MIME-Version: 1.0',
`Content-Type: ${contentType}`,
'',
body,
].join('\r\n');
return Buffer.from(msg)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
async function getClients() {
const keyPath = resolveKeyPath();
if (!keyPath) {
throw new Error('Service account key not found. Set GW_KEY_PATH or place the file in ~/.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json');
}
const auth = new google.auth.GoogleAuth({
keyFile: keyPath,
scopes: SCOPES,
clientOptions: { subject: DEFAULT_SUBJECT },
});
const authClient = await auth.getClient();
const gmail = google.gmail({ version: 'v1', auth: authClient });
const calendar = google.calendar({ version: 'v3', auth: authClient });
return { gmail, calendar, keyPath };
}
async function cmdWhoami(clients) {
const profile = await clients.gmail.users.getProfile({ userId: 'me' });
console.log(JSON.stringify({
impersonating: DEFAULT_SUBJECT,
keyPath: clients.keyPath,
profile: profile.data,
}, null, 2));
}
async function cmdSend(clients, opts) {
assertRequired(opts, ['to', 'subject', 'body']);
const raw = makeRawEmail({
from: DEFAULT_SUBJECT,
to: opts.to,
subject: opts.subject,
body: opts.body,
isHtml: !!opts.html,
});
const res = await clients.gmail.users.messages.send({
userId: 'me',
requestBody: { raw },
});
console.log(JSON.stringify({ ok: true, id: res.data.id, threadId: res.data.threadId }, null, 2));
}
async function cmdSearchMail(clients, opts) {
assertRequired(opts, ['query']);
const maxResults = Math.max(1, Math.min(50, Number(opts.max || 10)));
const list = await clients.gmail.users.messages.list({
userId: 'me',
q: opts.query,
maxResults,
});
const ids = (list.data.messages || []).map((m) => m.id).filter(Boolean);
const out = [];
for (const id of ids) {
const msg = await clients.gmail.users.messages.get({
userId: 'me',
id,
format: 'metadata',
metadataHeaders: ['From', 'To', 'Subject', 'Date'],
});
const headers = Object.fromEntries((msg.data.payload?.headers || []).map((h) => [h.name, h.value]));
out.push({
id,
threadId: msg.data.threadId,
snippet: msg.data.snippet,
from: headers.From,
to: headers.To,
subject: headers.Subject,
date: headers.Date,
});
}
console.log(JSON.stringify({ count: out.length, messages: out }, null, 2));
}
async function cmdSearchCalendar(clients, opts) {
const maxResults = Math.max(1, Math.min(50, Number(opts.max || 10)));
const res = await clients.calendar.events.list({
calendarId: opts.calendar || 'primary',
q: opts.query || undefined,
timeMin: opts.timeMin,
timeMax: opts.timeMax,
singleEvents: true,
orderBy: 'startTime',
maxResults,
});
const events = (res.data.items || []).map((e) => ({
id: e.id,
summary: e.summary,
status: e.status,
start: e.start,
end: e.end,
location: e.location,
hangoutLink: e.hangoutLink,
}));
console.log(JSON.stringify({ count: events.length, events }, null, 2));
}
async function cmdCreateEvent(clients, opts) {
assertRequired(opts, ['summary', 'start', 'end']);
const tz = opts.timeZone || 'America/Chicago';
const event = {
summary: opts.summary,
description: opts.description,
location: opts.location,
start: { dateTime: opts.start, timeZone: tz },
end: { dateTime: opts.end, timeZone: tz },
};
const res = await clients.calendar.events.insert({
calendarId: opts.calendar || 'primary',
requestBody: event,
});
console.log(JSON.stringify({ ok: true, id: res.data.id, htmlLink: res.data.htmlLink }, null, 2));
}
(async function main() {
try {
const args = parseArgs(process.argv.slice(2));
const cmd = args._[0];
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
usage();
process.exit(0);
}
const clients = await getClients();
if (cmd === 'whoami') return await cmdWhoami(clients);
if (cmd === 'send') return await cmdSend(clients, args);
if (cmd === 'search-mail') return await cmdSearchMail(clients, args);
if (cmd === 'search-calendar') return await cmdSearchCalendar(clients, args);
if (cmd === 'create-event') return await cmdCreateEvent(clients, args);
throw new Error(`Unknown command: ${cmd}`);
} catch (err) {
console.error(`ERROR: ${err.message}`);
process.exit(1);
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
{
"name": "google-workspace",
"version": "1.0.0",
"description": "",
"main": "gw.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"googleapis": "^171.4.0"
}
}