Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/nylas/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import (
"github.com/nylas/cli/internal/cli/scheduler"
"github.com/nylas/cli/internal/cli/setup"
"github.com/nylas/cli/internal/cli/slack"
templatecmd "github.com/nylas/cli/internal/cli/templatecmd"
"github.com/nylas/cli/internal/cli/timezone"
"github.com/nylas/cli/internal/cli/update"
"github.com/nylas/cli/internal/cli/webhook"
"github.com/nylas/cli/internal/cli/workflow"
"github.com/nylas/cli/internal/ui"
)

Expand Down Expand Up @@ -55,12 +57,14 @@ func main() {
rootCmd.AddCommand(timezone.NewTimezoneCmd())
rootCmd.AddCommand(mcp.NewMCPCmd())
rootCmd.AddCommand(slack.NewSlackCmd())
rootCmd.AddCommand(templatecmd.NewTemplateCmd())
rootCmd.AddCommand(demo.NewDemoCmd())
rootCmd.AddCommand(cli.NewTUICmd())
rootCmd.AddCommand(ui.NewUICmd())
rootCmd.AddCommand(air.NewAirCmd())
rootCmd.AddCommand(chat.NewChatCmd())
rootCmd.AddCommand(update.NewUpdateCmd())
rootCmd.AddCommand(workflow.NewWorkflowCmd())

if err := cli.Execute(); err != nil {
cli.LogAuditError(err)
Expand Down
38 changes: 37 additions & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ nylas email send ... --sign # Send GPG-signed
nylas email send ... --encrypt # Send GPG-encrypted email
nylas email send ... --sign --encrypt # Sign AND encrypt (recommended)
nylas email send --list-gpg-keys # List available GPG signing keys
nylas email send --to EMAIL --template-id TPL --template-data '{}' # Send using a hosted template
nylas email send --template-id TPL --template-data-file data.json --render-only
nylas email search --query "QUERY" # Search emails
nylas email delete <message-id> # Delete email
nylas email mark read <message-id> # Mark as read
Expand Down Expand Up @@ -226,7 +228,7 @@ nylas email smart-compose --prompt "..." # AI-powered email generation

---

## Email Templates
## Local Email Templates

```bash
nylas email templates list # List all templates
Expand All @@ -243,6 +245,40 @@ nylas email templates use <template-id> --to EMAIL # Send using template

---

## Hosted Templates

```bash
nylas template list
nylas template create --name NAME --subject SUBJECT --body BODY
nylas template show <template-id>
nylas template update <template-id> [flags]
nylas template delete <template-id> --yes
nylas template render <template-id> --data '{}'
nylas template render-html --body "<p>{{x}}</p>" --engine mustache --data '{}'
```

**Scopes:** `--scope app` for application templates, `--scope grant --grant-id <id>` for grant-level templates.

**Details:** `docs/commands/templates.md`

---

## Hosted Workflows

```bash
nylas workflow list
nylas workflow create --name NAME --template-id TPL --trigger-event booking.created
nylas workflow show <workflow-id>
nylas workflow update <workflow-id> [flags]
nylas workflow delete <workflow-id> --yes
```

**Scopes:** `--scope app` for application workflows, `--scope grant --grant-id <id>` for grant-level workflows.

**Details:** `docs/commands/workflows.md`

---

## Folders & Threads

```bash
Expand Down
46 changes: 45 additions & 1 deletion docs/commands/email.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ nylas email send --to "to@example.com" --subject "Newsletter" --body "..." \
# Send with custom metadata
nylas email send --to "to@example.com" --subject "Order Confirmation" --body "..." \
--metadata "order_id=12345" --metadata "customer_id=cust_abc"

# Send using a hosted template
nylas email send --to "to@example.com" --template-id tpl_123 \
--template-data '{"user":{"name":"Ada"}}'

# Preview a hosted template render without sending
nylas email send --template-id tpl_123 --template-data-file ./data.json --render-only
```

**Tracking Options:**
Expand All @@ -104,6 +111,15 @@ nylas email send --to "to@example.com" --subject "Order Confirmation" --body "..
- `--track-label` - Label for grouping tracked emails (for analytics)
- `--metadata` - Custom key=value metadata pairs (can be specified multiple times)

**Hosted template options:**
- `--template-id` - Render and send a Nylas-hosted template
- `--template-scope` - Hosted template scope: `app` or `grant`
- `--template-grant-id` - Override the grant used for grant-scoped hosted templates
- `--template-data` - Inline JSON variables for hosted template rendering
- `--template-data-file` - JSON file containing hosted template variables
- `--render-only` - Preview the rendered hosted template without sending
- `--template-strict` - Fail if the hosted template references missing variables (default: true)

**Example output (scheduled):**
```bash
$ nylas email send --to "user@example.com" --subject "Reminder" --body "Don't forget!" --schedule 2h --yes
Expand All @@ -118,6 +134,35 @@ Email preview:
Scheduled to send: Mon Dec 16, 2024 4:30 PM PST
```

### Hosted Templates

Use top-level hosted templates with `nylas email send` when you want a shared, API-backed template instead of a local file-backed template.

```bash
# Application-level hosted template
nylas email send \
--to "to@example.com" \
--template-id tpl_123 \
--template-scope app \
--template-data '{"user":{"name":"Ada"}}'

# Grant-level hosted template
nylas email send \
--to "to@example.com" \
--template-id tpl_456 \
--template-scope grant \
--template-grant-id <grant-id> \
--template-data-file ./data.json

# Preview the final rendered subject/body without sending
nylas email send \
--template-id tpl_123 \
--template-data '{"user":{"name":"Ada"}}' \
--render-only
```

For hosted template CRUD and standalone rendering, see `nylas template ...`.

### GPG Signing and Encryption

Sign and/or encrypt emails using GPG/PGP:
Expand Down Expand Up @@ -469,4 +514,3 @@ fi
- [Send email from the command line](https://cli.nylas.com/guides/send-email-from-terminal) — full guide with examples for Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP
- [Give your AI coding agent an email address](https://cli.nylas.com/guides/give-ai-agent-email-address) — connect Claude Code, Cursor, or Codex CLI to your inbox
- [Email deliverability from the CLI](https://cli.nylas.com/guides/email-deliverability-cli) — debug SPF, DKIM, and DMARC

13 changes: 13 additions & 0 deletions docs/commands/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ Manage locally-stored email templates for composing messages with variable subst

Templates are stored in `~/.config/nylas/templates.json` and support `{{variable}}` syntax for placeholders. Variables are automatically extracted from the subject and body when templates are created or updated.

For API-backed hosted templates shared across an application or grant, use the top-level `nylas template ...` commands.

---

### Hosted Templates Quick Reference

```bash
nylas template list
nylas template create --name "Welcome" --subject "Hello {{user.name}}" --body "<p>Hello {{user.name}}</p>"
nylas template render <template-id> --data '{"user":{"name":"Ada"}}'
nylas email send --to user@example.com --template-id <template-id> --template-data '{"user":{"name":"Ada"}}'
```

---

### List Templates
Expand Down
24 changes: 23 additions & 1 deletion docs/commands/workflows.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
## Hosted Workflows

Manage API-backed workflows that connect booking events to hosted templates.

```bash
nylas workflow list
nylas workflow create --name "Booking Confirmation" --template-id tpl_123 --trigger-event booking.created
nylas workflow show <workflow-id>
nylas workflow update <workflow-id> --disabled
nylas workflow delete <workflow-id> --yes
```

**Scopes:**
- `--scope app` for application-level workflows
- `--scope grant --grant-id <id>` for grant-level workflows

**Typical flow:**
1. Create a hosted template with `nylas template create`
2. Create a workflow that points at that template with `nylas workflow create`
3. Verify the rendered email with `nylas email send --template-id ... --render-only`

---

## Advanced Automation Workflows

Complex automation workflows and integration patterns that span multiple Nylas CLI commands.
Expand Down Expand Up @@ -621,4 +644,3 @@ def call_api():
- **API Reference:** https://developer.nylas.com/docs/api/v3/

---

2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/nylas/cli

go 1.26.0

toolchain go1.26.2

require (
github.com/atotto/clipboard v0.1.4
github.com/fatih/color v1.18.0
Expand Down
1 change: 1 addition & 0 deletions internal/adapters/ai/ollama_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestNewOllamaClient(t *testing.T) {

if client == nil {
t.Fatal("expected non-nil client")
return
}

if client.baseURL != "http://custom:8080" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ func TestConflictResolver_GenerateRecommendations(t *testing.T) {
// Verify the analysis structure is present
if analysis == nil {
t.Fatal("Expected analysis, got nil")
return
}

// Should have soft conflicts detected
Expand Down
2 changes: 2 additions & 0 deletions internal/adapters/analytics/focus_optimizer_test_advanced.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ func TestFocusOptimizer_AdaptSchedule(t *testing.T) {

if change == nil {
t.Fatal("Expected adaptive schedule change, got nil")
return
}

if change.Trigger != domain.TriggerMeetingOverload {
Expand Down Expand Up @@ -436,6 +437,7 @@ func TestFocusOptimizer_OptimizeMeetingDuration(t *testing.T) {

if optimization == nil {
t.Fatal("Expected optimization, got nil")
return
}

if optimization.EventID != "event-123" {
Expand Down
1 change: 1 addition & 0 deletions internal/adapters/analytics/focus_optimizer_test_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func TestFocusOptimizer_AnalyzeFocusTimePatterns(t *testing.T) {

if analysis == nil {
t.Fatal("Expected analysis, got nil")
return
}

if analysis.UserEmail != "grant-123" {
Expand Down
1 change: 1 addition & 0 deletions internal/adapters/browser/browser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func TestCreateCommand(t *testing.T) {

if cmd == nil {
t.Fatal("createCommand() returned nil")
return
}

// Verify the command was created (we can't test execution)
Expand Down
2 changes: 2 additions & 0 deletions internal/adapters/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ func TestMockConfigStore_LoadSave(t *testing.T) {
}
if config == nil {
t.Fatal("Load() returned nil config")
return
}

// Modify and save
Expand Down Expand Up @@ -310,6 +311,7 @@ func TestMockConfigStore_LoadWithNilConfig(t *testing.T) {
}
if config == nil {
t.Fatal("Load() returned nil, expected default config")
return
}

// Should return default config
Expand Down
2 changes: 2 additions & 0 deletions internal/adapters/gpg/encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func TestEncryptData_Integration(t *testing.T) {

if result == nil {
t.Fatal("EncryptData() returned nil result")
return
}

// Validate ciphertext
Expand Down Expand Up @@ -201,6 +202,7 @@ func TestSignAndEncryptData_Integration(t *testing.T) {

if result == nil {
t.Fatal("SignAndEncryptData() returned nil result")
return
}

// Validate ciphertext
Expand Down
2 changes: 2 additions & 0 deletions internal/adapters/gpg/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ func TestSignData_Integration(t *testing.T) {

if result == nil {
t.Fatal("SignData() returned nil result")
return
}

// Validate signature
Expand Down Expand Up @@ -535,6 +536,7 @@ func TestFindKeyByEmail_Integration(t *testing.T) {

if foundKey == nil {
t.Fatal("FindKeyByEmail() returned nil")
return
}

if foundKey.KeyID == "" {
Expand Down
1 change: 1 addition & 0 deletions internal/adapters/mcp/proxy_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func TestNewProxy(t *testing.T) {
proxy := NewProxy("test-api-key", "us")
if proxy == nil {
t.Fatal("NewProxy returned nil")
return
}
if proxy.apiKey != "test-api-key" {
t.Errorf("expected apiKey 'test-api-key', got '%s'", proxy.apiKey)
Expand Down
1 change: 1 addition & 0 deletions internal/adapters/nylas/demo/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func TestClient_ExchangeCode(t *testing.T) {
if !tt.wantErr {
if grant == nil {
t.Fatal("expected non-nil grant")
return
}
if grant.ID != "demo-grant-id" {
t.Errorf("expected ID 'demo-grant-id', got %q", grant.ID)
Expand Down
Loading
Loading