diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..74f184f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,31 @@ +# claude-code-conduit + +Supervised action bridge between Claude Code (in a Docker container) and the host system. Claude sends structured action requests; the server applies per-action policies and optionally holds them for human approval via a TUI queue manager. + +## Quick orientation + +- **Server** runs on the host (`bin/ccc-server.mjs`), listens on port 3015 +- **Client** runs in the container (`bin/ccc-client.mjs`), sends signed JSON action requests +- **Actions** are defined in `server/actions.mjs` — this is the main place to add new capabilities +- **Auth** is HMAC-SHA256; secrets live in a JSON file, never env vars +- **Policies**: `auto-accept` (runs immediately), `queue` (held for human approval), `auto-deny` + +## Adding a new action + +Edit `server/actions.mjs`. Each action needs `description`, `params`, `policy`, and `handler`. The handler receives `(params, ctx)` where ctx has `{ caller, users, mail_perm_store, exec, mailer_send }`. + +See [`claude-info/architecture.md`](claude-info/architecture.md) for full details. +See [`claude-info/contributing.md`](claude-info/contributing.md) for the checklist when adding features. + +## Active / planned work + +- [`claude-info/plan-google-calendar.md`](claude-info/plan-google-calendar.md) — Google Calendar integration ✓ implemented + +## Running + +```bash +ccc-server --config config.json +ccc-client '{"action": "list-actions"}' +``` + +Shell helpers (`ccc-open-terminal`, `ccc-edit-file`, `ccc-open-browser`) are available in the container via `~/.bashrc`. diff --git a/README.md b/README.md index 2502009..a541c64 100644 --- a/README.md +++ b/README.md @@ -167,9 +167,42 @@ Built-in actions: | `send-email` | auto-accept | `to`, `subject`, `body`, `topic` | | `set-mail-permission` | auto-accept | `target_user`, `to`, `allow` (bool), `topic` (optional — omit to match any topic) | | `get-mail-permissions` | auto-accept | `target_user` (optional) | +| `calendar-list-events` | auto-accept | `calendar_id` (default `primary`), `time_min`, `time_max`, `max_results` | +| `calendar-create-event` | queue | `summary`, `start`, `end` (ISO 8601), `description`, `calendar_id` | +| `calendar-update-event` | queue | `event_id`, `calendar_id`, `summary`, `start`, `end`, `description` | +| `calendar-delete-event` | queue | `event_id`, `calendar_id` | `send-email` checks that the caller has a mail permission entry matching `(caller, to, topic)` before sending. Permissions are managed via `set-mail-permission`, which requires the caller to have `canApprove` over the target user — so only humans can grant/revoke permissions for agents. +Calendar mutation actions (`create`, `update`, `delete`) are `queue` policy — they require human approval in the TUI before executing. Listing is `auto-accept`. + +### Google Calendar setup + +Google Calendar support requires OAuth2 credentials from Google Cloud Console. + +**One-time setup:** + +1. Create a project at [Google Cloud Console](https://console.cloud.google.com), enable the Calendar API +2. Create an OAuth2 credential (Desktop app type) and download the JSON file +3. Run the auth helper to do the consent flow: + +```bash +ccc-gcal-auth --credentials google-credentials.json --token google-token.json +``` + +This opens an authorization URL in the terminal. Open it in your browser, grant access, and the token is written automatically. + +4. Add `google_calendar` to your `config.json`: + +```json +"google_calendar": { + "credentials": "../google-credentials.json", + "token": "../google-token.json" +} +``` + +Paths are resolved relative to the config file. Tokens are refreshed automatically and written back to the token file. + ### Adding actions Edit `server/actions.mjs`. Each entry needs: @@ -180,7 +213,7 @@ Edit `server/actions.mjs`. Each entry needs: params: [{ name: 'foo', required: true, type: 'string' }], policy: 'auto-accept', // or 'auto-deny' | 'queue' handler: ({ foo }, ctx) => { - // ctx = { caller, users, mail_perm_store, mailer_send } + // ctx = { caller, users, mail_perm_store, exec, mailer_send, calendar } // ctx is optional — omit the second argument if you don't need it return { result: foo }; }, diff --git a/bin/ccc-gcal-auth.mjs b/bin/ccc-gcal-auth.mjs new file mode 100644 index 0000000..4ede10e --- /dev/null +++ b/bin/ccc-gcal-auth.mjs @@ -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 --token '); + 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'); diff --git a/claude-info/architecture.md b/claude-info/architecture.md new file mode 100644 index 0000000..60f529d --- /dev/null +++ b/claude-info/architecture.md @@ -0,0 +1,113 @@ +# Architecture + +## File layout + +``` +server/ + index.mjs — Express app, routing, policy enforcement + actions.mjs — Action registry (add new actions here) + auth.mjs — HMAC-SHA256 middleware, canApprove checks + config.mjs — Loads config.json + secrets.json + helpers.mjs — exec(), path resolution, volume mapping + queue.mjs — In-memory queue for policy=queue actions + mail_perms.mjs — Persistent mail permission store (JSON file) + mailer.mjs — Nodemailer wrapper + +bin/ + ccc-server.mjs — Server entry point + ccc-client.mjs — CLI client (used by agent) + ccc-queue.mjs — TUI queue manager (used by human on host) + ccc-keygen.mjs — Secret generation / filtering utility +``` + +## Request lifecycle + +1. Client signs request with HMAC-SHA256 (includes timestamp; replays rejected after 30s) +2. `auth.mjs` middleware validates signature, sets `req.conduit_user` +3. `POST /action` looks up the action definition in `actions.mjs` +4. Validates required params +5. Applies policy: + - `auto-accept` → runs handler immediately, returns result + - `queue` → enqueues, returns `{ status: 'queued', id }`; human approves/denies via TUI + - `auto-deny` → rejects immediately + +## Action handler context (`ctx`) + +```js +{ + caller, // string — authenticated username + users, // object — full users map from secrets.json + mail_perm_store, // MailPermStore instance + exec, // (bin, args) => void — runs a host process (no-op in --dry-run) + mailer_send, // async (to, subject, body) => void + calendar, // Google_Calendar_Client | null — null if not configured +} +``` + +New modules are instantiated in `index.mjs` and added to `make_ctx()`. + +## Config file (`config.json`) + +```json +{ + "secrets": "../secrets.json", // required — path to users/secrets + "mail_perms": "mail-perms.json", // optional — persists mail permissions + "smtp": { ... }, // optional — enables send-email action + "bind": "127.0.0.1", + "port": 3015 +} +``` + +All relative paths are resolved from the config file's directory. + +## Secrets file (`secrets.json`) + +```json +{ + "users": { + "agent": { "secret": "", "canApprove": [] }, + "user": { "secret": "", "canApprove": ["agent"] } + } +} +``` + +`canApprove` lists whose queued actions a user may approve/deny. + +## Path resolution + +Container paths are translated to host paths via `VOLUME_MAPPING` in `server/helpers.mjs`. Paths outside known volumes are rejected. Default mapping: + +| Container | Host | +|-----------|------| +| `/workspace` | `/workspace` | +| `/home/claude` | `/claude-home` | + +Edit `CONTAINER_PATH` and `VOLUME_MAPPING` in `helpers.mjs` to match the local setup. + +## Adding a new action + +```js +// server/actions.mjs +'my-action': { + description: 'What it does', + params: [ + { name: 'foo', required: true, type: 'string' }, + { name: 'bar', required: false, type: 'path' }, + ], + policy: 'auto-accept', // or 'queue' | 'auto-deny' + handler: ({ foo, bar }, ctx) => { + // ctx is optional — omit second arg if unused + return { result: foo }; + }, +}, +``` + +## Adding a new injectable service (like mailer or calendar) + +1. Create `server/my_service.mjs` exporting a factory function +2. Extend `load_config` in `server/config.mjs` to read and return the new config block +3. Instantiate the service in `index.mjs` using the config values +4. Add it to the `make_ctx()` return object +5. Use it in action handlers via `ctx.my_service` + +See [`contributing.md`](contributing.md) for the full checklist when adding any feature. diff --git a/claude-info/contributing.md b/claude-info/contributing.md new file mode 100644 index 0000000..442634c --- /dev/null +++ b/claude-info/contributing.md @@ -0,0 +1,53 @@ +# Contributing checklist + +Use this when adding any non-trivial feature to CCC. Go through every section — skipping docs or config is a common mistake. + +--- + +## Adding a new action (simple — no new service) + +- [ ] Add the action definition to `server/actions.mjs` +- [ ] Update the actions table in `README.md` +- [ ] If the action has special behavior, add a note under the table +- [ ] Update `claude-info/architecture.md` if ctx shape changes + +## Adding a new injectable service (e.g. a new API integration) + +### Code + +- [ ] Create `server/.mjs` with a factory function `create_(cfg)` +- [ ] Extend `server/config.mjs` to read and return the new config block (resolve paths relative to config dir) +- [ ] Import and instantiate in `server/index.mjs`; handle missing config gracefully (warn, set to null) +- [ ] Add the service to the `make_ctx()` return object in `index.mjs` +- [ ] Add actions to `server/actions.mjs`; guard with `if (!ctx.) throw new Error(' not configured')` +- [ ] If setup requires a one-time manual step (OAuth, keygen, etc.) add a bin script in `bin/` +- [ ] Add new dependencies to `package.json` and add bin scripts to the `bin` map + +### Config + +- [ ] Add the new config block to `config.example.json` + +### Documentation + +- [ ] Update `README.md`: + - Add new actions to the actions table + - Add a setup section explaining prerequisites and one-time steps + - Update the `ctx` comment in "Adding actions" if ctx changed +- [ ] Update `claude-info/architecture.md`: + - Add the new service to the ctx shape block + - Note any new files in the file layout +- [ ] Update or add a plan file in `claude-info/` — mark status as **implemented** +- [ ] Update `CLAUDE.md` — mark the plan entry as done + +### Git + +- [ ] `git status` — read every line before staging +- [ ] Stage files explicitly by name (never `git add -A` or `git add .`) +- [ ] Bump version in `package.json` if this is a meaningful release +- [ ] Commit with a clear message describing what was added +- [ ] Check git identity is set correctly for this repo (`git config user.name`) + +### After commit + +- [ ] Tell the user to run `npm install` if new dependencies were added +- [ ] Tell the user about any one-time setup steps required before the feature works diff --git a/claude-info/plan-google-calendar.md b/claude-info/plan-google-calendar.md new file mode 100644 index 0000000..2c07dcc --- /dev/null +++ b/claude-info/plan-google-calendar.md @@ -0,0 +1,126 @@ +# Plan: Google Calendar integration + +Status: **implemented** + +## Overview + +Add Google Calendar read/write support as CCC actions. Follows the same pattern as the SMTP/mail integration: credentials in config, a wrapper module, new actions in `actions.mjs`. + +Listing events is `auto-accept` (read-only). All mutations are `queue` — require human approval in the TUI before executing. + +--- + +## Step 1 — Google OAuth2 setup (one-time manual) + +1. Create a Google Cloud project, enable the Calendar API +2. Create an OAuth2 credential (Desktop app type), download the JSON +3. Run `ccc-gcal-auth` (see Step 7) to do the consent flow and write `google-token.json` +4. Point config at both files (see Step 2) + +--- + +## Step 2 — Config extension + +Add to `config.json` (and `config.example.json`): + +```json +"google_calendar": { + "credentials": "../google-credentials.json", + "token": "../google-token.json" +} +``` + +Both paths resolved relative to the config file, same as `secrets` and `mail_perms`. + +--- + +## Step 3 — New module: `server/google_calendar.mjs` + +Wraps the `googleapis` npm package. Exports: + +```js +export function create_calendar_client(cfg) +``` + +- Loads `cfg.credentials_path` and `cfg.token_path` +- Creates an OAuth2 client +- Registers `tokens` event to persist refreshed tokens back to disk automatically +- Returns a thin wrapper with methods: + - `list_events({ calendar_id, time_min, time_max, max_results })` + - `create_event({ calendar_id, summary, start, end, description })` + - `update_event({ calendar_id, event_id, summary, start, end, description })` + - `delete_event({ calendar_id, event_id })` + +`start` and `end` are ISO 8601 strings (e.g. `"2026-05-21T14:00:00+02:00"`). + +--- + +## Step 4 — Config loader update (`server/config.mjs`) + +Extend `load_config` to resolve `google_calendar.credentials` and `google_calendar.token` paths and return them (or `null` if the block is absent). + +--- + +## Step 5 — Server wiring (`server/index.mjs`) + +```js +import { create_calendar_client } from './google_calendar.mjs'; + +const calendar = cfg.google_calendar + ? create_calendar_client(cfg.google_calendar) + : null; + +// In make_ctx(): +return { caller, users, mail_perm_store, exec, mailer_send, calendar }; +``` + +--- + +## Step 6 — New actions (`server/actions.mjs`) + +| Action | Policy | Required params | Optional params | +|---|---|---|---| +| `calendar-list-events` | `auto-accept` | — | `calendar_id` (default `primary`), `time_min`, `time_max`, `max_results` | +| `calendar-create-event` | `queue` | `summary`, `start`, `end` | `calendar_id`, `description` | +| `calendar-update-event` | `queue` | `event_id` | `calendar_id`, `summary`, `start`, `end`, `description` | +| `calendar-delete-event` | `queue` | `event_id` | `calendar_id` | + +All handlers guard with: +```js +if (!ctx.calendar) { throw new Error('Google Calendar not configured'); } +``` + +--- + +## Step 7 — Auth helper: `bin/ccc-gcal-auth.mjs` + +One-time setup script run on the host: + +```bash +ccc-gcal-auth --credentials google-credentials.json --token google-token.json +``` + +- Reads the downloaded OAuth credentials JSON +- Generates an authorization URL, prints it for the user to open +- Starts a local HTTP server to receive the redirect with the auth code +- Exchanges the code for tokens and writes `google-token.json` + +Add to `bin` map in `package.json`. + +--- + +## Step 8 — Dependencies + +```bash +npm install googleapis +``` + +Add to `package.json` dependencies. + +--- + +## Open questions + +- Token storage: separate `google-token.json` (current plan) vs folded into `secrets.json`? +- Should `calendar-create-event` be `queue` or `auto-accept`? (Currently `queue` for safety) +- Multi-calendar support needed beyond `primary`? diff --git a/config.example.json b/config.example.json index 3f93834..9364a26 100644 --- a/config.example.json +++ b/config.example.json @@ -8,6 +8,10 @@ "auth": { "user": "relay@example.com", "pass": "" }, "from": "agent@example.com" }, + "google_calendar": { + "credentials": "../google-credentials.json", + "token": "../google-token.json" + }, "bind": "127.0.0.1", "port": 3015 } diff --git a/package.json b/package.json index c99faac..7143fa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-conduit", - "version": "1.1.0", + "version": "1.2.0", "description": "A supervised action bridge between Claude Code and the host system", "type": "module", "scripts": { @@ -11,11 +11,13 @@ "ccc-server": "bin/ccc-server.mjs", "ccc-client": "bin/ccc-client.mjs", "ccc-queue": "bin/ccc-queue.mjs", - "ccc-keygen": "bin/ccc-keygen.mjs" + "ccc-keygen": "bin/ccc-keygen.mjs", + "ccc-gcal-auth": "bin/ccc-gcal-auth.mjs" }, "dependencies": { "blessed": "^0.1.81", "express": "^5.2.1", + "googleapis": "^144.0.0", "nodemailer": "^8.0.2" } } diff --git a/server/actions.mjs b/server/actions.mjs index f9496f8..331fe8e 100644 --- a/server/actions.mjs +++ b/server/actions.mjs @@ -120,4 +120,68 @@ export const actions = { return { permissions: target_user ? all.filter(e => e.user === target_user) : all }; }, }, + + 'calendar-list-events': { + description: 'List upcoming calendar events', + params: [ + { name: 'calendar_id', required: false, type: 'string' }, + { name: 'time_min', required: false, type: 'string' }, + { name: 'time_max', required: false, type: 'string' }, + { name: 'max_results', required: false, type: 'number' }, + ], + policy: 'auto-accept', + handler: async ({ calendar_id, time_min, time_max, max_results }, { calendar }) => { + if (!calendar) { throw new Error('Google Calendar not configured'); } + const events = await calendar.list_events({ calendar_id, time_min, time_max, max_results }); + return { events }; + }, + }, + + 'calendar-create-event': { + description: 'Create a new calendar event', + params: [ + { name: 'summary', required: true, type: 'string' }, + { name: 'start', required: true, type: 'string' }, + { name: 'end', required: true, type: 'string' }, + { name: 'description', required: false, type: 'string' }, + { name: 'calendar_id', required: false, type: 'string' }, + ], + policy: 'queue', + handler: async ({ summary, start, end, description, calendar_id }, { calendar }) => { + if (!calendar) { throw new Error('Google Calendar not configured'); } + const event = await calendar.create_event({ calendar_id, summary, start, end, description }); + return { event }; + }, + }, + + 'calendar-update-event': { + description: 'Update fields on an existing calendar event', + params: [ + { name: 'event_id', required: true, type: 'string' }, + { name: 'calendar_id', required: false, type: 'string' }, + { name: 'summary', required: false, type: 'string' }, + { name: 'start', required: false, type: 'string' }, + { name: 'end', required: false, type: 'string' }, + { name: 'description', required: false, type: 'string' }, + ], + policy: 'queue', + handler: async ({ event_id, calendar_id, summary, start, end, description }, { calendar }) => { + if (!calendar) { throw new Error('Google Calendar not configured'); } + const event = await calendar.update_event({ calendar_id, event_id, summary, start, end, description }); + return { event }; + }, + }, + + 'calendar-delete-event': { + description: 'Delete a calendar event', + params: [ + { name: 'event_id', required: true, type: 'string' }, + { name: 'calendar_id', required: false, type: 'string' }, + ], + policy: 'queue', + handler: async ({ event_id, calendar_id }, { calendar }) => { + if (!calendar) { throw new Error('Google Calendar not configured'); } + return calendar.delete_event({ calendar_id, event_id }); + }, + }, }; diff --git a/server/config.mjs b/server/config.mjs index 90e3041..36deadc 100644 --- a/server/config.mjs +++ b/server/config.mjs @@ -46,10 +46,23 @@ export function load_config(file_path) { ? resolve(config_dir, parsed.mail_perms) : null; + let google_calendar = null; + if (parsed.google_calendar) { + const { credentials, token } = parsed.google_calendar; + if (!credentials || !token) { + throw new Error("Config 'google_calendar' must have 'credentials' and 'token' fields"); + } + google_calendar = { + credentials_path: resolve(config_dir, credentials), + token_path: resolve(config_dir, token), + }; + } + return { users: secrets.users, smtp: parsed.smtp ?? null, mail_perms_path, + google_calendar, bind: parsed.bind ?? null, port: parsed.port ?? null, }; diff --git a/server/google_calendar.mjs b/server/google_calendar.mjs new file mode 100644 index 0000000..dcd538a --- /dev/null +++ b/server/google_calendar.mjs @@ -0,0 +1,76 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { google } from 'googleapis'; + +// Returns a calendar client wrapping the Google Calendar API v3. +// cfg = { credentials_path, token_path } +export function create_calendar_client({ credentials_path, token_path }) { + const credentials = JSON.parse(readFileSync(credentials_path, 'utf8')); + const { client_id, client_secret, redirect_uris } = credentials.installed ?? credentials.web; + + const oauth2_client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + + const token = JSON.parse(readFileSync(token_path, 'utf8')); + oauth2_client.setCredentials(token); + + // Persist refreshed tokens automatically + oauth2_client.on('tokens', (new_tokens) => { + let current = {}; + try { + current = JSON.parse(readFileSync(token_path, 'utf8')); + } catch (_) {} + const merged = { ...current, ...new_tokens }; + writeFileSync(token_path, JSON.stringify(merged, null, '\t') + '\n', 'utf8'); + }); + + const cal = google.calendar({ version: 'v3', auth: oauth2_client }); + + async function list_events({ calendar_id = 'primary', time_min, time_max, max_results = 10 } = {}) { + const res = await cal.events.list({ + calendarId: calendar_id, + timeMin: time_min, + timeMax: time_max, + maxResults: max_results, + singleEvents: true, + orderBy: 'startTime', + }); + return res.data.items ?? []; + } + + async function create_event({ calendar_id = 'primary', summary, start, end, description }) { + const res = await cal.events.insert({ + calendarId: calendar_id, + requestBody: { + summary, + description, + start: { dateTime: start }, + end: { dateTime: end }, + }, + }); + return res.data; + } + + async function update_event({ calendar_id = 'primary', event_id, summary, start, end, description }) { + const patch = {}; + if (summary !== undefined) { patch.summary = summary; } + if (description !== undefined) { patch.description = description; } + if (start !== undefined) { patch.start = { dateTime: start }; } + if (end !== undefined) { patch.end = { dateTime: end }; } + + const res = await cal.events.patch({ + calendarId: calendar_id, + eventId: event_id, + requestBody: patch, + }); + return res.data; + } + + async function delete_event({ calendar_id = 'primary', event_id }) { + await cal.events.delete({ + calendarId: calendar_id, + eventId: event_id, + }); + return { deleted: true, event_id }; + } + + return { list_events, create_event, update_event, delete_event }; +} diff --git a/server/index.mjs b/server/index.mjs index f6f0287..df8da9b 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -6,6 +6,7 @@ import { create_auth_middleware, check_can_approve } from './auth.mjs'; import { create_mailer } from './mailer.mjs'; import { exec as real_exec } from './helpers.mjs'; import { load_mail_perms } from './mail_perms.mjs'; +import { create_calendar_client } from './google_calendar.mjs'; function get_arg(argv, flag) { const i = argv.indexOf(flag); @@ -29,7 +30,7 @@ try { process.exit(1); } -const { users, smtp, mail_perms_path } = cfg; +const { users, smtp, mail_perms_path, google_calendar: gcal_cfg } = cfg; const PORT = process.env.CONDUIT_PORT || cfg.port || 3015; const BIND = process.env.CONDUIT_BIND || cfg.bind || '127.0.0.1'; @@ -47,6 +48,16 @@ if (!mail_perms_path) { const real_mailer_send = create_mailer(smtp); +let calendar = null; +if (gcal_cfg) { + try { + calendar = create_calendar_client(gcal_cfg); + console.log('Google Calendar client initialized'); + } catch (err) { + console.error(`Warning: Failed to initialize Google Calendar client: ${err.message}`); + } +} + const app = express(); app.use(express.json({ verify: (req, _res, buf) => { @@ -70,7 +81,7 @@ function make_ctx(caller) { const mailer_send = DRY_RUN ? async (to, subject) => console.log(`[${ts()}] [DRY-RUN] send-mail: to=${to} subject=${JSON.stringify(subject)}`) : real_mailer_send; - return { caller, users, mail_perm_store, exec, mailer_send }; + return { caller, users, mail_perm_store, exec, mailer_send, calendar }; } async function run_action(def, params, ctx) {