openclaw/extensions/nylas/src/cli.ts
Qasim 4c74b1134d 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
2026-01-26 18:49:28 -05:00

363 lines
12 KiB
TypeScript

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)}`);
}
}
});
}