Skip to content

Commit 7c173e1

Browse files
plcclaude
andcommitted
Address user feedback: GET /man, email_sent, SMTP test, webhook on create
Feedback-driven improvements: - Convert POST /man to GET /man (cacheable, browser-friendly) - Add email_sent to event create/update responses (synchronous invites) - Add POST /agents/smtp/test endpoint to verify SMTP config - Add secure field to SMTP config for explicit SSL/TLS control - Accept webhook_url/secret/offsets on POST /calendars - Quote all curl URLs in double quotes for shell safety - Include name/description in /man guide agent creation curl - Improve welcome_event docs visibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 81e017d commit 7c173e1

File tree

15 files changed

+432
-261
lines changed

15 files changed

+432
-261
lines changed

CALDAVE_SPEC.md

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,44 @@ Update the authenticated agent's metadata. Does not change the API key.
143143
}
144144
```
145145

146+
#### `PUT /agents/smtp`
147+
Configure SMTP for outbound emails. When set, all invite and RSVP emails are sent via your SMTP server.
148+
149+
**Request:**
150+
```json
151+
{
152+
"host": "smtp.agentmail.to",
153+
"port": 465,
154+
"username": "inbox@agentmail.to",
155+
"password": "YOUR_SMTP_PASSWORD",
156+
"from": "inbox@agentmail.to",
157+
"secure": true
158+
}
159+
```
160+
161+
All fields except `secure` are required. `secure` defaults to `true` for port 465, `false` otherwise. Use `true` for implicit TLS, `false` for STARTTLS.
162+
163+
**Response:** Returns the config without the password.
164+
165+
#### `GET /agents/smtp`
166+
View SMTP configuration (password excluded). Returns `null` if not configured.
167+
168+
#### `DELETE /agents/smtp`
169+
Remove SMTP configuration. Outbound emails revert to CalDave's built-in delivery.
170+
171+
#### `POST /agents/smtp/test`
172+
Send a test email to verify SMTP configuration works. Sends to the configured `from` address.
173+
174+
**Response:**
175+
```json
176+
{
177+
"success": true,
178+
"message_id": "<abc123@smtp.agentmail.to>",
179+
"from": "inbox@agentmail.to",
180+
"message": "Test email sent successfully to inbox@agentmail.to."
181+
}
182+
```
183+
146184
---
147185

148186
### API Changelog
@@ -210,7 +248,7 @@ The `recommendations` array is only present when authenticated and when there ar
210248

211249
### API Manual
212250

213-
#### `POST /man`
251+
#### `GET /man`
214252
Machine-readable API manual. Returns a JSON document describing all CalDave endpoints with curl examples, parameter definitions, and example responses.
215253

216254
Auth is optional. If a valid Bearer token is provided, the response includes the agent's real calendar IDs and event counts, with personalized curl examples and a recommended next step.
@@ -246,10 +284,15 @@ Create a new calendar for the authenticated agent.
246284
{
247285
"name": "Work Schedule",
248286
"timezone": "America/Denver",
249-
"agentmail_api_key": "am_xxx..."
287+
"webhook_url": "https://example.com/webhook",
288+
"webhook_secret": "my_secret",
289+
"webhook_offsets": [300, 900],
290+
"welcome_event": false
250291
}
251292
```
252293

294+
Optional fields: `timezone` (default UTC), `webhook_url`, `webhook_secret`, `webhook_offsets`, `agentmail_api_key`, `welcome_event` (default true — set to false to skip the auto-created welcome event).
295+
253296
**Response:**
254297
```json
255298
{
@@ -331,7 +374,7 @@ Plain text table of upcoming events. Useful for quick inspection via curl.
331374

332375
**Example:**
333376
```
334-
curl -s http://127.0.0.1:3720/calendars/cal_xxx/view \
377+
curl -s "http://127.0.0.1:3720/calendars/cal_xxx/view" \
335378
-H "Authorization: Bearer sk_live_xxx"
336379
337380
Work (cal_xxx) tz: America/Denver

CHANGELOG.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
77
## [Unreleased]
88

99
### Added
10+
- **SMTP test endpoint**`POST /agents/smtp/test` sends a test email to verify your SMTP configuration works. Sends to the configured `from` address and reports success/failure with the SMTP error message if any.
11+
- **SMTP `secure` field**`PUT /agents/smtp` now accepts an optional `secure` boolean to explicitly control TLS mode. Use `true` for implicit TLS (port 465) or `false` for STARTTLS (port 587). Auto-detected from port when omitted.
12+
- **Webhook config at calendar creation**`POST /calendars` now accepts `webhook_url`, `webhook_secret`, and `webhook_offsets` at creation time, saving a separate `PATCH` call.
13+
- **`email_sent` in event responses**`POST` and `PATCH` event endpoints now return `email_sent: true/false` when the event has attendees, confirming whether the invite was dispatched. Invites are now sent synchronously before the response is returned.
1014
- **SMTP integration for outbound emails** — configure your own SMTP server via `PUT /agents/smtp` so calendar invites and RSVP replies are sent from your email address instead of CalDave's built-in delivery. New `GET /agents/smtp` (view config, password excluded) and `DELETE /agents/smtp` (revert to built-in). `GET /agents/me` now includes `smtp_configured` boolean. Supports any SMTP provider (AgentMail, SendGrid, Gmail, etc.).
1115
- **Webhook test endpoint**`POST /calendars/:id/webhook/test` sends a test payload to the calendar's configured webhook URL and returns the HTTP status code. Supports HMAC-SHA256 signing via `X-CalDave-Signature` when `webhook_secret` is set.
1216
- **Welcome event opt-out**`POST /calendars` now accepts `welcome_event: false` to skip the auto-created welcome event. Default remains true.
13-
- **Rate limit documentation** — rate limits documented in `/docs` and included in `POST /man` responses. All responses include `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers (RFC draft-7).
14-
- **Agent name/description prominence**`POST /agents` docs and quickstart now recommend including `name` and `description` at creation time. `POST /man` recommends naming your agent as the first step for unnamed agents. New `recommended` badge on params.
17+
- **Rate limit documentation** — rate limits documented in `/docs` and included in `GET /man` responses. All responses include `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers (RFC draft-7).
18+
- **Agent name/description prominence**`POST /agents` docs and quickstart now recommend including `name` and `description` at creation time. `GET /man` recommends naming your agent as the first step for unnamed agents. New `recommended` badge on params.
1519
- **Personalized recommendations in changelog**`GET /changelog` with auth now includes a `recommendations` array with actionable suggestions based on agent state (e.g. name your agent, create your first calendar, add a description).
1620
- **API changelog endpoint**`GET /changelog` returns a structured list of API changes with dates and docs links. With optional Bearer auth, highlights changes introduced since the agent was created. Designed for agents to poll ~weekly.
17-
- **Agent metadata**`POST /agents` now accepts optional `name` and `description` fields to identify agents. New `GET /agents/me` returns the agent's profile. New `PATCH /agents` updates metadata without changing the API key. Agent name and description are surfaced in `POST /man` context.
21+
- **Agent metadata**`POST /agents` now accepts optional `name` and `description` fields to identify agents. New `GET /agents/me` returns the agent's profile. New `PATCH /agents` updates metadata without changing the API key. Agent name and description are surfaced in `GET /man` context.
1822
- **Outbound calendar invites** — when an event is created or updated with attendees, CalDave sends METHOD:REQUEST iCal invite emails via Postmark. Invites include `.ics` attachments that work with Google Calendar, Outlook, and Apple Calendar. From address is the calendar's email so replies route back through the inbound webhook.
1923
- **Agent name in outbound emails** — when an agent has a name set (via `PATCH /agents`), outbound invite and RSVP reply emails use `"Agent Name" <calendar-email>` as the From address, so recipients see a friendly display name instead of just the calendar email.
2024
- **Outbound RSVP replies** — when an agent responds to an inbound invite via `POST /respond`, CalDave sends a METHOD:REPLY iCal email back to the organiser with the agent's acceptance, decline, or tentative status.
@@ -28,12 +32,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2832
- **Welcome event on new calendars** — new calendars automatically get a "Send Peter feedback" event at 9am the next day (in the calendar's timezone), with an invite sent to peter.clark@gmail.com.
2933
- **Terms of Service and Privacy Policy** — new `/terms` and `/privacy` pages with footer links on landing, docs, and quickstart pages.
3034

35+
### Changed
36+
- **`GET /man` (was `POST /man`)** — the machine-readable API manual is now a GET endpoint. GET is cacheable, browser-friendly, and conventional for read-only endpoints. The `?guide` query param still works. All docs, tests, and examples updated.
37+
3138
### Fixed
32-
- **Docs placeholder standardization** — all curl examples in `/docs` now use consistent `UPPER_SNAKE_CASE` placeholders (`YOUR_API_KEY`, `CAL_ID`, `EVT_ID`, `FEED_TOKEN`) with gold highlighting. Added placeholder legend, error endpoints (`GET /errors`, `GET /errors/:id`), and fixed agent description max length (1024, not 1000). Fixed `POST /man` respond example (`email_sent` field, not `response_sent`).
33-
- **API docs updated**`/docs` page now documents `GET /agents/me`, `PATCH /agents`, agent `name`/`description` fields on `POST /agents`, `GET /changelog`, and `POST /man`. Table of contents updated with Agents and Discovery sections.
34-
- **JSON 404 catch-all** — unmatched routes now return `{"error": "Not found. Try POST /man for the API reference."}` instead of Express's default HTML page.
35-
- **Guide mode discoverability**`POST /man?guide` now includes a `discover_more` object pointing to the full API reference, changelog, and agent update endpoint so agents don't have to guess at available endpoints.
36-
- **Welcome event in /man recommendation**`POST /man` recommendation logic now accounts for the auto-created welcome event (same fix as changelog recommendations).
39+
- **Curl URL quoting** — all curl examples across docs, homepage, quickstart, `/man`, README, and spec files now wrap URLs in double quotes for shell safety (especially URLs with `?` query parameters).
40+
- **`/man` guide mode agent creation** — the recommended next step for unauthenticated agents now includes `name` and `description` params in the example curl body, so agents set their identity from the start.
41+
- **Docs placeholder standardization** — all curl examples in `/docs` now use consistent `UPPER_SNAKE_CASE` placeholders (`YOUR_API_KEY`, `CAL_ID`, `EVT_ID`, `FEED_TOKEN`) with gold highlighting. Added placeholder legend, error endpoints (`GET /errors`, `GET /errors/:id`), and fixed agent description max length (1024, not 1000). Fixed `GET /man` respond example (`email_sent` field, not `response_sent`).
42+
- **API docs updated**`/docs` page now documents `GET /agents/me`, `PATCH /agents`, agent `name`/`description` fields on `POST /agents`, `GET /changelog`, and `GET /man`. Table of contents updated with Agents and Discovery sections.
43+
- **JSON 404 catch-all** — unmatched routes now return `{"error": "Not found. Try GET /man for the API reference."}` instead of Express's default HTML page.
44+
- **Guide mode discoverability**`GET /man?guide` now includes a `discover_more` object pointing to the full API reference, changelog, and agent update endpoint so agents don't have to guess at available endpoints.
45+
- **Welcome event in /man recommendation**`GET /man` recommendation logic now accounts for the auto-created welcome event (same fix as changelog recommendations).
3746
- **Agent creation rate limiter scope** — the strict agent creation rate limiter (POST /agents) no longer applies to GET /agents/me and PATCH /agents, which now use the general API rate limiter instead.
3847
- **`trust proxy` for Fly.io** — set `app.set('trust proxy', 1)` so `express-rate-limit` correctly identifies clients behind Fly's reverse proxy. Fixes `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` validation errors on every cold start.
3948
- **Improved outbound email logging** — all outbound email operations now log with `[outbound]` prefix including Postmark message IDs on success and status codes on failure.
@@ -48,7 +57,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4857
- **Inbound REQUEST after CANCEL** — when an organiser moves or re-sends an invite that was previously cancelled, the event is now un-cancelled (recurring events reset to `recurring` with rematerialized instances; non-recurring events reset to `tentative`)
4958
- **Calendar email domain** — calendar emails now correctly use `@invite.caldave.ai` (Postmark inbound domain) instead of `@caldave.ai`
5059
- **MCP server instructions** — the MCP server now sends a detailed `instructions` string during initialization, giving AI agents full context about CalDave's workflow, inbound email, recurring events, metadata, and tool usage guidance
51-
- **Machine-readable API manual** (`POST /man`) — JSON endpoint describing all CalDave API endpoints, with optional Bearer auth for personalized context. Returns real calendar IDs, event counts, and recommended next steps for authenticated agents. Designed for AI agent consumption.
60+
- **Machine-readable API manual** (`GET /man`) — JSON endpoint describing all CalDave API endpoints, with optional Bearer auth for personalized context. Returns real calendar IDs, event counts, and recommended next steps for authenticated agents. Designed for AI agent consumption.
5261
- **CalDave v1 core API** — calendar-as-a-service for AI agents
5362
- Agent provisioning (`POST /agents`) with API key generation (nanoid + SHA-256 hash)
5463
- Bearer token authentication middleware

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,22 @@ The database and schema are created automatically.
4646

4747
```bash
4848
# Create an agent (returns API key — save it!)
49-
curl -s -X POST http://127.0.0.1:3720/agents | jq
49+
curl -s -X POST "http://127.0.0.1:3720/agents" | jq
5050

5151
# Create a calendar
52-
curl -s -X POST http://127.0.0.1:3720/calendars \
52+
curl -s -X POST "http://127.0.0.1:3720/calendars" \
5353
-H "Content-Type: application/json" \
5454
-H "Authorization: Bearer YOUR_API_KEY" \
5555
-d '{"name": "Work Schedule", "timezone": "America/Denver"}' | jq
5656

5757
# Create an event
58-
curl -s -X POST http://127.0.0.1:3720/calendars/CAL_ID/events \
58+
curl -s -X POST "http://127.0.0.1:3720/calendars/CAL_ID/events" \
5959
-H "Content-Type: application/json" \
6060
-H "Authorization: Bearer YOUR_API_KEY" \
6161
-d '{"title": "Daily standup", "start": "2025-03-01T09:00:00-07:00", "end": "2025-03-01T09:15:00-07:00"}' | jq
6262

6363
# Check upcoming events
64-
curl -s http://127.0.0.1:3720/calendars/CAL_ID/upcoming \
64+
curl -s "http://127.0.0.1:3720/calendars/CAL_ID/upcoming" \
6565
-H "Authorization: Bearer YOUR_API_KEY" | jq
6666
```
6767

SPEC.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ docker compose up --build
1111
DATABASE_URL=postgres://plc:postgres@localhost:5432/caldave PORT=3720 npm run dev
1212

1313
# Test health
14-
curl http://127.0.0.1:3720/health
14+
curl "http://127.0.0.1:3720/health"
1515

1616
# Create an agent
17-
curl -X POST http://127.0.0.1:3720/agents
17+
curl -X POST "http://127.0.0.1:3720/agents"
1818

1919
# Deploy
2020
fly deploy
@@ -28,7 +28,7 @@ fly deploy
2828
| `GET /health` | No | Server health check |
2929
| `GET /health/db` | No | Database health check |
3030
| `POST /agents` | No | Create agent (returns API key) |
31-
| `POST /man` | Optional | Machine-readable API manual (JSON), personalized if authenticated |
31+
| `GET /man` | Optional | Machine-readable API manual (JSON), personalized if authenticated |
3232
| `POST /calendars` | Yes | Create calendar |
3333
| `GET /calendars` | Yes | List agent's calendars |
3434
| `GET /calendars/:id` | Yes | Get calendar details |
@@ -65,7 +65,7 @@ src/
6565
│ └── rateLimitStub.js — Stub rate limit headers
6666
└── routes/
6767
├── agents.js — POST /agents
68-
├── man.js — POST /man (machine-readable API manual)
68+
├── man.js — GET /man (machine-readable API manual)
6969
├── calendars.js — Calendar CRUD
7070
├── events.js — Event CRUD + upcoming + respond
7171
├── feeds.js — iCal feed generation

src/db.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ async function initSchema() {
152152
ALTER TABLE agents ADD COLUMN IF NOT EXISTS smtp_user text;
153153
ALTER TABLE agents ADD COLUMN IF NOT EXISTS smtp_pass text;
154154
ALTER TABLE agents ADD COLUMN IF NOT EXISTS smtp_from text;
155+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS smtp_secure boolean;
155156
`);
156157

157158
// Backfill inbound_token for existing calendars that don't have one

src/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ app.get('/', (req, res) => {
145145
</ul>
146146
147147
<pre><code><span class="dim"># Get started in seconds</span>
148-
curl -s -X POST https://${DOMAIN}/man?guide</code></pre>
148+
curl -s "https://${DOMAIN}/man?guide"</code></pre>
149149
150-
<p style="color:#fbbf24; font-size:0.8125rem; margin-bottom:1.5rem;"><strong>AI agents:</strong> Use curl or HTTP requests, not a browser. Run the command above or call <code style="background:#334155; padding:0.125rem 0.375rem; border-radius:4px; font-size:0.8125rem;">POST /man</code> for a machine-readable guide.</p>
150+
<p style="color:#fbbf24; font-size:0.8125rem; margin-bottom:1.5rem;"><strong>AI agents:</strong> Use curl or HTTP requests, not a browser. Run the command above or call <code style="background:#334155; padding:0.125rem 0.375rem; border-radius:4px; font-size:0.8125rem;">GET /man</code> for a machine-readable guide.</p>
151151
152152
<div class="links">
153153
<a href="/docs" class="primary">API Docs</a>
@@ -255,7 +255,7 @@ app.get('/errors/:id', auth, async (req, res) => {
255255
// ---------------------------------------------------------------------------
256256

257257
app.use((req, res) => {
258-
res.status(404).json({ error: 'Not found. Try POST /man for the API reference.' });
258+
res.status(404).json({ error: 'Not found. Try GET /man for the API reference.' });
259259
});
260260

261261
// ---------------------------------------------------------------------------

src/lib/outbound.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ function getPostmarkClient() {
4242
async function getAgentSmtpConfig(agentId) {
4343
if (!agentId) return null;
4444
const { rows } = await pool.query(
45-
'SELECT smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from FROM agents WHERE id = $1',
45+
'SELECT smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from, smtp_secure FROM agents WHERE id = $1',
4646
[agentId]
4747
);
4848
if (rows.length === 0 || !rows[0].smtp_host) return null;
4949
const r = rows[0];
50-
return { host: r.smtp_host, port: r.smtp_port, user: r.smtp_user, pass: r.smtp_pass, from: r.smtp_from };
50+
return { host: r.smtp_host, port: r.smtp_port, user: r.smtp_user, pass: r.smtp_pass, from: r.smtp_from, secure: r.smtp_secure };
5151
}
5252

5353
/**
@@ -62,7 +62,7 @@ async function sendViaSmtp(smtpConfig, params) {
6262
const transporter = nodemailer.createTransport({
6363
host: smtpConfig.host,
6464
port: smtpConfig.port,
65-
secure: smtpConfig.port === 465,
65+
secure: smtpConfig.secure !== null && smtpConfig.secure !== undefined ? smtpConfig.secure : (smtpConfig.port === 465),
6666
auth: { user: smtpConfig.user, pass: smtpConfig.pass },
6767
});
6868

0 commit comments

Comments
 (0)