npm package: @meet-sudo/sdk
Description: Sudo Server SDK — change-aware observability for Node.js backends
Author: Sudo
Latest version: 0.5.1

# @meet-sudo/sdk

Change-aware observability for Node.js backends. Auto-capture HTTP requests, then add first-class `model_call` and `job` instrumentation so Sudo can explain what changed and what regressed.

## Install

```bash
npm install @meet-sudo/sdk
```

Use your server-side workspace key (`sk_...`) when creating the SDK client. The env var name is up to you; the examples below use `SUDO_API_KEY`.

## Four integration paths

### Tier 1 — Auto-capture middleware (zero effort)

Add one line and Sudo captures every HTTP request, response status, latency, and unhandled error.

**Express**

```ts
import express from "express";
import { SudoServer, sudoExpress } from "@meet-sudo/sdk";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const app = express();

const sudo = sudoExpress({ sudo: ms, service: "api" });
app.use(sudo.middleware);

// ... your routes ...

app.use(sudo.errorHandler);
app.listen(3000);
```

**Fastify**

```ts
import Fastify from "fastify";
import { SudoServer } from "@meet-sudo/sdk";
import { sudoFastify } from "@meet-sudo/sdk/middleware/fastify";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const app = Fastify();

app.register(sudoFastify, { sudo: ms, service: "api" });
app.listen({ port: 3000 });
```

**Koa**

```ts
import Koa from "koa";
import { SudoServer } from "@meet-sudo/sdk";
import { sudoKoa } from "@meet-sudo/sdk/middleware/koa";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const app = new Koa();

const sudo = sudoKoa({ sudo: ms, service: "api" });
app.use(sudo.middleware);

app.listen(3000);
```

**Next.js (App Router)**

Assume `@/lib/meetsudo` exports your initialized `SudoServer` instance.

```ts
// app/api/users/route.ts
import { ms } from "@/lib/meetsudo";
import { withSudo } from "@meet-sudo/sdk";
import { NextResponse } from "next/server";

export const GET = withSudo(ms, { service: "api" }, async (req) => {
  const users = await db.users.findMany();
  return NextResponse.json(users);
});
```

The middleware auto-captures: method, path, status, duration (ms), user agent, IP, and request ID. User ID is extracted from `req.user` or `req.auth` if present. Errors include message and stack trace. These logs are explicitly classified as `event_class="http"` so Sudo can reason about request health without inferring from raw props.

### Tier 2 — Direct logging (manual instrumentation)

For business events, background jobs, cron tasks, and anything the middleware can't see.

```ts
await ms.error("Stripe returned 402", {
  service: "api",
  event: "checkout_failed",
  user_id: "user_123",
  props: { duration_ms: 742, amount: 99 },
});

await ms.warn("Worker retries increased", {
  service: "worker",
  event: "billing_sync_retry",
  event_class: "job",
  props: { retry_count: 3, queue_name: "critical" },
});
await ms.info("Deploy completed", { service: "deploy" });
await ms.debug("Cache miss", { service: "cache" });
```

For custom non-HTTP workloads, set `event_class` explicitly. Otherwise Sudo may store the log row but exclude it from Change Impact and SLO analysis.

### Tier 3 — Native AI and job instrumentation

For the signal types that matter most to Sudo's regression engine, use the first-class helpers instead of generic logs.

```ts
await ms.modelCall({
  service: "assistant-api",
  model: "claude-sonnet-4-20250514",
  provider: "anthropic",
  operation: "messages",
  prompt_version: "support-v12",
  duration_ms: 842,
  ttft_ms: 220,
  tokens_in: 1240,
  tokens_out: 311,
  cost_usd: 0.0182,
  request_id: "req_123",
});

await ms.jobRun({
  service: "worker",
  job_name: "sync_customer_state",
  queue_name: "critical",
  duration_ms: 1934,
  retry_count: 1,
  status: "success",
});
```

You can also time work automatically:

```ts
const completion = await ms.withModelCall({
  service: "assistant-api",
  model: "claude-sonnet-4-20250514",
  provider: "anthropic",
  operation: "chat_completion",
  prompt_version: "onboarding-v8",
  request_id: "req_123",
}, async () => {
  return anthropic.messages.create({ /* ... */ });
});

await ms.withJobRun({
  service: "worker",
  job_name: "rebuild_embeddings",
  queue_name: "embeddings",
}, async () => {
  await rebuildEmbeddingsForWorkspace(workspaceId);
});
```

For the common providers, you can let Sudo extract usage fields from the response automatically. Anthropic is shown first because that is the best-supported path for Sudo today; OpenAI remains available as an optional wrapper.

```ts
const message = await ms.withAnthropicModelCall({
  service: "assistant-api",
  prompt_version: "support-v12",
  model: "claude-sonnet-4-20250514", // optional on success, useful for error cases
}, async () => {
  return anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 400,
    messages: [{ role: "user", content: "Summarize the latest checkout failures" }],
  });
});

const response = await ms.withOpenAIModelCall({
  service: "assistant-api",
  prompt_version: "onboarding-v8",
  model: "gpt-5.4-mini",
}, async () => {
  return openai.responses.create({
    model: "gpt-5.4-mini",
    input: "Explain the regression",
  });
});
```

Those wrappers time the call and, on success, auto-fill:
- `provider`
- `model`
- `tokens_in`
- `tokens_out`
- common provider metadata like `response_id` or `stop_reason`

For job runtimes, you can wrap the processor instead of hand-writing `jobRun(...)` calls:

**BullMQ**

```ts
import { Worker } from "bullmq";
import { SudoServer, instrumentBullMQProcessor } from "@meet-sudo/sdk";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });

new Worker(
  "critical",
  instrumentBullMQProcessor(
    { sudo: ms, service: "worker", worker: "billing-worker" },
    async (job) => {
      await syncCustomerState(job.data);
    }
  )
);
```

**Trigger.dev**

```ts
import { task } from "@trigger.dev/sdk/v3";
import { SudoServer, instrumentTriggerRun } from "@meet-sudo/sdk";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });

export const rebuildEmbeddings = task({
  id: "rebuild-embeddings",
  run: instrumentTriggerRun(
    { sudo: ms, service: "worker" },
    async (payload, ctx) => {
      await rebuildEmbeddingsForWorkspace(payload.workspaceId);
    }
  ),
});
```

Those adapters automatically capture:
- job or task name
- queue name when available
- attempt number
- duration
- success vs failure
- run or job ID when available

### Tier 4 — Logger transports (existing logger integration)

Already using Pino or Winston? Add Sudo as a transport — zero changes to your existing logging code.

**Pino**

```ts
import pino from "pino";
import { SudoServer, createPinoTransport } from "@meet-sudo/sdk";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });

const logger = pino({ level: "info" }, pino.multistream([
  { stream: process.stdout },
  { stream: createPinoTransport({ sudo: ms, service: "api" }) },
]));
```

**Winston**

```ts
import winston from "winston";
import { SudoServer, createWinstonTransport } from "@meet-sudo/sdk";

const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.Stream({
      stream: createWinstonTransport({ sudo: ms, service: "api" }),
    }),
  ],
});
```

## Change markers

```ts
// Mark a deploy
await ms.markDeploy({
  version: "2026.04.03.1",
  scope: "assistant-api",
  metadata: { commit: "4b72f0a" },
});
```

You can also record prompt and model changes:

```ts
await ms.markPromptVersion({
  version: "support-v12",
  scope: "assistant-api",
});

await ms.markModelVersion({
  version: "claude-sonnet-4-20250514",
  scope: "assistant-api",
});
```

## Middleware configuration

All middleware accepts these options:

| Option | Default | Description |
|--------|---------|-------------|
| `sudo` | required | `MeetSudoServer` instance |
| `service` | `undefined` | Service name for logs (e.g. `"api"`) |
| `ignorePaths` | `["/health", "/healthz", ...]` | Paths to skip |
| `extractUserId` | Auto-detect from `req.user`/`req.auth` | Custom user ID extractor |
| `errorStatusThreshold` | `500` | Min status code logged as error |
| `batchSize` | `25` | Logs buffered before flush |
| `flushInterval` | `5000` | Max ms before flushing |

## License

MIT