Add Google Calendar integration and Claude docs
- New server/google_calendar.mjs wrapping googleapis v3 with auto token refresh - Four new actions: calendar-list-events (auto-accept), calendar-create/update/delete-event (queue) - bin/ccc-gcal-auth.mjs one-time OAuth2 consent flow helper - config.example.json updated with google_calendar block - server/config.mjs, index.mjs wired up following the same pattern as SMTP/mailer - Bump version to 1.2.0 - Add CLAUDE.md and claude-info/ with architecture reference, feature plans, and contributing checklist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
86
bin/ccc-gcal-auth.mjs
Normal file
86
bin/ccc-gcal-auth.mjs
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
// One-time OAuth2 authorization flow for Google Calendar.
|
||||
// Run this on the host to generate a token file for the server.
|
||||
//
|
||||
// Usage:
|
||||
// ccc-gcal-auth --credentials google-credentials.json --token google-token.json
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { google } from 'googleapis';
|
||||
|
||||
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
|
||||
const PORT = 3016;
|
||||
const REDIRECT_URI = `http://localhost:${PORT}/callback`;
|
||||
|
||||
function get_arg(argv, flag) {
|
||||
const i = argv.indexOf(flag);
|
||||
return i !== -1 ? argv[i + 1] : null;
|
||||
}
|
||||
|
||||
const credentials_path = get_arg(process.argv, '--credentials');
|
||||
const token_path = get_arg(process.argv, '--token');
|
||||
|
||||
if (!credentials_path || !token_path) {
|
||||
console.error('Usage: ccc-gcal-auth --credentials <path> --token <output-path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let credentials;
|
||||
try {
|
||||
credentials = JSON.parse(readFileSync(credentials_path, 'utf8'));
|
||||
} catch (err) {
|
||||
console.error(`Cannot read credentials file: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { client_id, client_secret } = credentials.installed ?? credentials.web;
|
||||
const oauth2_client = new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
|
||||
|
||||
const auth_url = oauth2_client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: SCOPES,
|
||||
prompt: 'consent',
|
||||
});
|
||||
|
||||
console.log('Open this URL in your browser to authorize Google Calendar access:\n');
|
||||
console.log(auth_url);
|
||||
console.log(`\nListening for redirect on http://localhost:${PORT}/callback ...`);
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
} catch (_) {
|
||||
res.end('Bad request');
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname !== '/callback') {
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const code = url.searchParams.get('code');
|
||||
if (!code) {
|
||||
res.writeHead(400);
|
||||
res.end('Missing code parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { tokens } = await oauth2_client.getToken(code);
|
||||
writeFileSync(token_path, JSON.stringify(tokens, null, '\t') + '\n', 'utf8');
|
||||
res.end('Authorization successful. You may close this tab.');
|
||||
console.log(`\nToken written to ${token_path}`);
|
||||
server.close();
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
res.end(`Error: ${err.message}`);
|
||||
console.error(`Token exchange failed: ${err.message}`);
|
||||
server.close();
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1');
|
||||
Reference in New Issue
Block a user