Agent Skills: Intercom Local Dev Loop

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/intercom-local-dev-loop

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/intercom-pack/skills/intercom-local-dev-loop

Skill Files

Browse the full folder contents for intercom-local-dev-loop.

Download Skill

Loading file tree…

plugins/saas-packs/intercom-pack/skills/intercom-local-dev-loop/SKILL.md

Skill Metadata

Name
intercom-local-dev-loop
Description
|

Intercom Local Dev Loop

Overview

Set up a fast local development workflow for Intercom integrations with proper test isolation, mocking strategies, and webhook tunneling.

Prerequisites

  • Completed intercom-install-auth setup
  • Node.js 18+ with npm/pnpm
  • A test/development Intercom workspace (separate from production)

Instructions

Step 1: Project Structure

my-intercom-app/
├── src/
│   ├── intercom/
│   │   ├── client.ts       # Singleton client
│   │   ├── contacts.ts     # Contact operations
│   │   ├── conversations.ts # Conversation operations
│   │   └── types.ts        # Intercom type extensions
│   └── index.ts
├── tests/
│   ├── mocks/
│   │   └── intercom.ts     # Mock client factory
│   ├── contacts.test.ts
│   └── conversations.test.ts
├── .env.development        # Dev workspace token
├── .env.test               # Test config (mocked)
├── .env.example            # Template
└── package.json

Step 2: Environment Configuration

# .env.example (commit this)
INTERCOM_ACCESS_TOKEN=
INTERCOM_WEBHOOK_SECRET=
NODE_ENV=development

# .env.development (git-ignored, real dev workspace token)
INTERCOM_ACCESS_TOKEN=dG9rOmRldl90b2tlbl9oZXJl
INTERCOM_WEBHOOK_SECRET=your-webhook-secret
NODE_ENV=development

Step 3: Client Singleton with Environment Awareness

// src/intercom/client.ts
import { IntercomClient } from "intercom-client";

let instance: IntercomClient | null = null;

export function getClient(): IntercomClient {
  if (!instance) {
    const token = process.env.INTERCOM_ACCESS_TOKEN;
    if (!token) {
      throw new Error(
        "INTERCOM_ACCESS_TOKEN not set. Copy .env.example to .env.development"
      );
    }
    instance = new IntercomClient({ token });
  }
  return instance;
}

// Reset for testing
export function resetClient(): void {
  instance = null;
}

Step 4: Mock Client for Tests

// tests/mocks/intercom.ts
import { vi } from "vitest";

export function createMockClient() {
  return {
    contacts: {
      create: vi.fn().mockResolvedValue({
        type: "contact",
        id: "mock-contact-id",
        role: "user",
        email: "test@example.com",
        name: "Test User",
        external_id: "ext-123",
        custom_attributes: {},
        created_at: 1711100000,
        updated_at: 1711100000,
      }),
      find: vi.fn().mockResolvedValue({
        type: "contact",
        id: "mock-contact-id",
        email: "test@example.com",
      }),
      search: vi.fn().mockResolvedValue({
        type: "list",
        data: [],
        total_count: 0,
        pages: { type: "pages", page: 1, per_page: 50, total_pages: 0 },
      }),
      list: vi.fn().mockResolvedValue({
        type: "list",
        data: [],
        total_count: 0,
        pages: { next: null },
      }),
      update: vi.fn(),
      delete: vi.fn(),
      tag: vi.fn(),
      untag: vi.fn(),
    },
    conversations: {
      create: vi.fn().mockResolvedValue({
        type: "conversation",
        id: "mock-convo-id",
        state: "open",
      }),
      find: vi.fn(),
      list: vi.fn().mockResolvedValue({
        type: "conversation.list",
        conversations: [],
        pages: { next: null },
      }),
      reply: vi.fn(),
      close: vi.fn(),
      assign: vi.fn(),
    },
    messages: {
      create: vi.fn().mockResolvedValue({
        type: "user_message",
        id: "mock-msg-id",
      }),
    },
    admins: {
      list: vi.fn().mockResolvedValue({
        type: "admin.list",
        admins: [{ id: "admin-1", name: "Test Admin", email: "admin@test.com" }],
      }),
    },
    tags: {
      create: vi.fn().mockResolvedValue({ type: "tag", id: "tag-1", name: "test" }),
      list: vi.fn().mockResolvedValue({ type: "list", data: [] }),
    },
  };
}

Step 5: Write Tests

// tests/contacts.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createMockClient } from "./mocks/intercom";

describe("Contact Operations", () => {
  let mockClient: ReturnType<typeof createMockClient>;

  beforeEach(() => {
    mockClient = createMockClient();
  });

  it("should create a user contact", async () => {
    const contact = await mockClient.contacts.create({
      role: "user",
      externalId: "user-123",
      email: "test@example.com",
    });

    expect(contact.id).toBe("mock-contact-id");
    expect(contact.role).toBe("user");
    expect(mockClient.contacts.create).toHaveBeenCalledWith({
      role: "user",
      externalId: "user-123",
      email: "test@example.com",
    });
  });

  it("should search contacts by email", async () => {
    await mockClient.contacts.search({
      query: { field: "email", operator: "=", value: "test@example.com" },
    });

    expect(mockClient.contacts.search).toHaveBeenCalledOnce();
  });
});

Step 6: Webhook Testing with ngrok

# Install ngrok
npm install -g ngrok

# Start your local server
npm run dev  # Starts on port 3000

# Tunnel to expose locally
ngrok http 3000

# Use the HTTPS URL (e.g., https://abc123.ngrok.io) as your webhook URL
# in Intercom Developer Hub > Webhooks

Step 7: Package Scripts

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:integration": "INTERCOM_ACCESS_TOKEN=$INTERCOM_DEV_TOKEN vitest --config vitest.integration.config.ts",
    "typecheck": "tsc --noEmit"
  }
}

Integration Test Pattern

// tests/integration/contacts.integration.test.ts
import { describe, it, expect } from "vitest";
import { IntercomClient } from "intercom-client";

const client = new IntercomClient({
  token: process.env.INTERCOM_ACCESS_TOKEN!,
});

describe.skipIf(!process.env.INTERCOM_ACCESS_TOKEN)("Contacts Integration", () => {
  it("should create and retrieve a contact", async () => {
    const created = await client.contacts.create({
      role: "lead",
      name: `Integration Test ${Date.now()}`,
    });

    expect(created.id).toBeDefined();

    // Clean up
    await client.contacts.delete({ contactId: created.id });
  });
});

Error Handling

| Error | Cause | Solution | |-------|-------|----------| | INTERCOM_ACCESS_TOKEN not set | Missing .env file | Copy .env.example to .env.development | | Port 3000 in use | Another process | lsof -i :3000 and kill, or change port | | ngrok tunnel expired | Free tier 2h limit | Restart ngrok or use paid plan | | Mock type mismatch | SDK updated | Regenerate mocks from SDK types | | rate_limit_exceeded in dev | Dev workspace limits | Add delays between integration tests |

Resources

Next Steps

See intercom-sdk-patterns for production-ready code patterns.