Skip to content
Open
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
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,65 @@ Set this in your `.env` (local) or in your hosting provider's environment variab

## Accessing Mainnet

To access the mainnet facilitator in Next.js, simply install and use the `@coinbase/x402` package.
To access the mainnet facilitator in Next.js, simply install and use the `@coinbase/x402` package.

## Testing Resend Email Integration

The coding agent supports email conversations via Resend. To test this integration:

### 1. Configure Environment Variables

Ensure these environment variables are set in your `.env`:

```bash
RESEND_API_KEY=re_your_api_key_here
# Optional: for webhook signature verification
RESEND_WEBHOOK_SECRET=whsec_your_webhook_secret_here
```

### 2. Set Up Resend Webhook

In your [Resend dashboard](https://resend.com/dashboard/webhooks):

1. Create a new webhook
2. Set the endpoint URL to: `https://<your-host>/api/coding-agent/resend`
3. Select events: `email.sent`, `email.delivered`, `email.opened`, `email.clicked`, `email.bounced`
4. Copy the webhook signing secret and set it as `RESEND_WEBHOOK_SECRET`

### 3. Send a Test Email

Send an email to `[email protected]` with:
- **Subject**: Your request (e.g., "Fix the login bug")
- **Body**: Detailed description of what you want the coding agent to do

The coding agent will:
1. Receive the email via Resend webhook
2. Create a new thread (same as a Slack mention)
3. Process your request
4. Reply via email with the results

### 4. Test Reply Threading

Reply to the agent's email to continue the conversation. The email threading uses standard `Message-ID` and `In-Reply-To` headers, so replies are automatically grouped into the same Chat SDK thread.

### 5. Local Development with ngrok

For local testing, use [ngrok](https://ngrok.com/) to expose your local server:

```bash
# Terminal 1: Start the dev server
pnpm dev

# Terminal 2: Expose via ngrok
npx ngrok http 3000

# Update Resend webhook URL to: https://<ngrok-id>.ngrok.io/api/coding-agent/resend
```

### 6. Verify in Logs

Check the server logs for:
- `POST /api/coding-agent/resend` — Webhook received
- `Syncing monorepo submodules` — Submodule sync before changes
- `Agent completed` — Coding agent finished
- Email sent confirmation via Resend
31 changes: 31 additions & 0 deletions lib/coding-agent/__tests__/bot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ vi.mock("@chat-adapter/github", () => ({
}),
}));

vi.mock("@resend/chat-sdk-adapter", () => ({
createResendAdapter: vi.fn().mockReturnValue({
name: "resend",
}),
}));

vi.mock("@chat-adapter/state-ioredis", () => ({
createIoRedisState: vi.fn().mockReturnValue({
connect: vi.fn(),
Expand Down Expand Up @@ -126,4 +132,29 @@ describe("createCodingAgentBot", () => {
const lastCall = vi.mocked(Chat).mock.calls.at(-1)!;
expect(lastCall[0].userName).toBe("Recoup Agent");
});

it("creates a Chat instance with resend adapter", async () => {
const { Chat } = await import("chat");
const { createCodingAgentBot } = await import("../bot");

createCodingAgentBot();

const lastCall = vi.mocked(Chat).mock.calls.at(-1)!;
const config = lastCall[0];
expect(config.adapters).toHaveProperty("resend");
});

it("creates Resend adapter with correct from address", async () => {
const { createResendAdapter } = await import("@resend/chat-sdk-adapter");
const { createCodingAgentBot } = await import("../bot");

createCodingAgentBot();

expect(createResendAdapter).toHaveBeenCalledWith(
expect.objectContaining({
fromAddress: "[email protected]",
fromName: "Recoup Agent",
}),
);
});
});
15 changes: 13 additions & 2 deletions lib/coding-agent/bot.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Chat, ConsoleLogger } from "chat";
import { SlackAdapter } from "@chat-adapter/slack";
import { createGitHubAdapter } from "@chat-adapter/github";
import { createResendAdapter } from "@resend/chat-sdk-adapter";
import { createIoRedisState } from "@chat-adapter/state-ioredis";
import redis from "@/lib/redis/connection";
import type { CodingAgentThreadState } from "./types";
import { validateCodingAgentEnv } from "./validateEnv";
import { CODING_AGENT_FROM_EMAIL } from "./const";

const logger = new ConsoleLogger();

Expand Down Expand Up @@ -40,9 +42,18 @@ export function createCodingAgentBot() {
logger,
});

return new Chat<{ slack: SlackAdapter; github: ReturnType<typeof createGitHubAdapter> }, CodingAgentThreadState>({
const resend = createResendAdapter({
fromAddress: CODING_AGENT_FROM_EMAIL,
fromName: "Recoup Agent",
});

return new Chat<{
slack: SlackAdapter;
github: ReturnType<typeof createGitHubAdapter>;
resend: ReturnType<typeof createResendAdapter>;
}, CodingAgentThreadState>({
userName: "Recoup Agent",
adapters: { slack, github },
adapters: { slack, github, resend },
state,
});
}
Expand Down
4 changes: 4 additions & 0 deletions lib/coding-agent/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* The from email address used by the coding agent for Resend emails.
*/
export const CODING_AGENT_FROM_EMAIL = "[email protected]";
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@chat-adapter/github": "^4.15.0",
"@chat-adapter/slack": "^4.15.0",
"@chat-adapter/state-ioredis": "^4.15.0",
"@resend/chat-sdk-adapter": "^0.1.0",
"@coinbase/cdp-sdk": "^1.38.6",
"@coinbase/x402": "^0.7.3",
"@composio/core": "^0.3.4",
Expand Down
Loading