feat(tools): add Google Docs, Drive, and Tasks read-only tools

Add three new Google service integrations following the established
Gmail/GCal pattern:

- Google Docs (docs.list, docs.search, docs.read): list, search, and
  read document content as plain text via Docs + Drive APIs
- Google Drive (drive.list, drive.search, drive.read): list, search,
  and read files with export support for Workspace files (Docs→text,
  Sheets→CSV, Slides→text)
- Google Tasks (tasks.lists, tasks.list): list task lists and tasks
  with status, due dates, and notes

Each service has its own config section, OAuth auth command, tool
policy group, and test suite (53 new tests). The setup wizard now
offers to configure all Google services together and run OAuth auth
flows automatically after saving config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-10 12:59:15 -08:00
parent 411c6d84a2
commit f204ff1dd7
20 changed files with 2844 additions and 15 deletions
+130 -12
View File
@@ -1,6 +1,61 @@
import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
const GOOGLE_SETUP_INSTRUCTIONS = [
' To set up Google API access:',
' 1. Go to https://console.cloud.google.com → create or select a project',
' 2. Enable the required APIs (see below)',
' 3. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID',
' 4. Choose "Desktop app", download the JSON file',
' 5. Save it as ~/.config/flynn/gmail-credentials.json (or a path of your choice)',
];
interface GoogleService {
name: string;
configKey: string;
apis: string[];
authCmd: string;
setter: (builder: ConfigBuilder, creds: string) => void;
}
const GOOGLE_SERVICES: GoogleService[] = [
{
name: 'Gmail',
configKey: 'gmail',
apis: ['Gmail API'],
authCmd: 'gmail-auth',
setter: (b, c) => b.setGmailEnabled(c, 'webchat', 'gmail'),
},
{
name: 'Google Calendar',
configKey: 'gcal',
apis: ['Google Calendar API'],
authCmd: 'gcal-auth',
setter: (b, c) => b.setGcalEnabled(c),
},
{
name: 'Google Docs',
configKey: 'gdocs',
apis: ['Google Docs API', 'Google Drive API'],
authCmd: 'gdocs-auth',
setter: (b, c) => b.setGdocsEnabled(c),
},
{
name: 'Google Drive',
configKey: 'gdrive',
apis: ['Google Drive API'],
authCmd: 'gdrive-auth',
setter: (b, c) => b.setGdriveEnabled(c),
},
{
name: 'Google Tasks',
configKey: 'gtasks',
apis: ['Google Tasks API'],
authCmd: 'gtasks-auth',
setter: (b, c) => b.setGtasksEnabled(c),
},
];
export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Promise<void> {
const cron = await p.confirm('Enable cron scheduler?', false);
if (cron) {
@@ -17,17 +72,80 @@ export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Prom
p.println('✓ Webhooks enabled — define triggers in config.yaml under automation.webhooks[]');
}
const gmail = await p.confirm('Enable Gmail watcher?', false);
if (gmail) {
p.println(' To set up Gmail access:');
p.println(' 1. Go to https://console.cloud.google.com → create or select a project');
p.println(' 2. Enable the Gmail API and Cloud Pub/Sub API');
p.println(' 3. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID');
p.println(' 4. Choose "Desktop app", download the JSON file');
p.println(' 5. Save it as ~/.config/flynn/gmail-credentials.json');
p.println(' On first run, Flynn will open a browser for OAuth consent and save the token.');
const creds = await p.ask('OAuth credentials file', '~/.config/flynn/gmail-credentials.json');
builder.setGmailEnabled(creds, 'webchat', 'gmail');
p.println('✓ Gmail watcher enabled');
// Google services
const wantGoogle = await p.confirm('Configure Google services (Gmail, Calendar, Docs, Drive, Tasks)?', false);
if (!wantGoogle) return;
p.println();
for (const line of GOOGLE_SETUP_INSTRUCTIONS) {
p.println(line);
}
p.println();
const creds = await p.ask('OAuth credentials file', '~/.config/flynn/gmail-credentials.json');
p.println();
const enabledServices: GoogleService[] = [];
for (const service of GOOGLE_SERVICES) {
const enable = await p.confirm(`Enable ${service.name}?`, false);
if (enable) {
service.setter(builder, creds);
const apis = service.apis.join(', ');
p.println(`${service.name} enabled (requires: ${apis})`);
enabledServices.push(service);
}
}
if (enabledServices.length > 0) {
p.println();
p.println('After saving config, authenticate each service:');
for (const svc of enabledServices) {
p.println(` flynn ${svc.authCmd}`);
}
}
}
/**
* Run OAuth auth flows for enabled Google services.
* Called after config is saved so the auth commands can read it.
*/
export async function runGoogleAuth(p: Prompter, config: Record<string, any>): Promise<void> {
const automation = config.automation as Record<string, any> | undefined;
if (!automation) return;
const pending: { name: string; authCmd: string }[] = [];
for (const svc of GOOGLE_SERVICES) {
const svcConfig = automation[svc.configKey] as { enabled?: boolean } | undefined;
if (svcConfig?.enabled) {
pending.push({ name: svc.name, authCmd: svc.authCmd });
}
}
if (pending.length === 0) return;
p.println();
const runAuth = await p.confirm(`Run OAuth authentication for ${pending.map(s => s.name).join(', ')}?`, true);
if (!runAuth) {
p.println('Skipped. Run these commands later:');
for (const svc of pending) {
p.println(` flynn ${svc.authCmd}`);
}
return;
}
const { execFileSync } = await import('child_process');
for (const svc of pending) {
p.println();
p.println(`Authenticating ${svc.name}...`);
try {
execFileSync(process.execPath, [process.argv[1], svc.authCmd], {
stdio: 'inherit',
});
p.println(`${svc.name} authenticated`);
} catch {
p.println(`${svc.name} auth failed — run "flynn ${svc.authCmd}" manually`);
}
}
}
+24
View File
@@ -125,6 +125,30 @@ export class ConfigBuilder {
this.config.automation = automation;
}
setGcalEnabled(credentialsFile: string): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
automation.gcal = { enabled: true, credentials_file: credentialsFile };
this.config.automation = automation;
}
setGdocsEnabled(credentialsFile: string): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
automation.gdocs = { enabled: true, credentials_file: credentialsFile };
this.config.automation = automation;
}
setGdriveEnabled(credentialsFile: string): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
automation.gdrive = { enabled: true, credentials_file: credentialsFile };
this.config.automation = automation;
}
setGtasksEnabled(credentialsFile: string): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
automation.gtasks = { enabled: true, credentials_file: credentialsFile };
this.config.automation = automation;
}
setCronEnabled(): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
if (!automation.cron) automation.cron = [];
+4
View File
@@ -25,6 +25,10 @@ export function renderSummary(config: Record<string, any>): string {
if (auto.cron?.length > 0) autoFeatures.push(`${auto.cron.length} cron jobs`);
if (auto.webhooks?.length > 0) autoFeatures.push('webhooks');
if (auto.gmail?.enabled) autoFeatures.push('gmail');
if (auto.gcal?.enabled) autoFeatures.push('gcal');
if (auto.gdocs?.enabled) autoFeatures.push('gdocs');
if (auto.gdrive?.enabled) autoFeatures.push('gdrive');
if (auto.gtasks?.enabled) autoFeatures.push('gtasks');
if (auto.heartbeat?.enabled) autoFeatures.push('heartbeat');
lines.push(` Automation: ${autoFeatures.join(', ') || 'none'}`);