agentsclimarketplace

Attio local dev loop

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/attio-pack/skills/attio-local-dev-loop

'Set up a fast local development loop for Attio integrations withFrom its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill attio-local-dev-loop

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.3 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

Attio Local Dev Loop

Overview

Set up a fast, reproducible local development workflow for Attio REST API integrations. Includes project structure, typed client, mock server for offline work, and integration test harness.

Prerequisites

  • Completed attio-install-auth setup
  • Node.js 18+ with npm or pnpm
  • TypeScript 5+

Instructions

Step 1: Project Structure

my-attio-integration/
├── src/
│   ├── attio/
│   │   ├── client.ts          # Typed fetch wrapper (see attio-install-auth)
│   │   ├── types.ts           # Attio response types
│   │   └── config.ts          # Env-based configuration
│   ├── services/
│   │   ├── people.ts          # People record operations
│   │   ├── companies.ts       # Company record operations
│   │   └── lists.ts           # List entry operations
│   └── index.ts
├── tests/
│   ├── mocks/
│   │   └── attio-fixtures.ts  # Realistic API response fixtures
│   ├── unit/
│   │   └── people.test.ts
│   └── integration/
│       └── attio-live.test.ts # Runs against real API (CI only)
├── .env.example
├── .env.local                 # Git-ignored, real credentials
├── tsconfig.json
└── package.json

Step 2: Type the Attio Response Model

// src/attio/types.ts

/** Attio record identifier */
export interface AttioRecordId {
  object_id: string;
  record_id: string;
}

/** Attio attribute value wrapper */
export interface AttioValue<T = unknown> {
  active_from: string;
  active_until: string | null;
  created_by_actor: { type: string; id: string };
  attribute_type: string;
  [key: string]: T | unknown;
}

/** Generic Attio record */
export interface AttioRecord {
  id: AttioRecordId;
  created_at: string;
  values: Record<string, AttioValue[]>;
}

/** Paginated list response */
export interface AttioListResponse<T> {
  data: T[];
  pagination?: {
    next_cursor?: string;
    has_more?: boolean;
  };
}

/** Attio API error response */
export interface AttioError {
  status_code: number;
  type: string;
  code: string;
  message: string;
}

Step 3: Environment Configuration

// src/attio/config.ts
export interface AttioConfig {
  apiKey: string;
  baseUrl: string;
  timeout: number;
  environment: "development" | "staging" | "production";
}

export function loadConfig(): AttioConfig {
  const env = process.env.NODE_ENV || "development";
  return {
    apiKey: process.env.ATTIO_API_KEY || "",
    baseUrl: process.env.ATTIO_BASE_URL || "https://api.attio.com/v2",
    timeout: parseInt(process.env.ATTIO_TIMEOUT || "30000", 10),
    environment: env as AttioConfig["environment"],
  };
}

Step 4: Package Scripts for Dev Loop

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "test": "vitest run",
    "test:watch": "vitest --watch",
    "test:integration": "ATTIO_LIVE=1 vitest run tests/integration/",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/ tests/"
  },
  "devDependencies": {
    "tsx": "^4.0.0",
    "vitest": "^2.0.0",
    "typescript": "^5.5.0",
    "msw": "^2.0.0"
  }
}

Step 5: Mock Attio API with MSW

// tests/mocks/attio-fixtures.ts
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";

const BASE = "https://api.attio.com/v2";

export const handlers = [
  // List objects
  http.get(`${BASE}/objects`, () =>
    HttpResponse.json({
      data: [
        { api_slug: "people", singular_noun: "Person", plural_noun: "People" },
        { api_slug: "companies", singular_noun: "Company", plural_noun: "Companies" },
      ],
    })
  ),

  // Query people records
  http.post(`${BASE}/objects/people/records/query`, () =>
    HttpResponse.json({
      data: [
        {
          id: { object_id: "obj_people", record_id: "rec_abc123" },
          created_at: "2025-01-15T10:00:00.000Z",
          values: {
            name: [{ first_name: "Ada", last_name: "Lovelace", full_name: "Ada Lovelace" }],
            email_addresses: [{ email_address: "[email protected]" }],
          },
        },
      ],
    })
  ),

  // Create person
  http.post(`${BASE}/objects/people/records`, async ({ request }) => {
    const body = (await request.json()) as Record<string, unknown>;
    return HttpResponse.json({
      data: {
        id: { object_id: "obj_people", record_id: `rec_${Date.now()}` },
        created_at: new Date().toISOString(),
        values: (body as any).data?.values || {},
      },
    }, { status: 200 });
  }),
];

export const mockServer = setupServer(...handlers);

Step 6: Write Tests Against Mocks

// tests/unit/people.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { mockServer } from "../mocks/attio-fixtures";
import { attioFetch } from "../../src/attio/client";

beforeAll(() => mockServer.listen());
afterAll(() => mockServer.close());

describe("People Service", () => {
  it("queries people records", async () => {
    const res = await attioFetch<{ data: any[] }>({
      method: "POST",
      path: "/objects/people/records/query",
      body: { limit: 10 },
    });
    expect(res.data).toHaveLength(1);
    expect(res.data[0].values.name[0].full_name).toBe("Ada Lovelace");
  });

  it("creates a person", async () => {
    const res = await attioFetch<{ data: { id: { record_id: string } } }>({
      method: "POST",
      path: "/objects/people/records",
      body: {
        data: { values: { email_addresses: ["[email protected]"] } },
      },
    });
    expect(res.data.id.record_id).toBeTruthy();
  });
});

Step 7: Integration Test (Live API)

// tests/integration/attio-live.test.ts
import { describe, it, expect } from "vitest";
import { attioFetch } from "../../src/attio/client";

const LIVE = process.env.ATTIO_LIVE === "1";

describe.skipIf(!LIVE)("Attio Live API", () => {
  it("lists objects from real workspace", async () => {
    const res = await attioFetch<{ data: Array<{ api_slug: string }> }>({
      path: "/objects",
    });
    expect(res.data.map((o) => o.api_slug)).toContain("people");
  });
});

Error Handling

IssueCauseSolution
fetch is not definedNode < 18Upgrade Node.js or add undici
MSW not interceptingWrong base URLMatch ATTIO_BASE_URL in mock handlers
Integration test failsMissing/invalid tokenSet ATTIO_API_KEY in .env.local
TypeScript errors on valuesAttio multiselect arraysValues are always arrays -- type as T[]

Resources

Next Steps

See attio-sdk-patterns for production-ready client patterns.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.