feat(nylas): add Nylas API v3 plugin for email, calendar, and contacts

Add a new plugin extension that integrates with Nylas API v3 to provide
email, calendar, and contacts functionality for Clawdbot agents.

## Features

### Tools (16 total)
- Discovery: nylas_discover_grants (auto-discover authenticated accounts)
- Email: list_emails, get_email, send_email, create_draft, list_threads, list_folders
- Calendar: list_calendars, list_events, get_event, create_event, update_event,
  delete_event, check_availability
- Contacts: list_contacts, get_contact

### CLI Commands
- `clawdbot nylas status` - Check API connection and auto-discover grants
- `clawdbot nylas discover` - List all authenticated accounts
- `clawdbot nylas test` - Test API with sample operations
- `clawdbot nylas grants` - Show configured grants

### Gateway Methods
- 8 RPC methods for programmatic access to Nylas functionality

## Technical Details

- Uses official Nylas Node SDK (nylas@^7.5.2)
- TypeBox schemas for tool parameters
- Zod for configuration validation
- Multi-account support via named grants
- Proper error handling with NylasApiError wrapper

## Configuration

```yaml
plugins:
  entries:
    nylas:
      config:
        apiKey: "nyk_v0_..."
        defaultGrantId: "grant-id"
        grants:
          work: "work-grant-id"
          personal: "personal-grant-id"
```

## Testing

- 104 unit tests (100% coverage on core logic)
- 19 integration tests against live Nylas API
- Run with: `bun test extensions/nylas/`
- Live tests: `LIVE=1 bun test extensions/nylas/index.live.test.ts`

## Documentation

- README with setup instructions, tool reference, and troubleshooting
- Support contacts: limitless@nylas.com, support@nylas.com
This commit is contained in:
Qasim 2026-01-26 18:49:28 -05:00
parent fe1f2d971a
commit 4c74b1134d
13 changed files with 4853 additions and 0 deletions

188
extensions/nylas/README.md Normal file
View File

@ -0,0 +1,188 @@
# Nylas Plugin for Clawdbot
Email, calendar, and contacts integration via Nylas API v3.
Built on the [official Nylas Node SDK](https://www.npmjs.com/package/nylas).
## Quick Start
1. **Get API Key** - Sign up at https://dashboard.nylas.com and create an API key
2. **Add API key to config:**
```yaml
plugins:
entries:
nylas:
config:
apiKey: "nyk_v0_your_key_here"
```
3. **Auto-discover grants:**
```bash
clawdbot nylas status
```
This will connect to Nylas, discover all authenticated accounts, and show the recommended configuration.
## Prerequisites
Before using this plugin, you need to set up a Nylas account:
1. **Create Nylas Account** - Sign up at https://dashboard.nylas.com
2. **Create Application** - All apps → Create new app → Choose region (US/EU)
3. **Get API Key** - API Keys section → Create new key
4. **Add Grants** - Grants section → Add Account → Authenticate email accounts
5. **Grant IDs are auto-discovered** - The plugin will automatically find your authenticated accounts
## Configuration
Add the following to your `clawdbot.yaml`:
```yaml
plugins:
entries:
nylas:
enabled: true
config:
apiKey: "nyk_v0_..." # Required: API key from Nylas dashboard
apiUri: "https://api.us.nylas.com" # Optional: US or EU region
defaultTimezone: "America/New_York" # Optional: Default timezone for dates
defaultGrantId: "grant-id-here" # Primary account's grant ID
grants: # Optional: Named grants for multi-account
work: "grant-id-for-work-email"
personal: "grant-id-for-personal"
```
## CLI Commands
```bash
# Check API connection and auto-discover grants (recommended first step)
clawdbot nylas status
# Discover all authenticated accounts and show config snippet
clawdbot nylas discover
clawdbot nylas discover --json # Output as JSON
# Test API with specific grant (auto-discovers if not configured)
clawdbot nylas test
clawdbot nylas test --grant work
# List configured and available grants
clawdbot nylas grants
clawdbot nylas grants --configured # Only show configured grants
```
## Available Tools
### Account Discovery
| Tool | Description |
|------|-------------|
| `nylas_discover_grants` | Discover all authenticated email accounts (grants) available via this API key |
### Email Tools
| Tool | Description |
|------|-------------|
| `nylas_list_emails` | List/search emails with filters (folder, from, subject, date, unread, starred) |
| `nylas_get_email` | Get full email content by ID |
| `nylas_send_email` | Send email (to, cc, bcc, subject, body) |
| `nylas_create_draft` | Create email draft |
| `nylas_list_threads` | List email threads (conversations) |
| `nylas_list_folders` | List email folders (INBOX, SENT, etc.) |
### Calendar Tools
| Tool | Description |
|------|-------------|
| `nylas_list_calendars` | List available calendars |
| `nylas_list_events` | List/search events with date range |
| `nylas_get_event` | Get event details |
| `nylas_create_event` | Create event with attendees, location |
| `nylas_update_event` | Update existing event |
| `nylas_delete_event` | Delete event |
| `nylas_check_availability` | Check availability for participants |
### Contact Tools
| Tool | Description |
|------|-------------|
| `nylas_list_contacts` | List/search contacts |
| `nylas_get_contact` | Get contact details |
## Multi-Account Support
Use the optional `grant` parameter on any tool to specify which account to use:
```
// Use the default grant
nylas_list_emails({ folder: "INBOX" })
// Use a named grant
nylas_list_emails({ grant: "work", folder: "INBOX" })
// Use a raw grant ID
nylas_list_emails({ grant: "abc123-grant-id", folder: "INBOX" })
```
## Example Usage
**List recent unread emails:**
```
nylas_list_emails({ unread: true, limit: 10 })
```
**Send an email:**
```
nylas_send_email({
to: "recipient@example.com",
subject: "Hello",
body: "<p>This is the email body.</p>"
})
```
**Create a calendar event:**
```
nylas_create_event({
calendar_id: "primary",
title: "Team Meeting",
start: "2024-03-20T14:00:00-05:00",
end: "2024-03-20T15:00:00-05:00",
participants: "alice@example.com, bob@example.com",
location: "Conference Room A"
})
```
**Check availability:**
```
nylas_check_availability({
emails: "alice@example.com, bob@example.com",
start: "2024-03-20T09:00:00-05:00",
end: "2024-03-20T17:00:00-05:00",
duration_minutes: 30
})
```
## Troubleshooting
**"apiKey is required"**
- Add your Nylas API key to the plugin configuration
**"No grant ID provided"**
- Set `defaultGrantId` or pass `grant` parameter to tools
**401 Unauthorized**
- Check that your API key is valid and not expired
- Verify the grant ID exists in your Nylas dashboard
**404 Not Found**
- Ensure the resource (message, event, contact) ID is correct
- Check that you're using the right grant for that resource
## Support
For Nylas API issues, contact:
- limitless@nylas.com
- support@nylas.com
## Nylas Documentation
- [Nylas API v3 Docs](https://developer.nylas.com/docs/v3/)
- [Nylas Dashboard](https://dashboard.nylas.com)

View File

@ -0,0 +1,56 @@
{
"id": "nylas",
"uiHints": {
"apiKey": {
"label": "API Key",
"help": "Nylas API key from dashboard.nylas.com",
"sensitive": true
},
"apiUri": {
"label": "API URI",
"help": "Use https://api.us.nylas.com (US) or https://api.eu.nylas.com (EU)",
"placeholder": "https://api.us.nylas.com"
},
"defaultGrantId": {
"label": "Default Grant ID",
"help": "Primary email account's grant ID from Nylas dashboard"
},
"defaultTimezone": {
"label": "Default Timezone",
"help": "Timezone for date/time operations (e.g., America/New_York)",
"placeholder": "America/New_York"
},
"grants": {
"label": "Named Grants",
"help": "Map of named grants for multi-account access (e.g., work: grant-id-1)",
"advanced": true
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"apiKey": {
"type": "string"
},
"apiUri": {
"type": "string"
},
"defaultGrantId": {
"type": "string"
},
"defaultTimezone": {
"type": "string"
},
"grants": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}

View File

@ -0,0 +1,477 @@
/**
* Nylas Plugin Integration Tests
*
* These tests run against the real Nylas API.
* Set NYLAS_API_KEY env variable and run with:
* LIVE=1 bun test extensions/nylas/index.live.test.ts
*
* Or run all live tests:
* pnpm test:live
*/
import { describe, expect, it, beforeAll } from "vitest";
import { parseNylasConfig, type NylasConfig } from "./src/config.js";
import { NylasClient, NylasApiError } from "./src/client.js";
import { discoverGrants } from "./src/tools/index.js";
import { listEmails, getEmail, listFolders, listThreads } from "./src/tools/email.js";
import { listCalendars, listEvents } from "./src/tools/calendar.js";
import { listContacts } from "./src/tools/contacts.js";
// Check environment
const NYLAS_API_KEY = process.env.NYLAS_API_KEY ?? "";
const LIVE = process.env.LIVE === "1" || process.env.NYLAS_LIVE_TEST === "1";
const describeLive = LIVE && NYLAS_API_KEY ? describe : describe.skip;
// Store discovered grant for subsequent tests
let testGrantId: string | undefined;
let testCalendarId: string | undefined;
describeLive("Nylas Plugin Integration Tests", () => {
let client: NylasClient;
let config: NylasConfig;
beforeAll(() => {
config = parseNylasConfig({
apiKey: NYLAS_API_KEY,
apiUri: "https://api.us.nylas.com",
defaultTimezone: "UTC",
});
client = new NylasClient({
config,
logger: {
info: () => {},
warn: console.warn,
error: console.error,
},
});
});
// ===========================================================================
// Grant Discovery Tests
// ===========================================================================
describe("Grant Discovery", () => {
it("lists grants from the API", async () => {
const response = await client.listGrants();
expect(response).toBeDefined();
expect(response.data).toBeInstanceOf(Array);
if (response.data.length > 0) {
const grant = response.data[0];
expect(grant.id).toBeDefined();
expect(grant.email).toBeDefined();
expect(grant.provider).toBeDefined();
// Store for subsequent tests
testGrantId = grant.id;
console.log(` Found grant: ${grant.email} (${grant.provider})`);
} else {
console.log(" No grants found - some tests will be skipped");
}
}, 15000);
it("discovers grants via tool", async () => {
const result = await discoverGrants(client, {});
expect(result).toBeDefined();
expect(result.grants).toBeInstanceOf(Array);
expect(result.count).toBe(result.grants.length);
if (result.grants.length > 0) {
expect(result.hint).toContain("grant");
expect(result.grants[0].id).toBeDefined();
expect(result.grants[0].email).toBeDefined();
}
}, 15000);
it("filters grants by provider", async () => {
const result = await discoverGrants(client, { provider: "google" });
expect(result).toBeDefined();
expect(result.grants).toBeInstanceOf(Array);
// All returned grants should be Google
for (const grant of result.grants) {
expect(grant.provider).toBe("google");
}
}, 15000);
it("gets specific grant details", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const response = await client.getGrant(testGrantId);
expect(response.data).toBeDefined();
expect(response.data.id).toBe(testGrantId);
expect(response.data.email).toBeDefined();
expect(response.data.grantStatus).toBeDefined();
}, 15000);
});
// ===========================================================================
// Email Tools Tests
// ===========================================================================
describe("Email Tools", () => {
it("lists email folders", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const result = await listFolders(client, { grant: testGrantId });
expect(result).toBeDefined();
expect(result.folders).toBeInstanceOf(Array);
if (result.folders.length > 0) {
const folder = result.folders[0];
expect(folder.id).toBeDefined();
expect(folder.name).toBeDefined();
console.log(` Found ${result.folders.length} folders`);
}
}, 15000);
it("lists emails with limit", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const result = await listEmails(client, {
grant: testGrantId,
limit: 5,
});
expect(result).toBeDefined();
expect(result.emails).toBeInstanceOf(Array);
expect(result.emails.length).toBeLessThanOrEqual(5);
if (result.emails.length > 0) {
const email = result.emails[0];
expect(email.id).toBeDefined();
expect(email.subject).toBeDefined();
console.log(` Found ${result.emails.length} emails`);
}
}, 15000);
it("lists unread emails", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const result = await listEmails(client, {
grant: testGrantId,
unread: true,
limit: 10,
});
expect(result).toBeDefined();
expect(result.emails).toBeInstanceOf(Array);
// All returned emails should be unread
for (const email of result.emails) {
expect(email.unread).toBe(true);
}
console.log(` Found ${result.emails.length} unread emails`);
}, 15000);
it("gets a specific email", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
// First get an email ID
const listResult = await listEmails(client, {
grant: testGrantId,
limit: 1,
});
if (listResult.emails.length === 0) {
console.log(" Skipping - no emails available");
return;
}
const emailId = listResult.emails[0].id;
const result = await getEmail(client, {
grant: testGrantId,
message_id: emailId,
});
expect(result).toBeDefined();
expect(result.id).toBe(emailId);
expect(result.subject).toBeDefined();
expect(result.body).toBeDefined();
console.log(` Got email: ${result.subject?.slice(0, 50)}...`);
}, 15000);
it("lists email threads", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const result = await listThreads(client, {
grant: testGrantId,
limit: 5,
});
expect(result).toBeDefined();
expect(result.threads).toBeInstanceOf(Array);
if (result.threads.length > 0) {
const thread = result.threads[0];
expect(thread.id).toBeDefined();
expect(thread.subject).toBeDefined();
console.log(` Found ${result.threads.length} threads`);
}
}, 15000);
});
// ===========================================================================
// Calendar Tools Tests
// ===========================================================================
describe("Calendar Tools", () => {
it("lists calendars", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const result = await listCalendars(client, { grant: testGrantId });
expect(result).toBeDefined();
expect(result.calendars).toBeInstanceOf(Array);
if (result.calendars.length > 0) {
const calendar = result.calendars[0];
expect(calendar.id).toBeDefined();
expect(calendar.name).toBeDefined();
// Find primary calendar for subsequent tests
const primary = result.calendars.find((c) => c.is_primary);
testCalendarId = primary?.id ?? result.calendars[0].id;
console.log(` Found ${result.calendars.length} calendars`);
}
}, 15000);
it("lists events with date range", async () => {
if (!testGrantId || !testCalendarId) {
console.log(" Skipping - no grant or calendar available");
return;
}
// Get events from the last 30 days
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const result = await listEvents(client, {
grant: testGrantId,
calendar_id: testCalendarId,
start: thirtyDaysAgo.toISOString(),
end: now.toISOString(),
limit: 10,
});
expect(result).toBeDefined();
expect(result.events).toBeInstanceOf(Array);
console.log(` Found ${result.events.length} events in last 30 days`);
if (result.events.length > 0) {
const event = result.events[0];
expect(event.id).toBeDefined();
expect(event.title).toBeDefined();
}
}, 15000);
it("lists upcoming events", async () => {
if (!testGrantId || !testCalendarId) {
console.log(" Skipping - no grant or calendar available");
return;
}
// Get events for the next 7 days
const now = new Date();
const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const result = await listEvents(client, {
grant: testGrantId,
calendar_id: testCalendarId,
start: now.toISOString(),
end: sevenDaysLater.toISOString(),
limit: 10,
});
expect(result).toBeDefined();
expect(result.events).toBeInstanceOf(Array);
console.log(` Found ${result.events.length} upcoming events`);
}, 15000);
});
// ===========================================================================
// Contact Tools Tests
// ===========================================================================
describe("Contact Tools", () => {
it("lists contacts", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
const result = await listContacts(client, {
grant: testGrantId,
limit: 10,
});
expect(result).toBeDefined();
expect(result.contacts).toBeInstanceOf(Array);
console.log(` Found ${result.contacts.length} contacts`);
if (result.contacts.length > 0) {
const contact = result.contacts[0];
expect(contact.id).toBeDefined();
}
}, 15000);
it("lists contacts with email filter", async () => {
if (!testGrantId) {
console.log(" Skipping - no grant available");
return;
}
// First get a contact to filter by
const allContacts = await listContacts(client, {
grant: testGrantId,
limit: 1,
});
if (allContacts.contacts.length === 0 || !allContacts.contacts[0].emails?.[0]) {
console.log(" Skipping - no contacts with email available");
return;
}
const emailToFind = allContacts.contacts[0].emails[0].email;
const result = await listContacts(client, {
grant: testGrantId,
email: emailToFind,
});
expect(result).toBeDefined();
expect(result.contacts).toBeInstanceOf(Array);
console.log(` Found ${result.contacts.length} contacts matching ${emailToFind}`);
}, 15000);
});
// ===========================================================================
// Error Handling Tests
// ===========================================================================
describe("Error Handling", () => {
it("handles invalid grant ID gracefully", async () => {
try {
await client.getGrant("invalid-grant-id-12345");
expect.fail("Should have thrown an error");
} catch (err) {
expect(err).toBeInstanceOf(NylasApiError);
const apiError = err as NylasApiError;
expect(apiError.statusCode).toBe(404);
}
}, 15000);
it("handles invalid API key gracefully", async () => {
const badConfig = parseNylasConfig({
apiKey: "invalid-api-key",
apiUri: "https://api.us.nylas.com",
});
const badClient = new NylasClient({
config: badConfig,
logger: {
info: () => {},
warn: () => {},
error: () => {},
},
});
try {
await badClient.listGrants();
expect.fail("Should have thrown an error");
} catch (err) {
expect(err).toBeInstanceOf(NylasApiError);
const apiError = err as NylasApiError;
expect(apiError.statusCode).toBe(401);
}
}, 15000);
});
// ===========================================================================
// Client Configuration Tests
// ===========================================================================
describe("Client Configuration", () => {
it("resolves named grants correctly", () => {
const configWithGrants = parseNylasConfig({
apiKey: NYLAS_API_KEY,
defaultGrantId: "default-grant-123",
grants: {
work: "work-grant-456",
personal: "personal-grant-789",
},
});
const clientWithGrants = new NylasClient({
config: configWithGrants,
logger: {
info: () => {},
warn: () => {},
error: () => {},
},
});
// Test resolving named grants
expect(clientWithGrants.resolveGrantId("work")).toBe("work-grant-456");
expect(clientWithGrants.resolveGrantId("personal")).toBe("personal-grant-789");
// Test default grant
expect(clientWithGrants.resolveGrantId()).toBe("default-grant-123");
// Test raw grant ID passthrough
expect(clientWithGrants.resolveGrantId("some-raw-id")).toBe("some-raw-id");
});
it("throws when no grant ID available", () => {
const configNoDefault = parseNylasConfig({
apiKey: NYLAS_API_KEY,
});
const clientNoDefault = new NylasClient({
config: configNoDefault,
logger: {
info: () => {},
warn: () => {},
error: () => {},
},
});
expect(() => clientNoDefault.resolveGrantId()).toThrow("No grant ID provided");
});
});
});
// ===========================================================================
// Summary when tests complete
// ===========================================================================
describeLive("Integration Test Summary", () => {
it("displays test environment info", () => {
console.log("\n Nylas Integration Tests Summary:");
console.log(` API URI: https://api.us.nylas.com`);
console.log(` API Key: ${NYLAS_API_KEY.slice(0, 10)}...`);
console.log(` Test Grant ID: ${testGrantId ?? "(none found)"}`);
console.log(` Test Calendar ID: ${testCalendarId ?? "(none found)"}`);
expect(true).toBe(true);
});
});

File diff suppressed because it is too large Load Diff

213
extensions/nylas/index.ts Normal file
View File

@ -0,0 +1,213 @@
import { NylasConfigSchema, parseNylasConfig, validateNylasConfig, type NylasConfig } from "./src/config.js";
import { NylasClient, NylasApiError } from "./src/client.js";
import { nylasTools } from "./src/tools/index.js";
import { registerNylasCli } from "./src/cli.js";
const nylasConfigSchema = {
parse(value: unknown): NylasConfig {
return parseNylasConfig(value);
},
uiHints: {
apiKey: {
label: "API Key",
help: "Nylas API key from dashboard.nylas.com",
sensitive: true,
},
apiUri: {
label: "API URI",
help: "Use https://api.us.nylas.com (US) or https://api.eu.nylas.com (EU)",
placeholder: "https://api.us.nylas.com",
},
defaultGrantId: {
label: "Default Grant ID",
help: "Primary email account's grant ID from Nylas dashboard",
},
defaultTimezone: {
label: "Default Timezone",
help: "Timezone for date/time operations (e.g., America/New_York)",
placeholder: "America/New_York",
},
grants: {
label: "Named Grants",
help: "Map of named grants for multi-account access",
advanced: true,
},
},
};
const nylasPlugin = {
id: "nylas",
name: "Nylas",
description: "Email, calendar, and contacts integration via Nylas API v3",
configSchema: nylasConfigSchema,
register(api) {
const config = nylasConfigSchema.parse(api.pluginConfig);
const validation = validateNylasConfig(config);
// Capture validation errors early for use in getClient closure
const validationErrors = !validation.valid ? validation.errors : null;
// Create client lazily
let client: NylasClient | null = null;
const getClient = () => {
if (!client) {
if (!config.enabled) {
throw new Error("Nylas plugin is disabled");
}
if (validationErrors) {
throw new Error(`Nylas configuration error: ${validationErrors.join("; ")}`);
}
client = new NylasClient({
config,
logger: api.logger,
});
}
return client;
};
// Register all tools
for (const toolDef of nylasTools) {
api.registerTool({
name: toolDef.name,
label: toolDef.label,
description: toolDef.description,
parameters: toolDef.parameters,
async execute(_toolCallId, params) {
const json = (payload: unknown) => ({
content: [
{ type: "text", text: JSON.stringify(payload, null, 2) },
],
details: payload,
});
try {
const result = await toolDef.execute(getClient(), params);
return json(result);
} catch (err) {
if (err instanceof NylasApiError) {
return json({
error: err.message,
status_code: err.statusCode,
error_type: err.errorType,
request_id: err.requestId,
});
}
return json({
error: err instanceof Error ? err.message : String(err),
});
}
},
});
}
// Register CLI commands
api.registerCli(
({ program, logger }) =>
registerNylasCli({
program,
config,
logger,
}),
{ commands: ["nylas"] },
);
// Register gateway methods for programmatic access
api.registerGatewayMethod("nylas.discoverGrants", async ({ params, respond }) => {
try {
const c = getClient();
const result = await c.listGrants(params ?? {});
respond(true, {
grants: result.data.map((g) => ({
id: g.id,
email: g.email,
provider: g.provider,
status: g.grant_status,
scopes: g.scope,
})),
});
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.listEmails", async ({ params, respond }) => {
try {
const c = getClient();
const result = await c.listMessages(params ?? {});
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.getMessage", async ({ params, respond }) => {
try {
const c = getClient();
const messageId = typeof params?.messageId === "string" ? params.messageId : "";
const grant = typeof params?.grant === "string" ? params.grant : undefined;
if (!messageId) {
respond(false, { error: "messageId required" });
return;
}
const result = await c.getMessage(messageId, grant);
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.sendMessage", async ({ params, respond }) => {
try {
const c = getClient();
const result = await c.sendMessage(params ?? {});
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.listCalendars", async ({ params, respond }) => {
try {
const c = getClient();
const grant = typeof params?.grant === "string" ? params.grant : undefined;
const result = await c.listCalendars(grant);
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.listEvents", async ({ params, respond }) => {
try {
const c = getClient();
const result = await c.listEvents(params ?? {});
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.createEvent", async ({ params, respond }) => {
try {
const c = getClient();
const result = await c.createEvent(params as Parameters<NylasClient["createEvent"]>[0]);
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
api.registerGatewayMethod("nylas.listContacts", async ({ params, respond }) => {
try {
const c = getClient();
const result = await c.listContacts(params ?? {});
respond(true, result);
} catch (err) {
respond(false, { error: err instanceof Error ? err.message : String(err) });
}
});
},
};
export default nylasPlugin;

View File

@ -0,0 +1,16 @@
{
"name": "@clawdbot/nylas",
"version": "2026.1.26",
"type": "module",
"description": "Clawdbot Nylas API v3 plugin for email, calendar, and contacts",
"dependencies": {
"@sinclair/typebox": "0.34.47",
"nylas": "^7.5.2",
"zod": "^4.3.6"
},
"clawdbot": {
"extensions": [
"./index.ts"
]
}
}

362
extensions/nylas/src/cli.ts Normal file
View File

@ -0,0 +1,362 @@
import type { Command } from "commander";
import type { NylasConfig } from "./config.js";
import { NylasClient, NylasApiError } from "./client.js";
type NylasCliContext = {
program: Command;
config: NylasConfig;
logger: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
};
export function registerNylasCli(ctx: NylasCliContext): void {
const { program, config, logger } = ctx;
const nylas = program
.command("nylas")
.description("Nylas email, calendar, and contacts integration");
// Status command - auto-discovers grants from API
nylas
.command("status")
.description("Check Nylas API connection and discover available grants")
.action(async () => {
logger.info("Nylas Plugin Status");
logger.info("===================");
logger.info("");
// Check configuration
logger.info("Configuration:");
logger.info(` API URI: ${config.apiUri}`);
logger.info(` API Key: ${config.apiKey ? "configured" : "NOT CONFIGURED"}`);
logger.info(` Default Grant ID: ${config.defaultGrantId ?? "not set"}`);
logger.info(` Default Timezone: ${config.defaultTimezone}`);
const namedGrants = Object.keys(config.grants);
if (namedGrants.length > 0) {
logger.info(` Named Grants: ${namedGrants.join(", ")}`);
}
logger.info("");
// Check API connection and discover grants
if (!config.apiKey) {
logger.error("Cannot connect to API - apiKey not configured");
logger.info("");
logger.info("Add your API key to clawdbot.yaml:");
logger.info(" plugins:");
logger.info(" entries:");
logger.info(" nylas:");
logger.info(" config:");
logger.info(" apiKey: nyk_v0_your_api_key_here");
return;
}
logger.info("Connecting to Nylas API...");
try {
const client = new NylasClient({ config, logger });
// Discover all grants associated with this API key
const grantsResponse = await client.listGrants();
const grants = grantsResponse.data;
logger.info(` Connection: OK`);
logger.info("");
if (grants.length === 0) {
logger.warn("No grants found. You need to authenticate email accounts in the Nylas dashboard.");
logger.info("");
logger.info("1. Go to https://dashboard.nylas.com");
logger.info("2. Navigate to Grants section");
logger.info("3. Click 'Add Account' to authenticate your email");
return;
}
logger.info(`Discovered ${grants.length} authenticated account(s):`);
logger.info("");
for (const grant of grants) {
const status = grant.grantStatus === "valid" ? "active" : grant.grantStatus;
logger.info(` Email: ${grant.email}`);
logger.info(` Grant ID: ${grant.id}`);
logger.info(` Provider: ${grant.provider}`);
logger.info(` Status: ${status}`);
if (grant.scope && grant.scope.length > 0) {
logger.info(` Scopes: ${grant.scope.join(", ")}`);
}
logger.info("");
}
// Show recommended configuration
if (!config.defaultGrantId && grants.length > 0) {
logger.info("Recommended configuration for clawdbot.yaml:");
logger.info("");
logger.info(" plugins:");
logger.info(" entries:");
logger.info(" nylas:");
logger.info(" config:");
logger.info(` apiKey: ${config.apiKey.slice(0, 10)}...`);
logger.info(` defaultGrantId: ${grants[0].id}`);
if (grants.length > 1) {
logger.info(" grants:");
for (const grant of grants) {
const name = grant.email.split("@")[0].replace(/[^a-z0-9]/gi, "_").toLowerCase();
logger.info(` ${name}: ${grant.id}`);
}
}
}
} catch (err) {
if (err instanceof NylasApiError) {
logger.error(` Connection: FAILED`);
logger.error(` Error: ${err.message}`);
logger.error(` Status: ${err.statusCode}`);
if (err.requestId) {
logger.error(` Request ID: ${err.requestId}`);
}
if (err.statusCode === 401) {
logger.info("");
logger.info("Your API key may be invalid or expired.");
logger.info("Get a new key from https://dashboard.nylas.com/api-keys");
}
} else {
logger.error(` Connection: FAILED`);
logger.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
}
}
});
// Discover command - fetch grants from API
nylas
.command("discover")
.description("Discover all authenticated accounts from Nylas API")
.option("--json", "Output as JSON")
.action(async (opts: { json?: boolean }) => {
if (!config.apiKey) {
if (opts.json) {
console.log(JSON.stringify({ error: "API key not configured" }));
} else {
logger.error("API key not configured. Set plugins.entries.nylas.config.apiKey");
}
return;
}
try {
const client = new NylasClient({ config, logger });
const grantsResponse = await client.listGrants();
const grants = grantsResponse.data;
if (opts.json) {
console.log(JSON.stringify({
grants: grants.map((g) => ({
id: g.id,
email: g.email,
provider: g.provider,
status: g.grantStatus,
scopes: g.scope,
})),
}, null, 2));
return;
}
if (grants.length === 0) {
logger.warn("No authenticated accounts found.");
logger.info("");
logger.info("Authenticate accounts at https://dashboard.nylas.com → Grants → Add Account");
return;
}
logger.info(`Found ${grants.length} authenticated account(s):`);
logger.info("");
for (const grant of grants) {
const status = grant.grantStatus === "valid" ? "active" : grant.grantStatus;
logger.info(`${grant.email}`);
logger.info(` ID: ${grant.id}`);
logger.info(` Provider: ${grant.provider} | Status: ${status}`);
logger.info("");
}
// Generate config snippet
logger.info("Add to clawdbot.yaml:");
logger.info("");
logger.info("plugins:");
logger.info(" entries:");
logger.info(" nylas:");
logger.info(" config:");
logger.info(` apiKey: "${config.apiKey}"`);
logger.info(` defaultGrantId: "${grants[0].id}"`);
if (grants.length > 1) {
logger.info(" grants:");
for (const grant of grants) {
const name = grant.email.split("@")[0].replace(/[^a-z0-9]/gi, "_").toLowerCase();
logger.info(` ${name}: "${grant.id}"`);
}
}
} catch (err) {
if (opts.json) {
console.log(JSON.stringify({
error: err instanceof Error ? err.message : String(err),
}));
} else if (err instanceof NylasApiError) {
logger.error(`API Error: ${err.message} (${err.statusCode})`);
} else {
logger.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
}
});
// Test command
nylas
.command("test")
.description("Test Nylas API with a specific grant")
.option("-g, --grant <grant>", "Grant name or ID to test")
.action(async (opts: { grant?: string }) => {
if (!config.apiKey) {
logger.error("API key not configured. Set plugins.entries.nylas.config.apiKey");
return;
}
try {
const client = new NylasClient({ config, logger });
// If no grant specified and no default, try to discover one
let grantId = opts.grant ?? config.defaultGrantId ?? Object.values(config.grants)[0];
if (!grantId) {
logger.info("No grant configured, discovering from API...");
const grantsResponse = await client.listGrants({ limit: 1 });
if (grantsResponse.data.length === 0) {
logger.error("No grants found. Authenticate an account at https://dashboard.nylas.com");
return;
}
grantId = grantsResponse.data[0].id;
logger.info(`Using discovered grant: ${grantsResponse.data[0].email}`);
}
logger.info(`Testing grant: ${grantId}`);
logger.info("");
// Get grant details
const grantDetails = await client.getGrant(grantId);
logger.info(`Account: ${grantDetails.data.email}`);
logger.info(`Provider: ${grantDetails.data.provider}`);
logger.info(`Status: ${grantDetails.data.grantStatus}`);
logger.info("");
// Test calendars
logger.info("Calendars:");
const calendars = await client.listCalendars(grantId);
for (const cal of calendars.data.slice(0, 5)) {
const primary = cal.is_primary ? " (primary)" : "";
logger.info(` - ${cal.name}${primary}`);
}
if (calendars.data.length > 5) {
logger.info(` ... and ${calendars.data.length - 5} more`);
}
logger.info("");
// Test folders
logger.info("Email Folders:");
const folders = await client.listFolders(grantId);
for (const folder of folders.data.slice(0, 10)) {
const count = folder.total_count !== undefined ? ` (${folder.total_count} emails)` : "";
logger.info(` - ${folder.name}${count}`);
}
if (folders.data.length > 10) {
logger.info(` ... and ${folders.data.length - 10} more`);
}
logger.info("");
// Test recent emails
logger.info("Recent Emails:");
const messages = await client.listMessages({ grant: grantId, limit: 5 });
for (const msg of messages.data) {
const from = msg.from?.[0]?.email ?? "unknown";
const subject = msg.subject ?? "(no subject)";
const date = new Date(msg.date * 1000).toLocaleDateString();
logger.info(` - ${date} | ${from} | ${subject.slice(0, 50)}`);
}
logger.info("");
logger.info("All tests passed.");
} catch (err) {
if (err instanceof NylasApiError) {
logger.error(`API Error: ${err.message}`);
logger.error(`Status: ${err.statusCode}`);
} else {
logger.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
}
});
// Grants command - shows both configured and discovered grants
nylas
.command("grants")
.description("List configured and available grants")
.option("--configured", "Show only configured grants")
.action(async (opts: { configured?: boolean }) => {
logger.info("Nylas Grants");
logger.info("============");
logger.info("");
// Show configured grants
logger.info("Configured:");
if (config.defaultGrantId) {
logger.info(` Default: ${config.defaultGrantId}`);
}
const namedGrants = Object.entries(config.grants);
if (namedGrants.length > 0) {
for (const [name, id] of namedGrants) {
logger.info(` ${name}: ${id}`);
}
}
if (!config.defaultGrantId && namedGrants.length === 0) {
logger.info(" (none)");
}
if (opts.configured) {
return;
}
logger.info("");
// Fetch from API
if (!config.apiKey) {
logger.warn("Cannot discover grants - apiKey not configured");
return;
}
try {
const client = new NylasClient({ config, logger });
const grantsResponse = await client.listGrants();
const grants = grantsResponse.data;
logger.info("Available from API:");
if (grants.length === 0) {
logger.info(" (none - authenticate accounts at dashboard.nylas.com)");
return;
}
for (const grant of grants) {
const status = grant.grantStatus === "valid" ? "" : ` [${grant.grantStatus}]`;
logger.info(` ${grant.email}: ${grant.id}${status}`);
}
} catch (err) {
if (err instanceof NylasApiError) {
logger.error(`Failed to fetch grants: ${err.message}`);
} else {
logger.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
}
});
}

View File

@ -0,0 +1,534 @@
import Nylas from "nylas";
import type { NylasConfig } from "./config.js";
/**
* Filter out undefined values from an object.
* The Nylas SDK sends undefined values as query params, causing API errors.
*/
function filterDefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
const result: Partial<T> = {};
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined) {
(result as any)[key] = value;
}
}
return result;
}
export type NylasClientOptions = {
config: NylasConfig;
logger?: {
debug?: (message: string) => void;
error: (message: string) => void;
};
};
export class NylasApiError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly requestId?: string,
public readonly errorType?: string,
) {
super(message);
this.name = "NylasApiError";
}
}
/**
* Wrapper around the official Nylas SDK that handles grant resolution
* and provides a simplified interface for the plugin.
*/
export class NylasClient {
public readonly sdk: Nylas;
private readonly defaultGrantId?: string;
private readonly grants: Record<string, string>;
private readonly logger?: NylasClientOptions["logger"];
constructor(options: NylasClientOptions) {
const { config, logger } = options;
this.sdk = new Nylas({
apiKey: config.apiKey ?? "",
apiUri: config.apiUri,
});
this.defaultGrantId = config.defaultGrantId;
this.grants = config.grants;
this.logger = logger;
}
/**
* Resolve a grant name or ID to an actual grant ID.
* If no grant is provided, uses the default grant ID.
*/
resolveGrantId(grantIdOrName?: string): string {
if (!grantIdOrName) {
if (!this.defaultGrantId) {
throw new NylasApiError("No grant ID provided and no defaultGrantId configured");
}
return this.defaultGrantId;
}
// Check if it's a named grant
if (this.grants[grantIdOrName]) {
return this.grants[grantIdOrName];
}
// Assume it's a raw grant ID
return grantIdOrName;
}
// ==========================================================================
// Grants (Account Discovery) - doesn't require a grant ID
// ==========================================================================
async listGrants(params?: {
limit?: number;
offset?: number;
provider?: string;
email?: string;
}) {
this.logger?.debug?.(`[nylas] listGrants`);
try {
const queryParams = params
? filterDefined({
limit: params.limit,
offset: params.offset,
provider: params.provider as any,
email: params.email,
})
: undefined;
return await this.sdk.grants.list({
queryParams: Object.keys(queryParams ?? {}).length > 0 ? queryParams : undefined,
});
} catch (err) {
throw this.wrapError(err);
}
}
async getGrant(grantId: string) {
this.logger?.debug?.(`[nylas] getGrant ${grantId}`);
try {
return await this.sdk.grants.find({ grantId });
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Messages
// ==========================================================================
async listMessages(params?: {
grant?: string;
limit?: number;
pageToken?: string;
subject?: string;
from?: string;
to?: string;
in?: string;
unread?: boolean;
starred?: boolean;
hasAttachment?: boolean;
receivedBefore?: number;
receivedAfter?: number;
}) {
const grantId = this.resolveGrantId(params?.grant);
this.logger?.debug?.(`[nylas] listMessages for ${grantId}`);
try {
const queryParams = params
? filterDefined({
limit: params.limit,
pageToken: params.pageToken,
subject: params.subject,
from: params.from,
to: params.to,
in: params.in,
unread: params.unread,
starred: params.starred,
hasAttachment: params.hasAttachment,
receivedBefore: params.receivedBefore,
receivedAfter: params.receivedAfter,
})
: undefined;
return await this.sdk.messages.list({
identifier: grantId,
queryParams: Object.keys(queryParams ?? {}).length > 0 ? queryParams : undefined,
});
} catch (err) {
throw this.wrapError(err);
}
}
async getMessage(messageId: string, grant?: string) {
const grantId = this.resolveGrantId(grant);
this.logger?.debug?.(`[nylas] getMessage ${messageId}`);
try {
return await this.sdk.messages.find({
identifier: grantId,
messageId,
});
} catch (err) {
throw this.wrapError(err);
}
}
async sendMessage(params: {
grant?: string;
to?: Array<{ email: string; name?: string }>;
cc?: Array<{ email: string; name?: string }>;
bcc?: Array<{ email: string; name?: string }>;
replyTo?: Array<{ email: string; name?: string }>;
subject?: string;
body?: string;
replyToMessageId?: string;
}) {
const grantId = this.resolveGrantId(params.grant);
this.logger?.debug?.(`[nylas] sendMessage`);
try {
return await this.sdk.messages.send({
identifier: grantId,
requestBody: {
to: params.to,
cc: params.cc,
bcc: params.bcc,
replyTo: params.replyTo,
subject: params.subject,
body: params.body,
replyToMessageId: params.replyToMessageId,
},
});
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Threads
// ==========================================================================
async listThreads(params?: {
grant?: string;
limit?: number;
pageToken?: string;
subject?: string;
from?: string;
to?: string;
in?: string;
unread?: boolean;
starred?: boolean;
}) {
const grantId = this.resolveGrantId(params?.grant);
this.logger?.debug?.(`[nylas] listThreads for ${grantId}`);
try {
const queryParams = params
? filterDefined({
limit: params.limit,
pageToken: params.pageToken,
subject: params.subject,
from: params.from,
to: params.to,
in: params.in,
unread: params.unread,
starred: params.starred,
})
: undefined;
return await this.sdk.threads.list({
identifier: grantId,
queryParams: Object.keys(queryParams ?? {}).length > 0 ? queryParams : undefined,
});
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Folders
// ==========================================================================
async listFolders(grant?: string) {
const grantId = this.resolveGrantId(grant);
this.logger?.debug?.(`[nylas] listFolders for ${grantId}`);
try {
return await this.sdk.folders.list({ identifier: grantId });
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Drafts
// ==========================================================================
async createDraft(params: {
grant?: string;
to?: Array<{ email: string; name?: string }>;
cc?: Array<{ email: string; name?: string }>;
bcc?: Array<{ email: string; name?: string }>;
subject?: string;
body?: string;
replyToMessageId?: string;
}) {
const grantId = this.resolveGrantId(params.grant);
this.logger?.debug?.(`[nylas] createDraft`);
try {
return await this.sdk.drafts.create({
identifier: grantId,
requestBody: {
to: params.to,
cc: params.cc,
bcc: params.bcc,
subject: params.subject,
body: params.body,
replyToMessageId: params.replyToMessageId,
},
});
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Calendars
// ==========================================================================
async listCalendars(grant?: string) {
const grantId = this.resolveGrantId(grant);
this.logger?.debug?.(`[nylas] listCalendars for ${grantId}`);
try {
return await this.sdk.calendars.list({ identifier: grantId });
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Events
// ==========================================================================
async listEvents(params?: {
grant?: string;
calendarId?: string;
limit?: number;
pageToken?: string;
start?: number;
end?: number;
title?: string;
showCancelled?: boolean;
expandRecurring?: boolean;
}) {
const grantId = this.resolveGrantId(params?.grant);
this.logger?.debug?.(`[nylas] listEvents for ${grantId}`);
try {
const queryParams = filterDefined({
calendarId: params?.calendarId ?? "primary",
limit: params?.limit,
pageToken: params?.pageToken,
start: params?.start,
end: params?.end,
title: params?.title,
showCancelled: params?.showCancelled,
expandRecurring: params?.expandRecurring,
});
return await this.sdk.events.list({
identifier: grantId,
queryParams: queryParams as any,
});
} catch (err) {
throw this.wrapError(err);
}
}
async getEvent(eventId: string, calendarId: string, grant?: string) {
const grantId = this.resolveGrantId(grant);
this.logger?.debug?.(`[nylas] getEvent ${eventId}`);
try {
return await this.sdk.events.find({
identifier: grantId,
eventId,
queryParams: { calendarId },
});
} catch (err) {
throw this.wrapError(err);
}
}
async createEvent(params: {
grant?: string;
calendarId: string;
title?: string;
description?: string;
location?: string;
when: any; // SDK's When type
participants?: Array<{ email: string; name?: string }>;
busy?: boolean;
}) {
const grantId = this.resolveGrantId(params.grant);
this.logger?.debug?.(`[nylas] createEvent`);
try {
return await this.sdk.events.create({
identifier: grantId,
queryParams: { calendarId: params.calendarId },
requestBody: {
title: params.title,
description: params.description,
location: params.location,
when: params.when,
participants: params.participants,
busy: params.busy,
},
});
} catch (err) {
throw this.wrapError(err);
}
}
async updateEvent(
eventId: string,
calendarId: string,
params: {
grant?: string;
title?: string;
description?: string;
location?: string;
when?: any;
participants?: Array<{ email: string; name?: string }>;
busy?: boolean;
},
) {
const grantId = this.resolveGrantId(params.grant);
this.logger?.debug?.(`[nylas] updateEvent ${eventId}`);
try {
return await this.sdk.events.update({
identifier: grantId,
eventId,
queryParams: { calendarId },
requestBody: {
title: params.title,
description: params.description,
location: params.location,
when: params.when,
participants: params.participants,
busy: params.busy,
},
});
} catch (err) {
throw this.wrapError(err);
}
}
async deleteEvent(eventId: string, calendarId: string, grant?: string) {
const grantId = this.resolveGrantId(grant);
this.logger?.debug?.(`[nylas] deleteEvent ${eventId}`);
try {
await this.sdk.events.destroy({
identifier: grantId,
eventId,
queryParams: { calendarId },
});
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Availability
// ==========================================================================
async checkAvailability(params: {
startTime: number;
endTime: number;
participants: Array<{ email: string }>;
durationMinutes: number;
intervalMinutes?: number;
}) {
this.logger?.debug?.(`[nylas] checkAvailability`);
try {
return await this.sdk.calendars.getAvailability({
requestBody: {
startTime: params.startTime,
endTime: params.endTime,
participants: params.participants.map((p) => ({ email: p.email })),
durationMinutes: params.durationMinutes,
intervalMinutes: params.intervalMinutes,
},
});
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Contacts
// ==========================================================================
async listContacts(params?: {
grant?: string;
limit?: number;
pageToken?: string;
email?: string;
phoneNumber?: string;
source?: string;
group?: string;
}) {
const grantId = this.resolveGrantId(params?.grant);
this.logger?.debug?.(`[nylas] listContacts for ${grantId}`);
try {
const queryParams = params
? filterDefined({
limit: params.limit,
pageToken: params.pageToken,
email: params.email,
phoneNumber: params.phoneNumber,
source: params.source as any,
group: params.group,
})
: undefined;
return await this.sdk.contacts.list({
identifier: grantId,
queryParams: Object.keys(queryParams ?? {}).length > 0 ? queryParams : undefined,
});
} catch (err) {
throw this.wrapError(err);
}
}
async getContact(contactId: string, grant?: string) {
const grantId = this.resolveGrantId(grant);
this.logger?.debug?.(`[nylas] getContact ${contactId}`);
try {
return await this.sdk.contacts.find({
identifier: grantId,
contactId,
});
} catch (err) {
throw this.wrapError(err);
}
}
// ==========================================================================
// Error handling
// ==========================================================================
private wrapError(err: unknown): NylasApiError {
if (err instanceof NylasApiError) {
return err;
}
// Handle Nylas SDK errors
const error = err as any;
if (error?.statusCode || error?.requestId) {
return new NylasApiError(
error.message ?? String(err),
error.statusCode,
error.requestId,
error.type,
);
}
return new NylasApiError(
err instanceof Error ? err.message : String(err),
);
}
}

View File

@ -0,0 +1,46 @@
import { z } from "zod";
export const NylasConfigSchema = z.object({
enabled: z.boolean().default(true),
apiKey: z.string().optional(),
apiUri: z.string().default("https://api.us.nylas.com"),
defaultGrantId: z.string().optional(),
defaultTimezone: z.string().default("UTC"),
grants: z.record(z.string(), z.string()).default({}),
});
export type NylasConfig = z.infer<typeof NylasConfigSchema>;
export function parseNylasConfig(value: unknown): NylasConfig {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return NylasConfigSchema.parse({
enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
apiKey: typeof raw.apiKey === "string" ? raw.apiKey : undefined,
apiUri: typeof raw.apiUri === "string" ? raw.apiUri : "https://api.us.nylas.com",
defaultGrantId: typeof raw.defaultGrantId === "string" ? raw.defaultGrantId : undefined,
defaultTimezone: typeof raw.defaultTimezone === "string" ? raw.defaultTimezone : "UTC",
grants: raw.grants && typeof raw.grants === "object" ? (raw.grants as Record<string, string>) : {},
});
}
export type NylasConfigValidation =
| { valid: true }
| { valid: false; errors: string[] };
export function validateNylasConfig(config: NylasConfig): NylasConfigValidation {
const errors: string[] = [];
if (!config.apiKey) {
errors.push("apiKey is required");
}
if (!config.defaultGrantId && Object.keys(config.grants).length === 0) {
errors.push("defaultGrantId or at least one named grant is required");
}
return errors.length > 0 ? { valid: false, errors } : { valid: true };
}

View File

@ -0,0 +1,339 @@
import { Type, type Static } from "@sinclair/typebox";
import type { NylasClient } from "../client.js";
// Helper to format participant
function formatParticipant(p: { email: string; name?: string; status?: string }): string {
const parts = [p.email];
if (p.name) parts.unshift(p.name);
if (p.status && p.status !== "noreply") parts.push(`(${p.status})`);
return parts.join(" ");
}
// Helper to parse participant emails
function parseParticipants(emails: string | string[] | undefined): Array<{ email: string }> | undefined {
if (!emails) return undefined;
const items = Array.isArray(emails) ? emails : emails.split(",").map((s) => s.trim());
return items.filter(Boolean).map((email) => ({ email }));
}
// Helper to format event time for display
function formatEventWhen(when: any): { start: string; end?: string; all_day?: boolean } {
if (!when) return { start: "unknown" };
// Handle different "when" types from Nylas SDK
if (when.date) {
return { start: when.date, all_day: true };
}
if (when.startDate && when.endDate) {
return { start: when.startDate, end: when.endDate, all_day: true };
}
if (when.time) {
return { start: new Date(when.time * 1000).toISOString() };
}
if (when.startTime && when.endTime) {
return {
start: new Date(when.startTime * 1000).toISOString(),
end: new Date(when.endTime * 1000).toISOString(),
};
}
return { start: JSON.stringify(when) };
}
// Helper to create "when" object for SDK
function createWhen(
start: string,
end?: string,
allDay?: boolean,
timezone?: string,
): any {
if (allDay) {
if (end) {
return {
startDate: start.slice(0, 10),
endDate: end.slice(0, 10),
};
}
return { date: start.slice(0, 10) };
}
const startTime = Math.floor(new Date(start).getTime() / 1000);
if (end) {
const endTime = Math.floor(new Date(end).getTime() / 1000);
return {
startTime,
endTime,
startTimezone: timezone,
endTimezone: timezone,
};
}
return { time: startTime, timezone };
}
// =============================================================================
// List Calendars Tool
// =============================================================================
export const ListCalendarsSchema = Type.Object({
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type ListCalendarsParams = Static<typeof ListCalendarsSchema>;
export async function listCalendars(client: NylasClient, params: ListCalendarsParams) {
const response = await client.listCalendars(params.grant);
const calendars = response.data.map((cal) => ({
id: cal.id,
name: cal.name,
description: cal.description,
is_primary: cal.isPrimary,
timezone: cal.timezone,
read_only: cal.readOnly,
}));
return { calendars };
}
// =============================================================================
// List Events Tool
// =============================================================================
export const ListEventsSchema = Type.Object({
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
calendar_id: Type.Optional(Type.String({ description: "Calendar ID to list events from (uses primary if not specified)" })),
start: Type.Optional(Type.String({ description: "ISO 8601 date/time - events starting after this time" })),
end: Type.Optional(Type.String({ description: "ISO 8601 date/time - events ending before this time" })),
title: Type.Optional(Type.String({ description: "Filter by event title (partial match)" })),
show_cancelled: Type.Optional(Type.Boolean({ description: "Include cancelled events" })),
expand_recurring: Type.Optional(Type.Boolean({ description: "Expand recurring events into individual instances" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 25, max: 100)" })),
page_token: Type.Optional(Type.String({ description: "Pagination token for next page" })),
});
export type ListEventsParams = Static<typeof ListEventsSchema>;
export async function listEvents(client: NylasClient, params: ListEventsParams) {
const limit = Math.min(params.limit ?? 25, 100);
const response = await client.listEvents({
grant: params.grant,
calendarId: params.calendar_id,
limit,
pageToken: params.page_token,
title: params.title,
start: params.start ? Math.floor(new Date(params.start).getTime() / 1000) : undefined,
end: params.end ? Math.floor(new Date(params.end).getTime() / 1000) : undefined,
showCancelled: params.show_cancelled,
expandRecurring: params.expand_recurring,
});
const events = response.data.map((event) => {
const time = formatEventWhen(event.when);
return {
id: event.id,
calendar_id: event.calendarId,
title: event.title ?? "(no title)",
description: event.description,
location: event.location,
start: time.start,
end: time.end,
all_day: time.all_day,
participants: event.participants?.map(formatParticipant),
status: event.status,
busy: event.busy,
conferencing_url: event.conferencing?.details?.url,
};
});
return {
events,
has_more: !!response.nextCursor,
next_page_token: response.nextCursor,
count: events.length,
};
}
// =============================================================================
// Get Event Tool
// =============================================================================
export const GetEventSchema = Type.Object({
event_id: Type.String({ description: "The event ID" }),
calendar_id: Type.String({ description: "The calendar ID the event belongs to" }),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type GetEventParams = Static<typeof GetEventSchema>;
export async function getEvent(client: NylasClient, params: GetEventParams) {
const response = await client.getEvent(params.event_id, params.calendar_id, params.grant);
const event = response.data;
const time = formatEventWhen(event.when);
return {
id: event.id,
calendar_id: event.calendarId,
title: event.title ?? "(no title)",
description: event.description,
location: event.location,
start: time.start,
end: time.end,
all_day: time.all_day,
participants: event.participants?.map(formatParticipant),
status: event.status,
visibility: event.visibility,
busy: event.busy,
read_only: event.readOnly,
conferencing: event.conferencing,
created_at: event.createdAt ? new Date(event.createdAt * 1000).toISOString() : undefined,
updated_at: event.updatedAt ? new Date(event.updatedAt * 1000).toISOString() : undefined,
};
}
// =============================================================================
// Create Event Tool
// =============================================================================
export const CreateEventSchema = Type.Object({
calendar_id: Type.String({ description: "Calendar ID to create the event in" }),
title: Type.String({ description: "Event title" }),
start: Type.String({ description: "ISO 8601 start date/time (e.g., '2024-03-20T10:00:00Z' or '2024-03-20' for all-day)" }),
end: Type.Optional(Type.String({ description: "ISO 8601 end date/time" })),
all_day: Type.Optional(Type.Boolean({ description: "Whether this is an all-day event" })),
timezone: Type.Optional(Type.String({ description: "Timezone for the event (e.g., 'America/New_York')" })),
description: Type.Optional(Type.String({ description: "Event description" })),
location: Type.Optional(Type.String({ description: "Event location" })),
participants: Type.Optional(Type.String({ description: "Comma-separated list of participant emails to invite" })),
busy: Type.Optional(Type.Boolean({ description: "Whether to show as busy during this event (default: true)" })),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type CreateEventParams = Static<typeof CreateEventSchema>;
export async function createEvent(client: NylasClient, params: CreateEventParams) {
const response = await client.createEvent({
grant: params.grant,
calendarId: params.calendar_id,
title: params.title,
when: createWhen(params.start, params.end, params.all_day, params.timezone),
description: params.description,
location: params.location,
participants: parseParticipants(params.participants),
busy: params.busy ?? true,
});
const event = response.data;
const time = formatEventWhen(event.when);
return {
success: true,
event_id: event.id,
calendar_id: event.calendarId,
title: event.title,
start: time.start,
end: time.end,
all_day: time.all_day,
};
}
// =============================================================================
// Update Event Tool
// =============================================================================
export const UpdateEventSchema = Type.Object({
event_id: Type.String({ description: "The event ID to update" }),
calendar_id: Type.String({ description: "The calendar ID the event belongs to" }),
title: Type.Optional(Type.String({ description: "New event title" })),
start: Type.Optional(Type.String({ description: "New ISO 8601 start date/time" })),
end: Type.Optional(Type.String({ description: "New ISO 8601 end date/time" })),
all_day: Type.Optional(Type.Boolean({ description: "Whether this is an all-day event" })),
timezone: Type.Optional(Type.String({ description: "Timezone for the event" })),
description: Type.Optional(Type.String({ description: "New event description" })),
location: Type.Optional(Type.String({ description: "New event location" })),
participants: Type.Optional(Type.String({ description: "New comma-separated list of participant emails" })),
busy: Type.Optional(Type.Boolean({ description: "Whether to show as busy during this event" })),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type UpdateEventParams = Static<typeof UpdateEventSchema>;
export async function updateEvent(client: NylasClient, params: UpdateEventParams) {
const updateData: any = { grant: params.grant };
if (params.title !== undefined) updateData.title = params.title;
if (params.description !== undefined) updateData.description = params.description;
if (params.location !== undefined) updateData.location = params.location;
if (params.busy !== undefined) updateData.busy = params.busy;
if (params.participants !== undefined) updateData.participants = parseParticipants(params.participants);
if (params.start !== undefined) {
updateData.when = createWhen(params.start, params.end, params.all_day, params.timezone);
}
const response = await client.updateEvent(params.event_id, params.calendar_id, updateData);
const event = response.data;
const time = formatEventWhen(event.when);
return {
success: true,
event_id: event.id,
title: event.title,
start: time.start,
end: time.end,
};
}
// =============================================================================
// Delete Event Tool
// =============================================================================
export const DeleteEventSchema = Type.Object({
event_id: Type.String({ description: "The event ID to delete" }),
calendar_id: Type.String({ description: "The calendar ID the event belongs to" }),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type DeleteEventParams = Static<typeof DeleteEventSchema>;
export async function deleteEvent(client: NylasClient, params: DeleteEventParams) {
await client.deleteEvent(params.event_id, params.calendar_id, params.grant);
return { success: true, event_id: params.event_id };
}
// =============================================================================
// Check Availability Tool
// =============================================================================
export const CheckAvailabilitySchema = Type.Object({
emails: Type.String({ description: "Comma-separated list of participant emails to check availability for" }),
start: Type.String({ description: "ISO 8601 start date/time for availability window" }),
end: Type.String({ description: "ISO 8601 end date/time for availability window" }),
duration_minutes: Type.Number({ description: "Required meeting duration in minutes" }),
interval_minutes: Type.Optional(Type.Number({ description: "Interval between time slots in minutes (default: 15)" })),
});
export type CheckAvailabilityParams = Static<typeof CheckAvailabilitySchema>;
export async function checkAvailability(client: NylasClient, params: CheckAvailabilityParams) {
const emails = params.emails.split(",").map((e) => e.trim()).filter(Boolean);
const response = await client.checkAvailability({
startTime: Math.floor(new Date(params.start).getTime() / 1000),
endTime: Math.floor(new Date(params.end).getTime() / 1000),
durationMinutes: params.duration_minutes,
intervalMinutes: params.interval_minutes ?? 15,
participants: emails.map((email) => ({ email })),
});
const slots = response.data.timeSlots?.map((slot) => ({
start: new Date(slot.startTime * 1000).toISOString(),
end: new Date(slot.endTime * 1000).toISOString(),
available_for: slot.emails,
})) ?? [];
return {
available_slots: slots,
checked_emails: response.data.order,
slot_count: slots.length,
};
}

View File

@ -0,0 +1,101 @@
import { Type, type Static } from "@sinclair/typebox";
import type { NylasClient } from "../client.js";
// =============================================================================
// List Contacts Tool
// =============================================================================
export const ListContactsSchema = Type.Object({
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
email: Type.Optional(Type.String({ description: "Filter by email address" })),
phone_number: Type.Optional(Type.String({ description: "Filter by phone number" })),
source: Type.Optional(Type.String({ description: "Filter by source (e.g., 'address_book', 'domain_contact')" })),
group: Type.Optional(Type.String({ description: "Filter by contact group ID" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 25, max: 100)" })),
page_token: Type.Optional(Type.String({ description: "Pagination token for next page" })),
});
export type ListContactsParams = Static<typeof ListContactsSchema>;
export async function listContacts(client: NylasClient, params: ListContactsParams) {
const limit = Math.min(params.limit ?? 25, 100);
const response = await client.listContacts({
grant: params.grant,
limit,
pageToken: params.page_token,
email: params.email,
phoneNumber: params.phone_number,
source: params.source,
group: params.group,
});
const contacts = response.data.map((contact) => {
const name = [contact.givenName, contact.middleName, contact.surname]
.filter(Boolean)
.join(" ") || contact.nickname;
return {
id: contact.id,
name: name || "(no name)",
company: contact.companyName,
job_title: contact.jobTitle,
emails: contact.emails?.map((e) => ({ email: e.email, type: e.type })),
phone_numbers: contact.phoneNumbers?.map((p) => ({ number: p.number, type: p.type })),
source: contact.source,
};
});
return {
contacts,
has_more: !!response.nextCursor,
next_page_token: response.nextCursor,
count: contacts.length,
};
}
// =============================================================================
// Get Contact Tool
// =============================================================================
export const GetContactSchema = Type.Object({
contact_id: Type.String({ description: "The contact ID" }),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type GetContactParams = Static<typeof GetContactSchema>;
export async function getContact(client: NylasClient, params: GetContactParams) {
const response = await client.getContact(params.contact_id, params.grant);
const contact = response.data;
const name = [contact.givenName, contact.middleName, contact.surname]
.filter(Boolean)
.join(" ") || contact.nickname;
return {
id: contact.id,
name: name || "(no name)",
given_name: contact.givenName,
middle_name: contact.middleName,
surname: contact.surname,
nickname: contact.nickname,
company: contact.companyName,
job_title: contact.jobTitle,
emails: contact.emails?.map((e) => ({ email: e.email, type: e.type })),
phone_numbers: contact.phoneNumbers?.map((p) => ({ number: p.number, type: p.type })),
addresses: contact.physicalAddresses?.map((a) => ({
street: a.streetAddress,
city: a.city,
state: a.state,
postal_code: a.postalCode,
country: a.country,
type: a.type,
})),
notes: contact.notes,
birthday: contact.birthday,
picture_url: contact.pictureUrl,
source: contact.source,
groups: contact.groups?.map((g) => ({ id: g.id })),
};
}

View File

@ -0,0 +1,273 @@
import { Type, type Static } from "@sinclair/typebox";
import type { NylasClient } from "../client.js";
// Helper to parse email addresses from string format "name <email>" or just "email"
function parseEmailAddress(input: string): { email: string; name?: string } {
const trimmed = input.trim();
const match = trimmed.match(/^(.+?)\s*<(.+?)>$/);
if (match) {
return { name: match[1].trim(), email: match[2].trim() };
}
return { email: trimmed };
}
function parseEmailAddresses(input: string | string[] | undefined): Array<{ email: string; name?: string }> | undefined {
if (!input) return undefined;
const items = Array.isArray(input) ? input : input.split(",").map((s) => s.trim());
return items.filter(Boolean).map(parseEmailAddress);
}
function formatEmailAddress(addr: { email: string; name?: string }): string {
return addr.name ? `${addr.name} <${addr.email}>` : addr.email;
}
function formatEmailAddresses(addrs?: Array<{ email: string; name?: string }>): string {
if (!addrs || addrs.length === 0) return "";
return addrs.map(formatEmailAddress).join(", ");
}
// =============================================================================
// List Emails Tool
// =============================================================================
export const ListEmailsSchema = Type.Object({
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
folder: Type.Optional(Type.String({ description: "Folder name or ID (e.g., 'INBOX', 'SENT', 'DRAFTS')" })),
subject: Type.Optional(Type.String({ description: "Filter by subject (partial match)" })),
from: Type.Optional(Type.String({ description: "Filter by sender email" })),
to: Type.Optional(Type.String({ description: "Filter by recipient email" })),
unread: Type.Optional(Type.Boolean({ description: "Filter by unread status" })),
starred: Type.Optional(Type.Boolean({ description: "Filter by starred status" })),
has_attachment: Type.Optional(Type.Boolean({ description: "Filter by attachment presence" })),
received_after: Type.Optional(Type.String({ description: "ISO 8601 date - emails received after this date" })),
received_before: Type.Optional(Type.String({ description: "ISO 8601 date - emails received before this date" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 10, max: 50)" })),
page_token: Type.Optional(Type.String({ description: "Pagination token for next page" })),
});
export type ListEmailsParams = Static<typeof ListEmailsSchema>;
export async function listEmails(client: NylasClient, params: ListEmailsParams) {
const limit = Math.min(params.limit ?? 10, 50);
const response = await client.listMessages({
grant: params.grant,
limit,
pageToken: params.page_token,
subject: params.subject,
from: params.from,
to: params.to,
in: params.folder,
unread: params.unread,
starred: params.starred,
hasAttachment: params.has_attachment,
receivedAfter: params.received_after ? Math.floor(new Date(params.received_after).getTime() / 1000) : undefined,
receivedBefore: params.received_before ? Math.floor(new Date(params.received_before).getTime() / 1000) : undefined,
});
const emails = response.data.map((msg) => ({
id: msg.id,
thread_id: msg.threadId,
subject: msg.subject ?? "(no subject)",
from: formatEmailAddresses(msg.from),
to: formatEmailAddresses(msg.to),
date: msg.date ? new Date(msg.date * 1000).toISOString() : undefined,
unread: msg.unread,
starred: msg.starred,
snippet: msg.snippet,
has_attachments: (msg.attachments?.length ?? 0) > 0,
folders: msg.folders,
}));
return {
emails,
has_more: !!response.nextCursor,
next_page_token: response.nextCursor,
count: emails.length,
};
}
// =============================================================================
// Get Email Tool
// =============================================================================
export const GetEmailSchema = Type.Object({
message_id: Type.String({ description: "The email message ID" }),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type GetEmailParams = Static<typeof GetEmailSchema>;
export async function getEmail(client: NylasClient, params: GetEmailParams) {
const response = await client.getMessage(params.message_id, params.grant);
const msg = response.data;
return {
id: msg.id,
thread_id: msg.threadId,
subject: msg.subject ?? "(no subject)",
from: formatEmailAddresses(msg.from),
to: formatEmailAddresses(msg.to),
cc: formatEmailAddresses(msg.cc),
bcc: formatEmailAddresses(msg.bcc),
reply_to: formatEmailAddresses(msg.replyTo),
date: msg.date ? new Date(msg.date * 1000).toISOString() : undefined,
unread: msg.unread,
starred: msg.starred,
body: msg.body,
folders: msg.folders,
attachments: msg.attachments?.map((a) => ({
id: a.id,
filename: a.filename,
content_type: a.contentType,
size: a.size,
})),
};
}
// =============================================================================
// Send Email Tool
// =============================================================================
export const SendEmailSchema = Type.Object({
to: Type.String({ description: "Recipient email(s), comma-separated. Format: 'email' or 'Name <email>'" }),
subject: Type.String({ description: "Email subject" }),
body: Type.String({ description: "Email body (HTML supported)" }),
cc: Type.Optional(Type.String({ description: "CC recipients, comma-separated" })),
bcc: Type.Optional(Type.String({ description: "BCC recipients, comma-separated" })),
reply_to_message_id: Type.Optional(Type.String({ description: "Message ID to reply to (for threading)" })),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type SendEmailParams = Static<typeof SendEmailSchema>;
export async function sendEmail(client: NylasClient, params: SendEmailParams) {
const response = await client.sendMessage({
grant: params.grant,
to: parseEmailAddresses(params.to),
subject: params.subject,
body: params.body,
cc: parseEmailAddresses(params.cc),
bcc: parseEmailAddresses(params.bcc),
replyToMessageId: params.reply_to_message_id,
});
return {
success: true,
message_id: response.data.id,
thread_id: response.data.threadId,
};
}
// =============================================================================
// Create Draft Tool
// =============================================================================
export const CreateDraftSchema = Type.Object({
to: Type.Optional(Type.String({ description: "Recipient email(s), comma-separated" })),
subject: Type.Optional(Type.String({ description: "Email subject" })),
body: Type.Optional(Type.String({ description: "Email body (HTML supported)" })),
cc: Type.Optional(Type.String({ description: "CC recipients, comma-separated" })),
bcc: Type.Optional(Type.String({ description: "BCC recipients, comma-separated" })),
reply_to_message_id: Type.Optional(Type.String({ description: "Message ID to reply to (for threading)" })),
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type CreateDraftParams = Static<typeof CreateDraftSchema>;
export async function createDraft(client: NylasClient, params: CreateDraftParams) {
const response = await client.createDraft({
grant: params.grant,
to: parseEmailAddresses(params.to),
subject: params.subject,
body: params.body,
cc: parseEmailAddresses(params.cc),
bcc: parseEmailAddresses(params.bcc),
replyToMessageId: params.reply_to_message_id,
});
return {
success: true,
draft_id: response.data.id,
thread_id: response.data.threadId,
};
}
// =============================================================================
// List Threads Tool
// =============================================================================
export const ListThreadsSchema = Type.Object({
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
folder: Type.Optional(Type.String({ description: "Folder name or ID" })),
subject: Type.Optional(Type.String({ description: "Filter by subject (partial match)" })),
from: Type.Optional(Type.String({ description: "Filter by sender email" })),
to: Type.Optional(Type.String({ description: "Filter by recipient email" })),
unread: Type.Optional(Type.Boolean({ description: "Filter by unread status" })),
starred: Type.Optional(Type.Boolean({ description: "Filter by starred status" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 10, max: 50)" })),
page_token: Type.Optional(Type.String({ description: "Pagination token for next page" })),
});
export type ListThreadsParams = Static<typeof ListThreadsSchema>;
export async function listThreads(client: NylasClient, params: ListThreadsParams) {
const limit = Math.min(params.limit ?? 10, 50);
const response = await client.listThreads({
grant: params.grant,
limit,
pageToken: params.page_token,
subject: params.subject,
from: params.from,
to: params.to,
in: params.folder,
unread: params.unread,
starred: params.starred,
});
const threads = response.data.map((thread) => ({
id: thread.id,
subject: thread.subject ?? "(no subject)",
participants: thread.participants?.map(formatEmailAddress) ?? [],
message_count: thread.messageIds?.length ?? 0,
snippet: thread.snippet,
unread: thread.unread,
starred: thread.starred,
latest_message_date: thread.latestMessageReceivedDate
? new Date(thread.latestMessageReceivedDate * 1000).toISOString()
: undefined,
folders: thread.folders,
}));
return {
threads,
has_more: !!response.nextCursor,
next_page_token: response.nextCursor,
count: threads.length,
};
}
// =============================================================================
// List Folders Tool
// =============================================================================
export const ListFoldersSchema = Type.Object({
grant: Type.Optional(Type.String({ description: "Named grant or grant ID (uses default if not specified)" })),
});
export type ListFoldersParams = Static<typeof ListFoldersSchema>;
export async function listFolders(client: NylasClient, params: ListFoldersParams) {
const response = await client.listFolders(params.grant);
const folders = response.data.map((folder) => ({
id: folder.id,
name: folder.name,
system_folder: folder.systemFolder,
unread_count: folder.unreadCount,
total_count: folder.totalCount,
}));
return { folders };
}

View File

@ -0,0 +1,211 @@
import { Type } from "@sinclair/typebox";
import type { NylasClient } from "../client.js";
// Email tools
import {
ListEmailsSchema,
listEmails,
GetEmailSchema,
getEmail,
SendEmailSchema,
sendEmail,
CreateDraftSchema,
createDraft,
ListThreadsSchema,
listThreads,
ListFoldersSchema,
listFolders,
} from "./email.js";
// Calendar tools
import {
ListCalendarsSchema,
listCalendars,
ListEventsSchema,
listEvents,
GetEventSchema,
getEvent,
CreateEventSchema,
createEvent,
UpdateEventSchema,
updateEvent,
DeleteEventSchema,
deleteEvent,
CheckAvailabilitySchema,
checkAvailability,
} from "./calendar.js";
// Contact tools
import {
ListContactsSchema,
listContacts,
GetContactSchema,
getContact,
} from "./contacts.js";
// =============================================================================
// Discover Grants Tool (Account Discovery)
// =============================================================================
export const DiscoverGrantsSchema = Type.Object({
provider: Type.Optional(Type.String({ description: "Filter by provider (e.g., 'google', 'microsoft', 'imap')" })),
email: Type.Optional(Type.String({ description: "Filter by email address" })),
});
export async function discoverGrants(
client: import("../client.js").NylasClient,
params: { provider?: string; email?: string },
) {
const response = await client.listGrants({
provider: params.provider,
email: params.email,
});
const grants = response.data.map((grant) => ({
id: grant.id,
email: grant.email,
provider: grant.provider,
status: grant.grantStatus,
scopes: grant.scope,
}));
return {
grants,
count: grants.length,
hint: grants.length > 0
? "Use the grant 'id' as the 'grant' parameter in other nylas tools to access this account."
: "No authenticated accounts found. User needs to add accounts at dashboard.nylas.com",
};
}
export type NylasToolDefinition = {
name: string;
label: string;
description: string;
parameters: unknown;
execute: (client: NylasClient, params: unknown) => Promise<unknown>;
};
export const nylasTools: NylasToolDefinition[] = [
// Account discovery
{
name: "nylas_discover_grants",
label: "Discover Grants",
description: "Discover all authenticated email accounts (grants) available via this Nylas API key. Use this to find grant IDs for multi-account access.",
parameters: DiscoverGrantsSchema,
execute: (client, params) => discoverGrants(client, params as Parameters<typeof discoverGrants>[1]),
},
// Email tools
{
name: "nylas_list_emails",
label: "List Emails",
description: "List and search emails with filters for folder, sender, subject, date range, etc.",
parameters: ListEmailsSchema,
execute: (client, params) => listEmails(client, params as Parameters<typeof listEmails>[1]),
},
{
name: "nylas_get_email",
label: "Get Email",
description: "Get full content of an email by ID including body, attachments, and metadata.",
parameters: GetEmailSchema,
execute: (client, params) => getEmail(client, params as Parameters<typeof getEmail>[1]),
},
{
name: "nylas_send_email",
label: "Send Email",
description: "Send an email to recipients with subject, body, and optional CC/BCC.",
parameters: SendEmailSchema,
execute: (client, params) => sendEmail(client, params as Parameters<typeof sendEmail>[1]),
},
{
name: "nylas_create_draft",
label: "Create Draft",
description: "Create an email draft to edit and send later.",
parameters: CreateDraftSchema,
execute: (client, params) => createDraft(client, params as Parameters<typeof createDraft>[1]),
},
{
name: "nylas_list_threads",
label: "List Threads",
description: "List email threads (conversations) with filters.",
parameters: ListThreadsSchema,
execute: (client, params) => listThreads(client, params as Parameters<typeof listThreads>[1]),
},
{
name: "nylas_list_folders",
label: "List Folders",
description: "List email folders/labels (INBOX, SENT, DRAFTS, custom folders, etc.).",
parameters: ListFoldersSchema,
execute: (client, params) => listFolders(client, params as Parameters<typeof listFolders>[1]),
},
// Calendar tools
{
name: "nylas_list_calendars",
label: "List Calendars",
description: "List available calendars for the account.",
parameters: ListCalendarsSchema,
execute: (client, params) => listCalendars(client, params as Parameters<typeof listCalendars>[1]),
},
{
name: "nylas_list_events",
label: "List Events",
description: "List calendar events with optional date range and title filters.",
parameters: ListEventsSchema,
execute: (client, params) => listEvents(client, params as Parameters<typeof listEvents>[1]),
},
{
name: "nylas_get_event",
label: "Get Event",
description: "Get full details of a calendar event by ID.",
parameters: GetEventSchema,
execute: (client, params) => getEvent(client, params as Parameters<typeof getEvent>[1]),
},
{
name: "nylas_create_event",
label: "Create Event",
description: "Create a new calendar event with title, time, location, and participants.",
parameters: CreateEventSchema,
execute: (client, params) => createEvent(client, params as Parameters<typeof createEvent>[1]),
},
{
name: "nylas_update_event",
label: "Update Event",
description: "Update an existing calendar event.",
parameters: UpdateEventSchema,
execute: (client, params) => updateEvent(client, params as Parameters<typeof updateEvent>[1]),
},
{
name: "nylas_delete_event",
label: "Delete Event",
description: "Delete a calendar event.",
parameters: DeleteEventSchema,
execute: (client, params) => deleteEvent(client, params as Parameters<typeof deleteEvent>[1]),
},
{
name: "nylas_check_availability",
label: "Check Availability",
description: "Check availability for a group of participants within a time window.",
parameters: CheckAvailabilitySchema,
execute: (client, params) => checkAvailability(client, params as Parameters<typeof checkAvailability>[1]),
},
// Contact tools
{
name: "nylas_list_contacts",
label: "List Contacts",
description: "List contacts with optional filters for email, phone, or group.",
parameters: ListContactsSchema,
execute: (client, params) => listContacts(client, params as Parameters<typeof listContacts>[1]),
},
{
name: "nylas_get_contact",
label: "Get Contact",
description: "Get full details of a contact by ID.",
parameters: GetContactSchema,
execute: (client, params) => getContact(client, params as Parameters<typeof getContact>[1]),
},
];
export { Type };