Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ RUN ARCH="$(dpkg --print-architecture)" \
# Install pnpm globally
RUN npm install -g pnpm

# Install gogcli (Google Workspace CLI for Gmail/Calendar/Drive)
# OpenClaw has a built-in 'gog' skill that wraps this CLI
ENV GOG_VERSION=0.9.0
RUN ARCH="$(dpkg --print-architecture)" \
&& curl -fsSL "https://github.com/steipete/gogcli/releases/download/v${GOG_VERSION}/gogcli_${GOG_VERSION}_linux_${ARCH}.tar.gz" -o /tmp/gogcli.tar.gz \
&& tar -xzf /tmp/gogcli.tar.gz -C /usr/local/bin gog \
&& rm /tmp/gogcli.tar.gz \
&& gog --version

# Install OpenClaw (formerly clawdbot/moltbot)
# Pin to specific version for reproducible builds
RUN npm install -g openclaw@2026.2.3 \
Expand All @@ -32,7 +41,7 @@ RUN mkdir -p /root/.openclaw \
&& mkdir -p /root/clawd/skills

# Copy startup script
# Build cache bust: 2026-02-06-v29-sync-workspace
# Build cache bust: 2026-02-09-v34-gogcli-auth-fix
COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh
RUN chmod +x /usr/local/bin/start-openclaw.sh

Expand Down
55 changes: 55 additions & 0 deletions skills/google-workspace/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
name: google-workspace
description: Access Gmail and Google Calendar via Google APIs. Search/read/send email and list/create calendar events. Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN env vars.
---

# Google Workspace

Access Gmail and Google Calendar from the container via Google APIs with OAuth2 authentication.

## Prerequisites

- `GOOGLE_CLIENT_ID` environment variable set
- `GOOGLE_CLIENT_SECRET` environment variable set
- `GOOGLE_REFRESH_TOKEN` environment variable set

## Quick Start

### Search Gmail
```bash
node /root/clawd/skills/google-workspace/scripts/gmail-search.js "from:someone@example.com" --max 10
```

### Read an email
```bash
node /root/clawd/skills/google-workspace/scripts/gmail-read.js <messageId>
```

### Send an email
```bash
node /root/clawd/skills/google-workspace/scripts/gmail-send.js --to user@example.com --subject "Hello" --body "Message body"
```

### List calendar events
```bash
node /root/clawd/skills/google-workspace/scripts/calendar-events.js primary --from 2026-02-09 --to 2026-02-10
```

### Create a calendar event
```bash
node /root/clawd/skills/google-workspace/scripts/calendar-create.js primary --summary "Meeting" --start "2026-02-10T10:00:00" --end "2026-02-10T11:00:00"
```

## Available Scripts

| Script | Purpose |
|--------|---------|
| `gmail-search.js` | Search Gmail messages by query |
| `gmail-read.js` | Read full content of a single email |
| `gmail-send.js` | Send an email |
| `calendar-events.js` | List calendar events in a date range |
| `calendar-create.js` | Create a new calendar event |

## Output Format

Gmail search and calendar events output TSV (tab-separated values) for easy parsing, matching the format used by gogcli.
80 changes: 80 additions & 0 deletions skills/google-workspace/scripts/calendar-create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node
/**
* Calendar Create Event
*
* Usage: node calendar-create.js <calendarId> --summary <text> --start <datetime> --end <datetime> [--description <text>] [--location <text>]
*
* Creates a new calendar event.
*
* calendarId: 'primary' for the user's main calendar, or a specific calendar ID
* Datetimes: ISO 8601 format (e.g., 2026-02-10T10:00:00)
*/

const { getCalendar } = require('./google-auth');

function parseArgs(args) {
const result = { calendarId: 'primary' };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--summary' && args[i + 1]) {
result.summary = args[++i];
} else if (args[i] === '--start' && args[i + 1]) {
result.start = args[++i];
} else if (args[i] === '--end' && args[i + 1]) {
result.end = args[++i];
} else if (args[i] === '--description' && args[i + 1]) {
result.description = args[++i];
} else if (args[i] === '--location' && args[i + 1]) {
result.location = args[++i];
} else if (!args[i].startsWith('--')) {
result.calendarId = args[i];
}
}
return result;
}

function toEventDateTime(dateStr) {
if (!dateStr) return undefined;
// All-day event: just a date
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
return { date: dateStr };
}
// Datetime event
let dt = dateStr;
if (!dt.includes('Z') && !dt.includes('+') && !dt.includes('-', 10)) {
dt += ':00'; // Ensure seconds
}
return { dateTime: dt, timeZone: 'America/Los_Angeles' };
}

async function main() {
const opts = parseArgs(process.argv.slice(2));

if (!opts.summary || !opts.start || !opts.end) {
console.error('Usage: node calendar-create.js <calendarId> --summary <text> --start <datetime> --end <datetime> [--description <text>] [--location <text>]');
process.exit(1);
}

const calendar = getCalendar();

const event = {
summary: opts.summary,
start: toEventDateTime(opts.start),
end: toEventDateTime(opts.end),
};

if (opts.description) event.description = opts.description;
if (opts.location) event.location = opts.location;

const res = await calendar.events.insert({
calendarId: opts.calendarId,
requestBody: event,
});

console.log(`Created event: ${res.data.id}`);
console.log(`Link: ${res.data.htmlLink}`);
}

main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
81 changes: 81 additions & 0 deletions skills/google-workspace/scripts/calendar-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* Calendar Events
*
* Usage: node calendar-events.js <calendarId> --from <date> --to <date> [--max N]
*
* Lists calendar events using events.list API.
* Outputs: ID, start, end, summary (TSV format)
*
* calendarId: 'primary' for the user's main calendar, or a specific calendar ID
* Dates: YYYY-MM-DD or ISO 8601 datetime
*/

const { getCalendar } = require('./google-auth');

function parseArgs(args) {
const result = { calendarId: 'primary', maxResults: 50 };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--from' && args[i + 1]) {
result.from = args[++i];
} else if (args[i] === '--to' && args[i + 1]) {
result.to = args[++i];
} else if (args[i] === '--max' && args[i + 1]) {
result.maxResults = parseInt(args[++i], 10);
} else if (!args[i].startsWith('--')) {
result.calendarId = args[i];
}
}
return result;
}

function toRFC3339(dateStr) {
if (!dateStr) return undefined;
// If it's just a date (YYYY-MM-DD), append time
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
return dateStr + 'T00:00:00Z';
}
// If no timezone info, assume UTC
if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) {
return dateStr + 'Z';
}
return dateStr;
}

async function main() {
const opts = parseArgs(process.argv.slice(2));

const calendar = getCalendar();

const params = {
calendarId: opts.calendarId,
maxResults: opts.maxResults,
singleEvents: true,
orderBy: 'startTime',
};

if (opts.from) params.timeMin = toRFC3339(opts.from);
if (opts.to) params.timeMax = toRFC3339(opts.to);

const res = await calendar.events.list(params);
const events = res.data.items || [];

if (events.length === 0) {
console.log('No events found.');
return;
}

console.log('ID\tStart\tEnd\tSummary');

for (const event of events) {
const start = event.start?.dateTime || event.start?.date || '';
const end = event.end?.dateTime || event.end?.date || '';
const summary = event.summary || '(no title)';
console.log(`${event.id}\t${start}\t${end}\t${summary}`);
}
}

main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
86 changes: 86 additions & 0 deletions skills/google-workspace/scripts/gmail-read.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Gmail Read
*
* Usage: node gmail-read.js <messageId>
*
* Reads a single email's full content.
*/

const { getGmail } = require('./google-auth');

function decodeBody(body) {
if (!body?.data) return '';
return Buffer.from(body.data, 'base64url').toString('utf-8');
}

function extractText(payload) {
if (!payload) return '';

// Simple text/plain or text/html body
if (payload.mimeType === 'text/plain' && payload.body?.data) {
return decodeBody(payload.body);
}

// Multipart: recurse through parts
if (payload.parts) {
// Prefer text/plain
for (const part of payload.parts) {
if (part.mimeType === 'text/plain' && part.body?.data) {
return decodeBody(part.body);
}
}
// Fall back to text/html
for (const part of payload.parts) {
if (part.mimeType === 'text/html' && part.body?.data) {
return decodeBody(part.body);
}
}
// Recurse into nested multipart
for (const part of payload.parts) {
const text = extractText(part);
if (text) return text;
}
}

// Fallback: decode whatever body is there
if (payload.body?.data) {
return decodeBody(payload.body);
}

return '';
}

async function main() {
const messageId = process.argv[2];
if (!messageId) {
console.error('Usage: node gmail-read.js <messageId>');
process.exit(1);
}

const gmail = getGmail();

const res = await gmail.users.messages.get({
userId: 'me',
id: messageId,
format: 'full',
});

const headers = res.data.payload?.headers || [];
const getHeader = (name) => headers.find(h => h.name === name)?.value || '';

console.log(`From: ${getHeader('From')}`);
console.log(`To: ${getHeader('To')}`);
console.log(`Date: ${getHeader('Date')}`);
console.log(`Subject: ${getHeader('Subject')}`);
console.log(`Labels: ${(res.data.labelIds || []).join(', ')}`);
console.log('---');

const body = extractText(res.data.payload);
console.log(body || '(no text content)');
}

main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
67 changes: 67 additions & 0 deletions skills/google-workspace/scripts/gmail-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node
/**
* Gmail Search
*
* Usage: node gmail-search.js <query> [--max N]
*
* Searches Gmail using the users.messages.list + get API.
* Outputs: ID, date, from, subject, labels (TSV format)
*/

const { getGmail } = require('./google-auth');

async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: node gmail-search.js <query> [--max N]');
process.exit(1);
}

let query = '';
let maxResults = 20;

for (let i = 0; i < args.length; i++) {
if (args[i] === '--max' && args[i + 1]) {
maxResults = parseInt(args[i + 1], 10);
i++;
} else {
query += (query ? ' ' : '') + args[i];
}
}

const gmail = getGmail();

const listRes = await gmail.users.messages.list({
userId: 'me',
q: query,
maxResults,
});

const messages = listRes.data.messages || [];
if (messages.length === 0) {
console.log('No messages found.');
return;
}

console.log('ID\tDate\tFrom\tSubject\tLabels');

for (const msg of messages) {
const detail = await gmail.users.messages.get({
userId: 'me',
id: msg.id,
format: 'metadata',
metadataHeaders: ['From', 'Subject', 'Date'],
});

const headers = detail.data.payload?.headers || [];
const getHeader = (name) => headers.find(h => h.name === name)?.value || '';
const labels = (detail.data.labelIds || []).join(',');

console.log(`${msg.id}\t${getHeader('Date')}\t${getHeader('From')}\t${getHeader('Subject')}\t${labels}`);
}
}

main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
Loading