From 4ee7ae7d329c25b16b283f3b9dd53a4e217c7315 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Feb 2026 11:27:18 -0500 Subject: [PATCH 01/20] feat: include email attachments in agent.generate and tool calls Fetch attachment download URLs from Resend, append them to email text so sandbox/tools can download files, and inject image parts into agent messages so the LLM can visually process images. Co-Authored-By: Claude Opus 4.6 --- lib/chat/validateChatRequest.ts | 2 + .../__tests__/formatAttachmentsText.test.ts | 49 ++++++ .../__tests__/generateEmailResponse.test.ts | 165 ++++++++++++++++++ .../__tests__/getEmailAttachments.test.ts | 108 ++++++++++++ .../__tests__/validateNewEmailMemory.test.ts | 101 ++++++++++- lib/emails/inbound/formatAttachmentsText.ts | 18 ++ lib/emails/inbound/generateEmailResponse.ts | 30 +++- lib/emails/inbound/getEmailAttachments.ts | 30 ++++ lib/emails/inbound/validateNewEmailMemory.ts | 7 +- 9 files changed, 500 insertions(+), 10 deletions(-) create mode 100644 lib/emails/inbound/__tests__/formatAttachmentsText.test.ts create mode 100644 lib/emails/inbound/__tests__/generateEmailResponse.test.ts create mode 100644 lib/emails/inbound/__tests__/getEmailAttachments.test.ts create mode 100644 lib/emails/inbound/formatAttachmentsText.ts create mode 100644 lib/emails/inbound/getEmailAttachments.ts diff --git a/lib/chat/validateChatRequest.ts b/lib/chat/validateChatRequest.ts index 92219e6d..c2c516be 100644 --- a/lib/chat/validateChatRequest.ts +++ b/lib/chat/validateChatRequest.ts @@ -11,6 +11,7 @@ import { getApiKeyDetails } from "@/lib/keys/getApiKeyDetails"; import { validateOrganizationAccess } from "@/lib/organizations/validateOrganizationAccess"; import { setupConversation } from "@/lib/chat/setupConversation"; import { validateMessages } from "@/lib/chat/validateMessages"; +import type { EmailAttachment } from "@/lib/emails/inbound/getEmailAttachments"; export const chatRequestSchema = z .object({ @@ -50,6 +51,7 @@ export type ChatRequestBody = BaseChatRequestBody & { accountId: string; orgId: string | null; authToken?: string; + attachments?: EmailAttachment[]; }; /** diff --git a/lib/emails/inbound/__tests__/formatAttachmentsText.test.ts b/lib/emails/inbound/__tests__/formatAttachmentsText.test.ts new file mode 100644 index 00000000..820a3247 --- /dev/null +++ b/lib/emails/inbound/__tests__/formatAttachmentsText.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { formatAttachmentsText } from "../formatAttachmentsText"; +import type { EmailAttachment } from "../getEmailAttachments"; + +describe("formatAttachmentsText", () => { + it("returns empty string for empty array", () => { + expect(formatAttachmentsText([])).toBe(""); + }); + + it("formats a single attachment", () => { + const attachments: EmailAttachment[] = [ + { + id: "att-1", + filename: "logo.svg", + contentType: "image/svg+xml", + downloadUrl: "https://resend.com/dl/att-1", + }, + ]; + + const result = formatAttachmentsText(attachments); + + expect(result).toBe( + "\n\nAttached files:\n- logo.svg (image/svg+xml): https://resend.com/dl/att-1", + ); + }); + + it("formats multiple attachments", () => { + const attachments: EmailAttachment[] = [ + { + id: "att-1", + filename: "logo.svg", + contentType: "image/svg+xml", + downloadUrl: "https://resend.com/dl/att-1", + }, + { + id: "att-2", + filename: "report.pdf", + contentType: "application/pdf", + downloadUrl: "https://resend.com/dl/att-2", + }, + ]; + + const result = formatAttachmentsText(attachments); + + expect(result).toContain("Attached files:"); + expect(result).toContain("- logo.svg (image/svg+xml): https://resend.com/dl/att-1"); + expect(result).toContain("- report.pdf (application/pdf): https://resend.com/dl/att-2"); + }); +}); diff --git a/lib/emails/inbound/__tests__/generateEmailResponse.test.ts b/lib/emails/inbound/__tests__/generateEmailResponse.test.ts new file mode 100644 index 00000000..ed5216dd --- /dev/null +++ b/lib/emails/inbound/__tests__/generateEmailResponse.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { generateEmailResponse } from "../generateEmailResponse"; +import type { ChatRequestBody } from "@/lib/chat/validateChatRequest"; + +import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; +import { getEmailRoomMessages } from "@/lib/emails/inbound/getEmailRoomMessages"; + +vi.mock("@/lib/agents/generalAgent/getGeneralAgent", () => ({ + default: vi.fn(), +})); + +vi.mock("@/lib/emails/inbound/getEmailRoomMessages", () => ({ + getEmailRoomMessages: vi.fn(), +})); + +vi.mock("@/lib/emails/getEmailFooter", () => ({ + getEmailFooter: vi.fn(() => "
footer
"), +})); + +vi.mock("@/lib/supabase/rooms/selectRoomWithArtist", () => ({ + selectRoomWithArtist: vi.fn(() => ({ artist_name: "Test Artist" })), +})); + +const mockGenerate = vi.fn(); + +describe("generateEmailResponse", () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(getGeneralAgent).mockResolvedValue({ + agent: { generate: mockGenerate }, + } as unknown as Awaited>); + + mockGenerate.mockResolvedValue({ text: "Hello from assistant" }); + }); + + it("throws when roomId is missing", async () => { + const body = { accountId: "acc-1", orgId: null, messages: [] } as ChatRequestBody; + + await expect(generateEmailResponse(body)).rejects.toThrow( + "roomId is required to generate email response HTML", + ); + }); + + it("generates response without attachments", async () => { + vi.mocked(getEmailRoomMessages).mockResolvedValue([{ role: "user", content: "Hi there" }]); + + const body: ChatRequestBody = { + accountId: "acc-1", + orgId: null, + messages: [], + roomId: "room-1", + }; + + const result = await generateEmailResponse(body); + + expect(mockGenerate).toHaveBeenCalledWith({ + messages: [{ role: "user", content: "Hi there" }], + }); + expect(result.text).toBe("Hello from assistant"); + expect(result.html).toContain("Hello from assistant"); + expect(result.html).toContain("
footer
"); + }); + + it("appends image parts to last user message when image attachments exist", async () => { + vi.mocked(getEmailRoomMessages).mockResolvedValue([ + { role: "user", content: "Check this image" }, + ]); + + const body: ChatRequestBody = { + accountId: "acc-1", + orgId: null, + messages: [], + roomId: "room-1", + attachments: [ + { + id: "att-1", + filename: "logo.png", + contentType: "image/png", + downloadUrl: "https://resend.com/dl/att-1", + }, + ], + }; + + await generateEmailResponse(body); + + const callArgs = mockGenerate.mock.calls[0][0]; + const lastUserMsg = callArgs.messages[0]; + + // Should have been converted to parts array with text + image + expect(Array.isArray(lastUserMsg.content)).toBe(true); + expect(lastUserMsg.content[0]).toEqual({ type: "text", text: "Check this image" }); + expect(lastUserMsg.content[1]).toMatchObject({ + type: "image", + mimeType: "image/png", + }); + expect(lastUserMsg.content[1].image).toBeInstanceOf(URL); + expect(lastUserMsg.content[1].image.href).toBe("https://resend.com/dl/att-1"); + }); + + it("does not modify messages when only non-image attachments exist", async () => { + vi.mocked(getEmailRoomMessages).mockResolvedValue([ + { role: "user", content: "Check this file" }, + ]); + + const body: ChatRequestBody = { + accountId: "acc-1", + orgId: null, + messages: [], + roomId: "room-1", + attachments: [ + { + id: "att-1", + filename: "report.pdf", + contentType: "application/pdf", + downloadUrl: "https://resend.com/dl/att-1", + }, + ], + }; + + await generateEmailResponse(body); + + const callArgs = mockGenerate.mock.calls[0][0]; + const lastUserMsg = callArgs.messages[0]; + + // Should remain as plain string (no image parts to add) + expect(lastUserMsg.content).toBe("Check this file"); + }); + + it("appends image parts to the last user message in multi-message conversations", async () => { + vi.mocked(getEmailRoomMessages).mockResolvedValue([ + { role: "user", content: "First message" }, + { role: "assistant", content: "Reply" }, + { role: "user", content: "Here is the image" }, + ]); + + const body: ChatRequestBody = { + accountId: "acc-1", + orgId: null, + messages: [], + roomId: "room-1", + attachments: [ + { + id: "att-1", + filename: "photo.jpg", + contentType: "image/jpeg", + downloadUrl: "https://resend.com/dl/att-1", + }, + ], + }; + + await generateEmailResponse(body); + + const callArgs = mockGenerate.mock.calls[0][0]; + // First user message should be unchanged + expect(callArgs.messages[0].content).toBe("First message"); + // Last user message should have image parts + expect(Array.isArray(callArgs.messages[2].content)).toBe(true); + expect(callArgs.messages[2].content[0]).toEqual({ + type: "text", + text: "Here is the image", + }); + expect(callArgs.messages[2].content[1].type).toBe("image"); + }); +}); diff --git a/lib/emails/inbound/__tests__/getEmailAttachments.test.ts b/lib/emails/inbound/__tests__/getEmailAttachments.test.ts new file mode 100644 index 00000000..537b4dfc --- /dev/null +++ b/lib/emails/inbound/__tests__/getEmailAttachments.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getEmailAttachments } from "../getEmailAttachments"; + +import { getResendClient } from "@/lib/emails/client"; + +vi.mock("@/lib/emails/client", () => ({ + getResendClient: vi.fn(), +})); + +const mockList = vi.fn(); + +describe("getEmailAttachments", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getResendClient).mockReturnValue({ + emails: { + receiving: { + attachments: { + list: mockList, + }, + }, + }, + } as ReturnType); + }); + + it("returns mapped attachments when Resend returns data", async () => { + mockList.mockResolvedValue({ + data: { + data: [ + { + id: "att-1", + filename: "logo.svg", + content_type: "image/svg+xml", + download_url: "https://resend.com/dl/att-1", + size: 1024, + content_disposition: "attachment", + expires_at: "2025-01-01T01:00:00Z", + }, + { + id: "att-2", + filename: "report.pdf", + content_type: "application/pdf", + download_url: "https://resend.com/dl/att-2", + size: 2048, + content_disposition: "attachment", + expires_at: "2025-01-01T01:00:00Z", + }, + ], + }, + }); + + const result = await getEmailAttachments("email-123"); + + expect(mockList).toHaveBeenCalledWith({ emailId: "email-123" }); + expect(result).toEqual([ + { + id: "att-1", + filename: "logo.svg", + contentType: "image/svg+xml", + downloadUrl: "https://resend.com/dl/att-1", + }, + { + id: "att-2", + filename: "report.pdf", + contentType: "application/pdf", + downloadUrl: "https://resend.com/dl/att-2", + }, + ]); + }); + + it("returns empty array when no attachments exist", async () => { + mockList.mockResolvedValue({ data: { data: [] } }); + + const result = await getEmailAttachments("email-123"); + + expect(result).toEqual([]); + }); + + it("returns empty array when data is null", async () => { + mockList.mockResolvedValue({ data: null }); + + const result = await getEmailAttachments("email-123"); + + expect(result).toEqual([]); + }); + + it("defaults filename to 'attachment' when not provided", async () => { + mockList.mockResolvedValue({ + data: { + data: [ + { + id: "att-3", + filename: undefined, + content_type: "application/octet-stream", + download_url: "https://resend.com/dl/att-3", + size: 512, + content_disposition: "attachment", + expires_at: "2025-01-01T01:00:00Z", + }, + ], + }, + }); + + const result = await getEmailAttachments("email-123"); + + expect(result[0].filename).toBe("attachment"); + }); +}); diff --git a/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts b/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts index 911e3221..663ad8ec 100644 --- a/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts +++ b/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts @@ -3,6 +3,13 @@ import { validateNewEmailMemory } from "../validateNewEmailMemory"; import type { ResendEmailReceivedEvent } from "@/lib/emails/validateInboundEmailEvent"; import { NextResponse } from "next/server"; +import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; +import { getEmailContent } from "@/lib/emails/inbound/getEmailContent"; +import { getEmailRoomId } from "@/lib/emails/inbound/getEmailRoomId"; +import { setupConversation } from "@/lib/chat/setupConversation"; +import { getEmailAttachments } from "@/lib/emails/inbound/getEmailAttachments"; +import { formatAttachmentsText } from "@/lib/emails/inbound/formatAttachmentsText"; + vi.mock("@/lib/supabase/account_emails/selectAccountEmails", () => ({ default: vi.fn(), })); @@ -35,17 +42,26 @@ vi.mock("@/lib/const", () => ({ RECOUP_API_KEY: "test-recoup-api-key", })); -import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; -import { getEmailContent } from "@/lib/emails/inbound/getEmailContent"; -import { getEmailRoomId } from "@/lib/emails/inbound/getEmailRoomId"; -import { setupConversation } from "@/lib/chat/setupConversation"; +vi.mock("@/lib/emails/inbound/getEmailAttachments", () => ({ + getEmailAttachments: vi.fn(), +})); + +vi.mock("@/lib/emails/inbound/formatAttachmentsText", () => ({ + formatAttachmentsText: vi.fn(), +})); const MOCK_ACCOUNT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; const MOCK_ROOM_ID = "11111111-2222-3333-4444-555555555555"; const MOCK_EMAIL_ID = "email-123"; const MOCK_MESSAGE_ID = "msg-456"; -function createMockEvent(overrides?: Partial): ResendEmailReceivedEvent { +/** + * + * @param overrides + */ +function createMockEvent( + overrides?: Partial, +): ResendEmailReceivedEvent { return { type: "email.received", created_at: "2024-01-01T00:00:00.000Z", @@ -77,6 +93,10 @@ describe("validateNewEmailMemory", () => { vi.mocked(getEmailRoomId).mockResolvedValue(undefined); vi.mocked(setupConversation).mockResolvedValue({ roomId: MOCK_ROOM_ID }); + + vi.mocked(getEmailAttachments).mockResolvedValue([]); + + vi.mocked(formatAttachmentsText).mockReturnValue(""); }); it("includes authToken from RECOUP_API_KEY in chatRequestBody", async () => { @@ -87,7 +107,10 @@ describe("validateNewEmailMemory", () => { // Should not be a response (duplicate) expect(result).not.toHaveProperty("response"); - const { chatRequestBody } = result as { chatRequestBody: { authToken?: string }; emailText: string }; + const { chatRequestBody } = result as { + chatRequestBody: { authToken?: string }; + emailText: string; + }; expect(chatRequestBody.authToken).toBe("test-recoup-api-key"); }); @@ -95,7 +118,10 @@ describe("validateNewEmailMemory", () => { const event = createMockEvent(); const result = await validateNewEmailMemory(event); - const { chatRequestBody } = result as { chatRequestBody: Record; emailText: string }; + const { chatRequestBody } = result as { + chatRequestBody: Record; + emailText: string; + }; expect(chatRequestBody.accountId).toBe(MOCK_ACCOUNT_ID); expect(chatRequestBody.orgId).toBeNull(); @@ -113,4 +139,65 @@ describe("validateNewEmailMemory", () => { const { response } = result as { response: NextResponse }; expect(response.status).toBe(200); }); + + it("fetches attachments and includes them in chatRequestBody", async () => { + const mockAttachments = [ + { + id: "att-1", + filename: "logo.svg", + contentType: "image/svg+xml", + downloadUrl: "https://resend.com/dl/att-1", + }, + ]; + vi.mocked(getEmailAttachments).mockResolvedValue(mockAttachments); + vi.mocked(formatAttachmentsText).mockReturnValue( + "\n\nAttached files:\n- logo.svg (image/svg+xml): https://resend.com/dl/att-1", + ); + + const event = createMockEvent(); + const result = await validateNewEmailMemory(event); + + expect(result).not.toHaveProperty("response"); + const { chatRequestBody } = result as { + chatRequestBody: Record; + emailText: string; + }; + expect(chatRequestBody.attachments).toEqual(mockAttachments); + expect(getEmailAttachments).toHaveBeenCalledWith(MOCK_EMAIL_ID); + }); + + it("appends attachment URLs to emailText", async () => { + const attachmentText = + "\n\nAttached files:\n- logo.svg (image/svg+xml): https://resend.com/dl/att-1"; + vi.mocked(getEmailAttachments).mockResolvedValue([ + { + id: "att-1", + filename: "logo.svg", + contentType: "image/svg+xml", + downloadUrl: "https://resend.com/dl/att-1", + }, + ]); + vi.mocked(formatAttachmentsText).mockReturnValue(attachmentText); + + const event = createMockEvent(); + const result = await validateNewEmailMemory(event); + + const { emailText } = result as { chatRequestBody: Record; emailText: string }; + expect(emailText).toContain("Attached files:"); + expect(emailText).toContain("https://resend.com/dl/att-1"); + }); + + it("includes empty attachments array when no attachments exist", async () => { + vi.mocked(getEmailAttachments).mockResolvedValue([]); + vi.mocked(formatAttachmentsText).mockReturnValue(""); + + const event = createMockEvent(); + const result = await validateNewEmailMemory(event); + + const { chatRequestBody } = result as { + chatRequestBody: Record; + emailText: string; + }; + expect(chatRequestBody.attachments).toEqual([]); + }); }); diff --git a/lib/emails/inbound/formatAttachmentsText.ts b/lib/emails/inbound/formatAttachmentsText.ts new file mode 100644 index 00000000..9674fcb0 --- /dev/null +++ b/lib/emails/inbound/formatAttachmentsText.ts @@ -0,0 +1,18 @@ +import type { EmailAttachment } from "./getEmailAttachments"; + +/** + * Formats attachment info as text to append to the email body. + * This makes download URLs available to the LLM and any tools + * (sandbox, legacy tools, etc.) via the prompt text. + * + * @param attachments - Array of email attachments with download URLs + * @returns Formatted text block listing attachments, or empty string if none + */ +export function formatAttachmentsText(attachments: EmailAttachment[]): string { + if (!attachments.length) return ""; + + const lines = attachments.map( + att => `- ${att.filename} (${att.contentType}): ${att.downloadUrl}`, + ); + return `\n\nAttached files:\n${lines.join("\n")}`; +} diff --git a/lib/emails/inbound/generateEmailResponse.ts b/lib/emails/inbound/generateEmailResponse.ts index 59edf811..1c9c318f 100644 --- a/lib/emails/inbound/generateEmailResponse.ts +++ b/lib/emails/inbound/generateEmailResponse.ts @@ -1,3 +1,4 @@ +import type { ModelMessage } from "ai"; import { marked } from "marked"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; @@ -8,6 +9,7 @@ import { selectRoomWithArtist } from "@/lib/supabase/rooms/selectRoomWithArtist" /** * Generates the assistant response HTML for an email, including: * - Running the general agent to generate a reply for the given room + * - Appending image attachments as visual content parts to the last user message * - Fetching the room messages * - Appending a standardized footer with reply and link instructions * @@ -17,7 +19,7 @@ import { selectRoomWithArtist } from "@/lib/supabase/rooms/selectRoomWithArtist" export async function generateEmailResponse( body: ChatRequestBody, ): Promise<{ text: string; html: string }> { - const { roomId } = body; + const { roomId, attachments } = body; if (!roomId) { throw new Error("roomId is required to generate email response HTML"); } @@ -25,7 +27,31 @@ export async function generateEmailResponse( const decision = await getGeneralAgent(body); const agent = decision.agent; - const messages = await getEmailRoomMessages(roomId); + const messages: ModelMessage[] = await getEmailRoomMessages(roomId); + + // Append image attachments as visual content parts to the last user message + // so the LLM can visually process images (in addition to having download URLs in text) + if (attachments?.length) { + const imageAttachments = attachments.filter(a => a.contentType.startsWith("image/")); + if (imageAttachments.length) { + const lastUserIdx = messages.findLastIndex(m => m.role === "user"); + if (lastUserIdx >= 0) { + const msg = messages[lastUserIdx]; + const textContent = typeof msg.content === "string" ? msg.content : ""; + const parts: Array<{ type: string; text?: string; image?: URL; mimeType?: string }> = [ + { type: "text", text: textContent }, + ]; + for (const att of imageAttachments) { + parts.push({ + type: "image", + image: new URL(att.downloadUrl), + mimeType: att.contentType, + }); + } + messages[lastUserIdx] = { ...msg, content: parts as ModelMessage["content"] }; + } + } + } const chatResponse = await agent.generate({ messages }); const text = chatResponse.text; diff --git a/lib/emails/inbound/getEmailAttachments.ts b/lib/emails/inbound/getEmailAttachments.ts new file mode 100644 index 00000000..fac16d32 --- /dev/null +++ b/lib/emails/inbound/getEmailAttachments.ts @@ -0,0 +1,30 @@ +import { getResendClient } from "@/lib/emails/client"; + +export interface EmailAttachment { + id: string; + filename: string; + contentType: string; + downloadUrl: string; +} + +/** + * Fetches attachment download URLs for a received email from Resend. + * Webhooks only include attachment metadata (id, filename, content_type), + * so this calls the Attachments API to get download URLs. + * + * @param emailId - The email ID from the Resend webhook event + * @returns Array of attachments with download URLs (empty if none) + */ +export async function getEmailAttachments(emailId: string): Promise { + const resend = getResendClient(); + const { data } = await resend.emails.receiving.attachments.list({ emailId }); + + if (!data?.data?.length) return []; + + return data.data.map(att => ({ + id: att.id, + filename: att.filename || "attachment", + contentType: att.content_type, + downloadUrl: att.download_url, + })); +} diff --git a/lib/emails/inbound/validateNewEmailMemory.ts b/lib/emails/inbound/validateNewEmailMemory.ts index b062d1b0..ffa91d9a 100644 --- a/lib/emails/inbound/validateNewEmailMemory.ts +++ b/lib/emails/inbound/validateNewEmailMemory.ts @@ -9,6 +9,8 @@ import { RECOUP_API_KEY } from "@/lib/const"; import { setupConversation } from "@/lib/chat/setupConversation"; import insertMemoryEmail from "@/lib/supabase/memory_emails/insertMemoryEmail"; import { trimRepliedContext } from "@/lib/emails/inbound/trimRepliedContext"; +import { getEmailAttachments } from "./getEmailAttachments"; +import { formatAttachmentsText } from "./formatAttachmentsText"; /** * Validates and processes a new memory from an inbound email. @@ -29,7 +31,9 @@ export async function validateNewEmailMemory( const accountId = accountEmails[0].account_id; const emailContent = await getEmailContent(emailId); - const emailText = trimRepliedContext(emailContent.html || ""); + const attachments = await getEmailAttachments(emailId); + const emailText = + trimRepliedContext(emailContent.html || "") + formatAttachmentsText(attachments); const roomId = await getEmailRoomId(emailContent); const promptMessage = getMessages(emailText)[0]; @@ -72,6 +76,7 @@ export async function validateNewEmailMemory( messages: getMessages(emailText), roomId: finalRoomId, authToken: RECOUP_API_KEY, + attachments, }; return { chatRequestBody, emailText }; From a9574966cc3f47a22fc0c65cb4fbc5f8fbe19356 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Feb 2026 11:35:18 -0500 Subject: [PATCH 02/20] fix: resolve TypeScript type error in generateEmailResponse Construct a proper UserModelMessage instead of spreading a generic ModelMessage, fixing the discriminated union type incompatibility. Co-Authored-By: Claude Opus 4.6 --- lib/emails/inbound/generateEmailResponse.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/emails/inbound/generateEmailResponse.ts b/lib/emails/inbound/generateEmailResponse.ts index 1c9c318f..87c3fe63 100644 --- a/lib/emails/inbound/generateEmailResponse.ts +++ b/lib/emails/inbound/generateEmailResponse.ts @@ -1,4 +1,4 @@ -import type { ModelMessage } from "ai"; +import type { ModelMessage, UserModelMessage } from "ai"; import { marked } from "marked"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; @@ -38,17 +38,15 @@ export async function generateEmailResponse( if (lastUserIdx >= 0) { const msg = messages[lastUserIdx]; const textContent = typeof msg.content === "string" ? msg.content : ""; - const parts: Array<{ type: string; text?: string; image?: URL; mimeType?: string }> = [ + const parts: UserModelMessage["content"] = [ { type: "text", text: textContent }, - ]; - for (const att of imageAttachments) { - parts.push({ - type: "image", + ...imageAttachments.map(att => ({ + type: "image" as const, image: new URL(att.downloadUrl), mimeType: att.contentType, - }); - } - messages[lastUserIdx] = { ...msg, content: parts as ModelMessage["content"] }; + })), + ]; + messages[lastUserIdx] = { role: "user" as const, content: parts }; } } } From e091fba60511578392fd3e9ed0ec5f47117efaca Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Feb 2026 11:59:18 -0500 Subject: [PATCH 03/20] refactor: remove image parts, rely on download URLs for all file types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove image part injection into agent messages. Attachment download URLs in the email text are sufficient — OpenClaw in the sandbox can fetch any file type directly. This avoids signed URL access issues and unsupported format errors (e.g. SVG) with LLM vision APIs. Co-Authored-By: Claude Opus 4.6 --- lib/chat/validateChatRequest.ts | 2 - .../__tests__/generateEmailResponse.test.ts | 103 +----------------- .../__tests__/validateNewEmailMemory.test.ts | 40 ++----- lib/emails/inbound/generateEmailResponse.ts | 28 +---- lib/emails/inbound/validateNewEmailMemory.ts | 1 - 5 files changed, 10 insertions(+), 164 deletions(-) diff --git a/lib/chat/validateChatRequest.ts b/lib/chat/validateChatRequest.ts index c2c516be..92219e6d 100644 --- a/lib/chat/validateChatRequest.ts +++ b/lib/chat/validateChatRequest.ts @@ -11,7 +11,6 @@ import { getApiKeyDetails } from "@/lib/keys/getApiKeyDetails"; import { validateOrganizationAccess } from "@/lib/organizations/validateOrganizationAccess"; import { setupConversation } from "@/lib/chat/setupConversation"; import { validateMessages } from "@/lib/chat/validateMessages"; -import type { EmailAttachment } from "@/lib/emails/inbound/getEmailAttachments"; export const chatRequestSchema = z .object({ @@ -51,7 +50,6 @@ export type ChatRequestBody = BaseChatRequestBody & { accountId: string; orgId: string | null; authToken?: string; - attachments?: EmailAttachment[]; }; /** diff --git a/lib/emails/inbound/__tests__/generateEmailResponse.test.ts b/lib/emails/inbound/__tests__/generateEmailResponse.test.ts index ed5216dd..faa3b8e9 100644 --- a/lib/emails/inbound/__tests__/generateEmailResponse.test.ts +++ b/lib/emails/inbound/__tests__/generateEmailResponse.test.ts @@ -42,7 +42,7 @@ describe("generateEmailResponse", () => { ); }); - it("generates response without attachments", async () => { + it("generates response with text and footer", async () => { vi.mocked(getEmailRoomMessages).mockResolvedValue([{ role: "user", content: "Hi there" }]); const body: ChatRequestBody = { @@ -61,105 +61,4 @@ describe("generateEmailResponse", () => { expect(result.html).toContain("Hello from assistant"); expect(result.html).toContain("
footer
"); }); - - it("appends image parts to last user message when image attachments exist", async () => { - vi.mocked(getEmailRoomMessages).mockResolvedValue([ - { role: "user", content: "Check this image" }, - ]); - - const body: ChatRequestBody = { - accountId: "acc-1", - orgId: null, - messages: [], - roomId: "room-1", - attachments: [ - { - id: "att-1", - filename: "logo.png", - contentType: "image/png", - downloadUrl: "https://resend.com/dl/att-1", - }, - ], - }; - - await generateEmailResponse(body); - - const callArgs = mockGenerate.mock.calls[0][0]; - const lastUserMsg = callArgs.messages[0]; - - // Should have been converted to parts array with text + image - expect(Array.isArray(lastUserMsg.content)).toBe(true); - expect(lastUserMsg.content[0]).toEqual({ type: "text", text: "Check this image" }); - expect(lastUserMsg.content[1]).toMatchObject({ - type: "image", - mimeType: "image/png", - }); - expect(lastUserMsg.content[1].image).toBeInstanceOf(URL); - expect(lastUserMsg.content[1].image.href).toBe("https://resend.com/dl/att-1"); - }); - - it("does not modify messages when only non-image attachments exist", async () => { - vi.mocked(getEmailRoomMessages).mockResolvedValue([ - { role: "user", content: "Check this file" }, - ]); - - const body: ChatRequestBody = { - accountId: "acc-1", - orgId: null, - messages: [], - roomId: "room-1", - attachments: [ - { - id: "att-1", - filename: "report.pdf", - contentType: "application/pdf", - downloadUrl: "https://resend.com/dl/att-1", - }, - ], - }; - - await generateEmailResponse(body); - - const callArgs = mockGenerate.mock.calls[0][0]; - const lastUserMsg = callArgs.messages[0]; - - // Should remain as plain string (no image parts to add) - expect(lastUserMsg.content).toBe("Check this file"); - }); - - it("appends image parts to the last user message in multi-message conversations", async () => { - vi.mocked(getEmailRoomMessages).mockResolvedValue([ - { role: "user", content: "First message" }, - { role: "assistant", content: "Reply" }, - { role: "user", content: "Here is the image" }, - ]); - - const body: ChatRequestBody = { - accountId: "acc-1", - orgId: null, - messages: [], - roomId: "room-1", - attachments: [ - { - id: "att-1", - filename: "photo.jpg", - contentType: "image/jpeg", - downloadUrl: "https://resend.com/dl/att-1", - }, - ], - }; - - await generateEmailResponse(body); - - const callArgs = mockGenerate.mock.calls[0][0]; - // First user message should be unchanged - expect(callArgs.messages[0].content).toBe("First message"); - // Last user message should have image parts - expect(Array.isArray(callArgs.messages[2].content)).toBe(true); - expect(callArgs.messages[2].content[0]).toEqual({ - type: "text", - text: "Here is the image", - }); - expect(callArgs.messages[2].content[1].type).toBe("image"); - }); }); diff --git a/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts b/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts index 663ad8ec..3b0ebafd 100644 --- a/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts +++ b/lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts @@ -140,33 +140,7 @@ describe("validateNewEmailMemory", () => { expect(response.status).toBe(200); }); - it("fetches attachments and includes them in chatRequestBody", async () => { - const mockAttachments = [ - { - id: "att-1", - filename: "logo.svg", - contentType: "image/svg+xml", - downloadUrl: "https://resend.com/dl/att-1", - }, - ]; - vi.mocked(getEmailAttachments).mockResolvedValue(mockAttachments); - vi.mocked(formatAttachmentsText).mockReturnValue( - "\n\nAttached files:\n- logo.svg (image/svg+xml): https://resend.com/dl/att-1", - ); - - const event = createMockEvent(); - const result = await validateNewEmailMemory(event); - - expect(result).not.toHaveProperty("response"); - const { chatRequestBody } = result as { - chatRequestBody: Record; - emailText: string; - }; - expect(chatRequestBody.attachments).toEqual(mockAttachments); - expect(getEmailAttachments).toHaveBeenCalledWith(MOCK_EMAIL_ID); - }); - - it("appends attachment URLs to emailText", async () => { + it("fetches attachments and appends download URLs to emailText", async () => { const attachmentText = "\n\nAttached files:\n- logo.svg (image/svg+xml): https://resend.com/dl/att-1"; vi.mocked(getEmailAttachments).mockResolvedValue([ @@ -182,22 +156,22 @@ describe("validateNewEmailMemory", () => { const event = createMockEvent(); const result = await validateNewEmailMemory(event); + expect(result).not.toHaveProperty("response"); + expect(getEmailAttachments).toHaveBeenCalledWith(MOCK_EMAIL_ID); + const { emailText } = result as { chatRequestBody: Record; emailText: string }; expect(emailText).toContain("Attached files:"); expect(emailText).toContain("https://resend.com/dl/att-1"); }); - it("includes empty attachments array when no attachments exist", async () => { + it("does not append text when no attachments exist", async () => { vi.mocked(getEmailAttachments).mockResolvedValue([]); vi.mocked(formatAttachmentsText).mockReturnValue(""); const event = createMockEvent(); const result = await validateNewEmailMemory(event); - const { chatRequestBody } = result as { - chatRequestBody: Record; - emailText: string; - }; - expect(chatRequestBody.attachments).toEqual([]); + const { emailText } = result as { chatRequestBody: Record; emailText: string }; + expect(emailText).toBe("

Hello from email

"); }); }); diff --git a/lib/emails/inbound/generateEmailResponse.ts b/lib/emails/inbound/generateEmailResponse.ts index 87c3fe63..59edf811 100644 --- a/lib/emails/inbound/generateEmailResponse.ts +++ b/lib/emails/inbound/generateEmailResponse.ts @@ -1,4 +1,3 @@ -import type { ModelMessage, UserModelMessage } from "ai"; import { marked } from "marked"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; @@ -9,7 +8,6 @@ import { selectRoomWithArtist } from "@/lib/supabase/rooms/selectRoomWithArtist" /** * Generates the assistant response HTML for an email, including: * - Running the general agent to generate a reply for the given room - * - Appending image attachments as visual content parts to the last user message * - Fetching the room messages * - Appending a standardized footer with reply and link instructions * @@ -19,7 +17,7 @@ import { selectRoomWithArtist } from "@/lib/supabase/rooms/selectRoomWithArtist" export async function generateEmailResponse( body: ChatRequestBody, ): Promise<{ text: string; html: string }> { - const { roomId, attachments } = body; + const { roomId } = body; if (!roomId) { throw new Error("roomId is required to generate email response HTML"); } @@ -27,29 +25,7 @@ export async function generateEmailResponse( const decision = await getGeneralAgent(body); const agent = decision.agent; - const messages: ModelMessage[] = await getEmailRoomMessages(roomId); - - // Append image attachments as visual content parts to the last user message - // so the LLM can visually process images (in addition to having download URLs in text) - if (attachments?.length) { - const imageAttachments = attachments.filter(a => a.contentType.startsWith("image/")); - if (imageAttachments.length) { - const lastUserIdx = messages.findLastIndex(m => m.role === "user"); - if (lastUserIdx >= 0) { - const msg = messages[lastUserIdx]; - const textContent = typeof msg.content === "string" ? msg.content : ""; - const parts: UserModelMessage["content"] = [ - { type: "text", text: textContent }, - ...imageAttachments.map(att => ({ - type: "image" as const, - image: new URL(att.downloadUrl), - mimeType: att.contentType, - })), - ]; - messages[lastUserIdx] = { role: "user" as const, content: parts }; - } - } - } + const messages = await getEmailRoomMessages(roomId); const chatResponse = await agent.generate({ messages }); const text = chatResponse.text; diff --git a/lib/emails/inbound/validateNewEmailMemory.ts b/lib/emails/inbound/validateNewEmailMemory.ts index ffa91d9a..ed0dea75 100644 --- a/lib/emails/inbound/validateNewEmailMemory.ts +++ b/lib/emails/inbound/validateNewEmailMemory.ts @@ -76,7 +76,6 @@ export async function validateNewEmailMemory( messages: getMessages(emailText), roomId: finalRoomId, authToken: RECOUP_API_KEY, - attachments, }; return { chatRequestBody, emailText }; From 92a537a32d95a56ef92a46fa65d37b0597320782 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Feb 2026 12:14:50 -0500 Subject: [PATCH 04/20] feat: add Slack-driven coding agent with Chat SDK - Add Chat SDK (chat, @chat-adapter/slack, @chat-adapter/github, @chat-adapter/state-ioredis) - Create bot singleton with Slack + GitHub adapters and ioredis state - Add config for submodule base branches and channel/user allowlists - Add event handlers: onNewMention, onSubscribedMessage, onMergeAction - Add callback handler with secret verification and status dispatching - Add Zod validator for callback payloads - Add webhook route at /api/coding-agent/[platform] for Slack + GitHub - Add callback route at /api/coding-agent/callback for task notifications - Add trigger wrappers: triggerCodingAgent, triggerUpdatePR - 36 tests covering config, bot, handlers, validator, and callback handler Co-Authored-By: Claude Opus 4.6 --- app/api/coding-agent/[platform]/route.ts | 28 + app/api/coding-agent/callback/route.ts | 14 + lib/coding-agent/__tests__/bot.test.ts | 117 ++ lib/coding-agent/__tests__/config.test.ts | 77 ++ .../handleCodingAgentCallback.test.ts | 137 ++ lib/coding-agent/__tests__/handlers.test.ts | 153 +++ .../validateCodingAgentCallback.test.ts | 76 ++ lib/coding-agent/bot.ts | 44 + lib/coding-agent/config.ts | 39 + lib/coding-agent/handleCodingAgentCallback.ts | 94 ++ lib/coding-agent/handlers/onMergeAction.ts | 53 + lib/coding-agent/handlers/onNewMention.ts | 49 + .../handlers/onSubscribedMessage.ts | 37 + lib/coding-agent/handlers/registerHandlers.ts | 12 + lib/coding-agent/types.ts | 21 + .../validateCodingAgentCallback.ts | 50 + lib/trigger/triggerCodingAgent.ts | 18 + lib/trigger/triggerUpdatePR.ts | 22 + package.json | 4 + pnpm-lock.yaml | 1112 +++++++++++++++-- 20 files changed, 2083 insertions(+), 74 deletions(-) create mode 100644 app/api/coding-agent/[platform]/route.ts create mode 100644 app/api/coding-agent/callback/route.ts create mode 100644 lib/coding-agent/__tests__/bot.test.ts create mode 100644 lib/coding-agent/__tests__/config.test.ts create mode 100644 lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts create mode 100644 lib/coding-agent/__tests__/handlers.test.ts create mode 100644 lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts create mode 100644 lib/coding-agent/bot.ts create mode 100644 lib/coding-agent/config.ts create mode 100644 lib/coding-agent/handleCodingAgentCallback.ts create mode 100644 lib/coding-agent/handlers/onMergeAction.ts create mode 100644 lib/coding-agent/handlers/onNewMention.ts create mode 100644 lib/coding-agent/handlers/onSubscribedMessage.ts create mode 100644 lib/coding-agent/handlers/registerHandlers.ts create mode 100644 lib/coding-agent/types.ts create mode 100644 lib/coding-agent/validateCodingAgentCallback.ts create mode 100644 lib/trigger/triggerCodingAgent.ts create mode 100644 lib/trigger/triggerUpdatePR.ts diff --git a/app/api/coding-agent/[platform]/route.ts b/app/api/coding-agent/[platform]/route.ts new file mode 100644 index 00000000..b6e55d0e --- /dev/null +++ b/app/api/coding-agent/[platform]/route.ts @@ -0,0 +1,28 @@ +import type { NextRequest } from "next/server"; +import { after } from "next/server"; +import { codingAgentBot } from "@/lib/coding-agent/bot"; +import "@/lib/coding-agent/handlers/registerHandlers"; + +/** + * POST /api/coding-agent/[platform] + * + * Webhook endpoint for the coding agent bot. + * Handles both Slack and GitHub webhooks via dynamic [platform] segment. + * + * @param request - The incoming webhook request + * @param params.params + * @param params - Route params containing the platform name (slack or github) + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ platform: string }> }, +) { + const { platform } = await params; + const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks]; + + if (!handler) { + return new Response("Unknown platform", { status: 404 }); + } + + return handler(request, { waitUntil: p => after(() => p) }); +} diff --git a/app/api/coding-agent/callback/route.ts b/app/api/coding-agent/callback/route.ts new file mode 100644 index 00000000..46349018 --- /dev/null +++ b/app/api/coding-agent/callback/route.ts @@ -0,0 +1,14 @@ +import type { NextRequest } from "next/server"; +import { handleCodingAgentCallback } from "@/lib/coding-agent/handleCodingAgentCallback"; + +/** + * POST /api/coding-agent/callback + * + * Callback endpoint for the coding agent Trigger.dev task. + * Receives task results and posts them back to the Slack thread. + * + * @param request - The incoming callback request + */ +export async function POST(request: NextRequest) { + return handleCodingAgentCallback(request); +} diff --git a/lib/coding-agent/__tests__/bot.test.ts b/lib/coding-agent/__tests__/bot.test.ts new file mode 100644 index 00000000..29695539 --- /dev/null +++ b/lib/coding-agent/__tests__/bot.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@chat-adapter/slack", () => ({ + SlackAdapter: vi.fn().mockImplementation(() => ({ + name: "slack", + })), +})); + +vi.mock("@chat-adapter/github", () => ({ + GitHubAdapter: vi.fn().mockImplementation(() => ({ + name: "github", + })), +})); + +vi.mock("@chat-adapter/state-ioredis", () => ({ + createIoRedisState: vi.fn().mockReturnValue({ + connect: vi.fn(), + disconnect: vi.fn(), + }), +})); + +vi.mock("@/lib/redis/connection", () => ({ + default: {}, +})); + +vi.mock("chat", () => ({ + Chat: vi.fn().mockImplementation((config: Record) => { + const instance = { + ...config, + webhooks: {}, + onNewMention: vi.fn(), + onSubscribedMessage: vi.fn(), + onAction: vi.fn(), + registerSingleton: vi.fn().mockReturnThis(), + }; + instance.registerSingleton = vi.fn().mockReturnValue(instance); + return instance; + }), +})); + +describe("createCodingAgentBot", () => { + beforeEach(() => { + vi.clearAllMocks(); + process.env.SLACK_BOT_TOKEN = "xoxb-test"; + process.env.SLACK_SIGNING_SECRET = "test-signing-secret"; + process.env.GITHUB_TOKEN = "ghp_test"; + process.env.GITHUB_WEBHOOK_SECRET = "test-webhook-secret"; + process.env.GITHUB_BOT_USERNAME = "recoup-bot"; + }); + + it("creates a Chat instance with slack and github adapters", async () => { + const { Chat } = await import("chat"); + const { createCodingAgentBot } = await import("../bot"); + + createCodingAgentBot(); + + expect(Chat).toHaveBeenCalled(); + const lastCall = vi.mocked(Chat).mock.calls.at(-1)!; + const config = lastCall[0]; + expect(config.adapters).toHaveProperty("slack"); + expect(config.adapters).toHaveProperty("github"); + }); + + it("creates a SlackAdapter with correct config", async () => { + const { SlackAdapter } = await import("@chat-adapter/slack"); + const { createCodingAgentBot } = await import("../bot"); + + createCodingAgentBot(); + + expect(SlackAdapter).toHaveBeenCalledWith( + expect.objectContaining({ + botToken: "xoxb-test", + signingSecret: "test-signing-secret", + }), + ); + }); + + it("creates a GitHubAdapter with correct config", async () => { + const { GitHubAdapter } = await import("@chat-adapter/github"); + const { createCodingAgentBot } = await import("../bot"); + + createCodingAgentBot(); + + expect(GitHubAdapter).toHaveBeenCalledWith( + expect.objectContaining({ + token: "ghp_test", + webhookSecret: "test-webhook-secret", + userName: "recoup-bot", + }), + ); + }); + + it("uses ioredis state adapter with existing Redis client", async () => { + const { createIoRedisState } = await import("@chat-adapter/state-ioredis"); + const redis = (await import("@/lib/redis/connection")).default; + const { createCodingAgentBot } = await import("../bot"); + + createCodingAgentBot(); + + expect(createIoRedisState).toHaveBeenCalledWith( + expect.objectContaining({ + client: redis, + keyPrefix: "coding-agent", + }), + ); + }); + + it("sets userName to Recoup Agent", async () => { + const { Chat } = await import("chat"); + const { createCodingAgentBot } = await import("../bot"); + + createCodingAgentBot(); + + const lastCall = vi.mocked(Chat).mock.calls.at(-1)!; + expect(lastCall[0].userName).toBe("Recoup Agent"); + }); +}); diff --git a/lib/coding-agent/__tests__/config.test.ts b/lib/coding-agent/__tests__/config.test.ts new file mode 100644 index 00000000..14dc2ed3 --- /dev/null +++ b/lib/coding-agent/__tests__/config.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +describe("config", () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("SUBMODULE_CONFIG", () => { + it("maps api to test base branch", async () => { + const { SUBMODULE_CONFIG } = await import("../config"); + expect(SUBMODULE_CONFIG.api.baseBranch).toBe("test"); + }); + + it("maps chat to test base branch", async () => { + const { SUBMODULE_CONFIG } = await import("../config"); + expect(SUBMODULE_CONFIG.chat.baseBranch).toBe("test"); + }); + + it("maps tasks to main base branch", async () => { + const { SUBMODULE_CONFIG } = await import("../config"); + expect(SUBMODULE_CONFIG.tasks.baseBranch).toBe("main"); + }); + + it("maps docs to main base branch", async () => { + const { SUBMODULE_CONFIG } = await import("../config"); + expect(SUBMODULE_CONFIG.docs.baseBranch).toBe("main"); + }); + + it("includes repo URL for each submodule", async () => { + const { SUBMODULE_CONFIG } = await import("../config"); + expect(SUBMODULE_CONFIG.api.repo).toBe("recoupable/recoup-api"); + expect(SUBMODULE_CONFIG.chat.repo).toBe("recoupable/chat"); + expect(SUBMODULE_CONFIG.tasks.repo).toBe("recoupable/tasks"); + }); + }); + + describe("getAllowedChannelIds", () => { + it("parses comma-separated channel IDs from env", async () => { + process.env.CODING_AGENT_CHANNELS = "C123,C456,C789"; + const { getAllowedChannelIds } = await import("../config"); + expect(getAllowedChannelIds()).toEqual(["C123", "C456", "C789"]); + }); + + it("returns empty array when env is not set", async () => { + delete process.env.CODING_AGENT_CHANNELS; + const { getAllowedChannelIds } = await import("../config"); + expect(getAllowedChannelIds()).toEqual([]); + }); + + it("trims whitespace from channel IDs", async () => { + process.env.CODING_AGENT_CHANNELS = " C123 , C456 "; + const { getAllowedChannelIds } = await import("../config"); + expect(getAllowedChannelIds()).toEqual(["C123", "C456"]); + }); + }); + + describe("getAllowedUserIds", () => { + it("parses comma-separated user IDs from env", async () => { + process.env.CODING_AGENT_USERS = "U111,U222"; + const { getAllowedUserIds } = await import("../config"); + expect(getAllowedUserIds()).toEqual(["U111", "U222"]); + }); + + it("returns empty array when env is not set", async () => { + delete process.env.CODING_AGENT_USERS; + const { getAllowedUserIds } = await import("../config"); + expect(getAllowedUserIds()).toEqual([]); + }); + }); +}); diff --git a/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts b/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts new file mode 100644 index 00000000..05dfe22a --- /dev/null +++ b/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@/lib/networking/getCorsHeaders", () => ({ + getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), +})); + +const mockPost = vi.fn(); +const mockSetState = vi.fn(); + +vi.mock("chat", () => { + const ThreadImpl = vi.fn().mockImplementation(() => ({ + post: mockPost, + setState: mockSetState, + })); + return { + ThreadImpl, + deriveChannelId: vi.fn((_, threadId: string) => { + const parts = threadId.split(":"); + return `${parts[0]}:${parts[1]}`; + }), + }; +}); + +vi.mock("../bot", () => ({ + codingAgentBot: {}, +})); + +const { handleCodingAgentCallback } = await import("../handleCodingAgentCallback"); + +beforeEach(() => { + vi.clearAllMocks(); + process.env.CODING_AGENT_CALLBACK_SECRET = "test-secret"; +}); + +describe("handleCodingAgentCallback", () => { + /** + * + * @param body + * @param secret + */ + function makeRequest(body: unknown, secret = "test-secret") { + return { + json: () => Promise.resolve(body), + headers: new Headers({ + "x-callback-secret": secret, + }), + } as unknown as Request; + } + + it("returns 401 when secret header is missing", async () => { + const request = { + json: () => Promise.resolve({}), + headers: new Headers(), + } as unknown as Request; + + const response = await handleCodingAgentCallback(request); + expect(response.status).toBe(401); + }); + + it("returns 401 when secret header is wrong", async () => { + const request = makeRequest({}, "wrong-secret"); + const response = await handleCodingAgentCallback(request); + expect(response.status).toBe(401); + }); + + it("returns 400 for invalid body", async () => { + const request = makeRequest({ invalid: true }); + const response = await handleCodingAgentCallback(request); + expect(response.status).toBe(400); + }); + + it("posts PR links for pr_created status", async () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "pr_created", + branch: "agent/fix-bug-1234", + snapshotId: "snap_abc", + prs: [ + { + repo: "recoupable/recoup-api", + number: 42, + url: "https://github.com/recoupable/recoup-api/pull/42", + baseBranch: "test", + }, + ], + }; + const request = makeRequest(body); + + const response = await handleCodingAgentCallback(request); + + expect(response.status).toBe(200); + expect(mockPost).toHaveBeenCalled(); + expect(mockSetState).toHaveBeenCalledWith(expect.objectContaining({ status: "pr_created" })); + }); + + it("posts no-changes message for no_changes status", async () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "no_changes", + message: "No files modified", + }; + const request = makeRequest(body); + + const response = await handleCodingAgentCallback(request); + + expect(response.status).toBe(200); + expect(mockPost).toHaveBeenCalledWith(expect.stringContaining("No changes")); + }); + + it("posts error message for failed status", async () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "failed", + message: "Sandbox timed out", + }; + const request = makeRequest(body); + + const response = await handleCodingAgentCallback(request); + + expect(response.status).toBe(200); + expect(mockPost).toHaveBeenCalledWith(expect.stringContaining("Sandbox timed out")); + }); + + it("posts updated confirmation for updated status", async () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "updated", + snapshotId: "snap_new", + }; + const request = makeRequest(body); + + const response = await handleCodingAgentCallback(request); + + expect(response.status).toBe(200); + expect(mockPost).toHaveBeenCalledWith(expect.stringContaining("updated")); + }); +}); diff --git a/lib/coding-agent/__tests__/handlers.test.ts b/lib/coding-agent/__tests__/handlers.test.ts new file mode 100644 index 00000000..d69c9087 --- /dev/null +++ b/lib/coding-agent/__tests__/handlers.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@/lib/trigger/triggerCodingAgent", () => ({ + triggerCodingAgent: vi.fn().mockResolvedValue({ id: "run_123" }), +})); + +vi.mock("@/lib/trigger/triggerUpdatePR", () => ({ + triggerUpdatePR: vi.fn().mockResolvedValue({ id: "run_456" }), +})); + +const { registerOnNewMention } = await import("../handlers/onNewMention"); +const { registerOnSubscribedMessage } = await import("../handlers/onSubscribedMessage"); +const { registerOnMergeAction } = await import("../handlers/onMergeAction"); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +/** + * + */ +function createMockBot() { + return { + onNewMention: vi.fn(), + onSubscribedMessage: vi.fn(), + onAction: vi.fn(), + } as any; +} + +describe("registerOnNewMention", () => { + it("registers a handler on the bot", () => { + const bot = createMockBot(); + registerOnNewMention(bot); + expect(bot.onNewMention).toHaveBeenCalledOnce(); + }); + + it("posts acknowledgment and triggers coding agent task", async () => { + process.env.CODING_AGENT_CHANNELS = ""; + process.env.CODING_AGENT_USERS = ""; + + const bot = createMockBot(); + registerOnNewMention(bot); + const handler = bot.onNewMention.mock.calls[0][0]; + + const mockThread = { + id: "slack:C123:1234567890.123456", + subscribe: vi.fn(), + post: vi.fn(), + setState: vi.fn(), + }; + const mockMessage = { + text: "fix the login bug in the api", + author: { id: "U111" }, + }; + + await handler(mockThread, mockMessage); + + expect(mockThread.subscribe).toHaveBeenCalledOnce(); + expect(mockThread.post).toHaveBeenCalledWith(expect.stringContaining("Starting work")); + expect(mockThread.setState).toHaveBeenCalledWith( + expect.objectContaining({ + status: "running", + prompt: "fix the login bug in the api", + }), + ); + }); + + it("rejects mentions from non-allowed channels", async () => { + process.env.CODING_AGENT_CHANNELS = "C999"; + process.env.CODING_AGENT_USERS = ""; + + const bot = createMockBot(); + registerOnNewMention(bot); + const handler = bot.onNewMention.mock.calls[0][0]; + + const mockThread = { + id: "slack:C123:1234567890.123456", + subscribe: vi.fn(), + post: vi.fn(), + setState: vi.fn(), + }; + + await handler(mockThread, { text: "hi", author: { id: "U111" } }); + + expect(mockThread.subscribe).not.toHaveBeenCalled(); + }); +}); + +describe("registerOnSubscribedMessage", () => { + it("registers a handler on the bot", () => { + const bot = createMockBot(); + registerOnSubscribedMessage(bot); + expect(bot.onSubscribedMessage).toHaveBeenCalledOnce(); + }); + + it("triggers update PR task when status is pr_created", async () => { + const { triggerUpdatePR } = await import("@/lib/trigger/triggerUpdatePR"); + + const bot = createMockBot(); + registerOnSubscribedMessage(bot); + const handler = bot.onSubscribedMessage.mock.calls[0][0]; + + const mockThread = { + id: "slack:C123:1234567890.123456", + state: Promise.resolve({ + status: "pr_created", + prompt: "fix bug", + snapshotId: "snap_abc", + branch: "agent/fix-bug", + prs: [{ repo: "recoupable/recoup-api", number: 1, url: "url", baseBranch: "test" }], + }), + post: vi.fn(), + setState: vi.fn(), + }; + + await handler(mockThread, { text: "make the button blue", author: { id: "U111" } }); + + expect(triggerUpdatePR).toHaveBeenCalledWith( + expect.objectContaining({ + feedback: "make the button blue", + snapshotId: "snap_abc", + }), + ); + expect(mockThread.setState).toHaveBeenCalledWith( + expect.objectContaining({ status: "updating" }), + ); + }); + + it("tells user to wait when agent is running", async () => { + const bot = createMockBot(); + registerOnSubscribedMessage(bot); + const handler = bot.onSubscribedMessage.mock.calls[0][0]; + + const mockThread = { + id: "slack:C123:1234567890.123456", + state: Promise.resolve({ status: "running", prompt: "fix bug" }), + post: vi.fn(), + setState: vi.fn(), + }; + + await handler(mockThread, { text: "hurry up", author: { id: "U111" } }); + + expect(mockThread.post).toHaveBeenCalledWith(expect.stringContaining("still working")); + }); +}); + +describe("registerOnMergeAction", () => { + it("registers merge_all_prs action handler", () => { + const bot = createMockBot(); + registerOnMergeAction(bot); + expect(bot.onAction).toHaveBeenCalledWith("merge_all_prs", expect.any(Function)); + }); +}); diff --git a/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts b/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts new file mode 100644 index 00000000..525aa4ff --- /dev/null +++ b/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from "vitest"; +import { NextResponse } from "next/server"; +import { validateCodingAgentCallback } from "../validateCodingAgentCallback"; + +describe("validateCodingAgentCallback", () => { + describe("valid payloads", () => { + it("accepts pr_created status with prs", () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "pr_created", + branch: "agent/fix-bug-1234", + snapshotId: "snap_abc123", + prs: [ + { + repo: "recoupable/recoup-api", + number: 42, + url: "https://github.com/recoupable/recoup-api/pull/42", + baseBranch: "test", + }, + ], + }; + const result = validateCodingAgentCallback(body); + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual(body); + }); + + it("accepts no_changes status", () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "no_changes", + message: "No files were modified", + }; + const result = validateCodingAgentCallback(body); + expect(result).not.toBeInstanceOf(NextResponse); + }); + + it("accepts failed status with message", () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "failed", + message: "Sandbox timed out", + }; + const result = validateCodingAgentCallback(body); + expect(result).not.toBeInstanceOf(NextResponse); + }); + + it("accepts updated status with new snapshotId", () => { + const body = { + threadId: "slack:C123:1234567890.123456", + status: "updated", + snapshotId: "snap_new456", + }; + const result = validateCodingAgentCallback(body); + expect(result).not.toBeInstanceOf(NextResponse); + }); + }); + + describe("invalid payloads", () => { + it("rejects missing threadId", () => { + const body = { status: "no_changes" }; + const result = validateCodingAgentCallback(body); + expect(result).toBeInstanceOf(NextResponse); + }); + + it("rejects invalid status value", () => { + const body = { threadId: "slack:C123:123", status: "unknown" }; + const result = validateCodingAgentCallback(body); + expect(result).toBeInstanceOf(NextResponse); + }); + + it("rejects empty object", () => { + const result = validateCodingAgentCallback({}); + expect(result).toBeInstanceOf(NextResponse); + }); + }); +}); diff --git a/lib/coding-agent/bot.ts b/lib/coding-agent/bot.ts new file mode 100644 index 00000000..8869bbed --- /dev/null +++ b/lib/coding-agent/bot.ts @@ -0,0 +1,44 @@ +import { Chat } from "chat"; +import { SlackAdapter } from "@chat-adapter/slack"; +import { GitHubAdapter } from "@chat-adapter/github"; +import { createIoRedisState } from "@chat-adapter/state-ioredis"; +import redis from "@/lib/redis/connection"; +import type { CodingAgentThreadState } from "./types"; + +/** + * Creates a new Chat bot instance configured with Slack and GitHub adapters. + */ +export function createCodingAgentBot() { + const state = createIoRedisState({ + client: redis, + keyPrefix: "coding-agent", + logger: console, + }); + + const slack = new SlackAdapter({ + botToken: process.env.SLACK_BOT_TOKEN!, + signingSecret: process.env.SLACK_SIGNING_SECRET!, + logger: console, + }); + + const github = new GitHubAdapter({ + token: process.env.GITHUB_TOKEN!, + webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!, + userName: process.env.GITHUB_BOT_USERNAME!, + logger: console, + }); + + return new Chat<{ slack: SlackAdapter; github: GitHubAdapter }, CodingAgentThreadState>({ + userName: "Recoup Agent", + adapters: { slack, github }, + state, + }); +} + +export type CodingAgentBot = ReturnType; + +/** + * Singleton bot instance. Registers as the Chat SDK singleton + * so ThreadImpl can resolve adapters lazily from thread IDs. + */ +export const codingAgentBot = createCodingAgentBot().registerSingleton(); diff --git a/lib/coding-agent/config.ts b/lib/coding-agent/config.ts new file mode 100644 index 00000000..7ca48f41 --- /dev/null +++ b/lib/coding-agent/config.ts @@ -0,0 +1,39 @@ +/** + * Submodule configuration mapping for PR creation. + * Defines the GitHub repo and base branch for each submodule. + */ +export const SUBMODULE_CONFIG: Record = { + api: { repo: "recoupable/recoup-api", baseBranch: "test" }, + chat: { repo: "recoupable/chat", baseBranch: "test" }, + tasks: { repo: "recoupable/tasks", baseBranch: "main" }, + docs: { repo: "recoupable/docs", baseBranch: "main" }, + database: { repo: "recoupable/database", baseBranch: "main" }, + remotion: { repo: "recoupable/remotion", baseBranch: "main" }, + bash: { repo: "recoupable/bash", baseBranch: "main" }, + skills: { repo: "recoupable/skills", baseBranch: "main" }, + cli: { repo: "recoupable/cli", baseBranch: "main" }, +}; + +/** + * Returns the list of allowed Slack channel IDs from the environment. + */ +export function getAllowedChannelIds(): string[] { + const raw = process.env.CODING_AGENT_CHANNELS; + if (!raw) return []; + return raw + .split(",") + .map(id => id.trim()) + .filter(Boolean); +} + +/** + * Returns the list of allowed Slack user IDs from the environment. + */ +export function getAllowedUserIds(): string[] { + const raw = process.env.CODING_AGENT_USERS; + if (!raw) return []; + return raw + .split(",") + .map(id => id.trim()) + .filter(Boolean); +} diff --git a/lib/coding-agent/handleCodingAgentCallback.ts b/lib/coding-agent/handleCodingAgentCallback.ts new file mode 100644 index 00000000..4c370ffa --- /dev/null +++ b/lib/coding-agent/handleCodingAgentCallback.ts @@ -0,0 +1,94 @@ +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { validateCodingAgentCallback } from "./validateCodingAgentCallback"; +import type { CodingAgentCallbackBody } from "./validateCodingAgentCallback"; +import { ThreadImpl } from "chat"; +import type { CodingAgentThreadState } from "./types"; + +/** + * Reconstructs a Thread from a stored thread ID using the Chat SDK singleton. + * + * @param threadId + */ +function getThread(threadId: string) { + const adapterName = threadId.split(":")[0]; + const channelId = `${adapterName}:${threadId.split(":")[1]}`; + return new ThreadImpl({ + adapterName, + id: threadId, + channelId, + }); +} + +/** + * Handles the pr_created callback status. + * + * @param threadId + * @param body + */ +async function handlePRCreated(threadId: string, body: CodingAgentCallbackBody) { + const thread = getThread(threadId); + const prLinks = (body.prs ?? []) + .map(pr => `- [${pr.repo}#${pr.number}](${pr.url}) → \`${pr.baseBranch}\``) + .join("\n"); + + await thread.post( + `PRs created:\n${prLinks}\n\nReply in this thread to give feedback, or click Merge when ready.`, + ); + + await thread.setState({ + status: "pr_created", + branch: body.branch, + snapshotId: body.snapshotId, + prs: body.prs, + }); +} + +/** + * Handles coding agent task callback from Trigger.dev. + * Verifies the shared secret and dispatches based on callback status. + * + * @param request - The incoming callback request + * @returns A NextResponse + */ +export async function handleCodingAgentCallback(request: Request): Promise { + const secret = request.headers.get("x-callback-secret"); + const expectedSecret = process.env.CODING_AGENT_CALLBACK_SECRET; + + if (!secret || secret !== expectedSecret) { + return NextResponse.json( + { status: "error", error: "Unauthorized" }, + { status: 401, headers: getCorsHeaders() }, + ); + } + + const body = await request.json(); + const validated = validateCodingAgentCallback(body); + + if (validated instanceof NextResponse) { + return validated; + } + + const thread = getThread(validated.threadId); + + switch (validated.status) { + case "pr_created": + await handlePRCreated(validated.threadId, validated); + break; + + case "no_changes": + await thread.post("No changes were detected. The agent didn't modify any files."); + break; + + case "failed": + await thread.post(`Agent failed: ${validated.message ?? "Unknown error"}`); + break; + + case "updated": + await thread.setState({ snapshotId: validated.snapshotId }); + await thread.post("PRs updated with your feedback. Review the latest commits."); + break; + } + + return NextResponse.json({ status: "ok" }, { headers: getCorsHeaders() }); +} diff --git a/lib/coding-agent/handlers/onMergeAction.ts b/lib/coding-agent/handlers/onMergeAction.ts new file mode 100644 index 00000000..67279b1d --- /dev/null +++ b/lib/coding-agent/handlers/onMergeAction.ts @@ -0,0 +1,53 @@ +import type { CodingAgentBot } from "../bot"; + +/** + * Registers the "Merge All PRs" button action handler on the bot. + * Squash-merges each PR via the GitHub API. + * + * @param bot + */ +export function registerOnMergeAction(bot: CodingAgentBot) { + bot.onAction("merge_all_prs", async event => { + const thread = event.thread; + const state = await thread.state; + + if (!state?.prs?.length) { + await thread.post("No PRs to merge."); + return; + } + + const token = process.env.GITHUB_TOKEN; + if (!token) { + await thread.post("Missing GITHUB_TOKEN — cannot merge PRs."); + return; + } + + const results: string[] = []; + + for (const pr of state.prs) { + const [owner, repo] = pr.repo.split("/"); + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pr.number}/merge`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + body: JSON.stringify({ merge_method: "squash" }), + }, + ); + + if (response.ok) { + results.push(`${pr.repo}#${pr.number} merged`); + } else { + const error = await response.json(); + results.push(`${pr.repo}#${pr.number} failed: ${error.message}`); + } + } + + await thread.setState({ status: "merged" }); + await thread.post(`Merge results:\n${results.map(r => `- ${r}`).join("\n")}`); + }); +} diff --git a/lib/coding-agent/handlers/onNewMention.ts b/lib/coding-agent/handlers/onNewMention.ts new file mode 100644 index 00000000..b6f1b6e7 --- /dev/null +++ b/lib/coding-agent/handlers/onNewMention.ts @@ -0,0 +1,49 @@ +import type { CodingAgentBot } from "../bot"; +import { getAllowedChannelIds, getAllowedUserIds } from "../config"; +import { triggerCodingAgent } from "@/lib/trigger/triggerCodingAgent"; +import type { CodingAgentThreadState } from "../types"; + +/** + * Registers the onNewMention handler on the bot. + * Validates channel/user against allowlist, subscribes to the thread, + * and triggers the coding agent Trigger.dev task. + * + * @param bot + */ +export function registerOnNewMention(bot: CodingAgentBot) { + bot.onNewMention(async (thread, message) => { + const allowedChannels = getAllowedChannelIds(); + const allowedUsers = getAllowedUserIds(); + + if (allowedChannels.length > 0) { + const channelId = thread.id.split(":")[1]; + if (!allowedChannels.includes(channelId)) { + return; + } + } + + if (allowedUsers.length > 0) { + const userId = message.author.id; + if (!allowedUsers.includes(userId)) { + return; + } + } + + const prompt = message.text; + + await thread.subscribe(); + await thread.post(`Starting work on: "${prompt}"\n\nI'll reply here when done.`); + + const handle = await triggerCodingAgent({ + prompt, + callbackThreadId: thread.id, + }); + + await thread.setState({ + status: "running", + prompt, + runId: handle.id, + slackThreadId: thread.id, + } as Partial); + }); +} diff --git a/lib/coding-agent/handlers/onSubscribedMessage.ts b/lib/coding-agent/handlers/onSubscribedMessage.ts new file mode 100644 index 00000000..bca90fc3 --- /dev/null +++ b/lib/coding-agent/handlers/onSubscribedMessage.ts @@ -0,0 +1,37 @@ +import type { CodingAgentBot } from "../bot"; +import { triggerUpdatePR } from "@/lib/trigger/triggerUpdatePR"; + +/** + * Registers the onSubscribedMessage handler on the bot. + * If the agent has created PRs, treats the message as feedback and + * triggers the update-pr task. If the agent is currently working, + * tells the user to wait. + * + * @param bot + */ +export function registerOnSubscribedMessage(bot: CodingAgentBot) { + bot.onSubscribedMessage(async (thread, message) => { + const state = await thread.state; + + if (!state) return; + + if (state.status === "running" || state.status === "updating") { + await thread.post("I'm still working on this. I'll let you know when I'm done."); + return; + } + + if (state.status === "pr_created" && state.snapshotId && state.branch && state.prs) { + await thread.post("Got your feedback. Updating the PRs..."); + + await thread.setState({ status: "updating" }); + + await triggerUpdatePR({ + feedback: message.text, + snapshotId: state.snapshotId, + branch: state.branch, + prs: state.prs, + callbackThreadId: thread.id, + }); + } + }); +} diff --git a/lib/coding-agent/handlers/registerHandlers.ts b/lib/coding-agent/handlers/registerHandlers.ts new file mode 100644 index 00000000..96f24748 --- /dev/null +++ b/lib/coding-agent/handlers/registerHandlers.ts @@ -0,0 +1,12 @@ +import { codingAgentBot } from "../bot"; +import { registerOnNewMention } from "./onNewMention"; +import { registerOnSubscribedMessage } from "./onSubscribedMessage"; +import { registerOnMergeAction } from "./onMergeAction"; + +/** + * Registers all coding agent event handlers on the bot singleton. + * Import this file once to attach handlers to the bot. + */ +registerOnNewMention(codingAgentBot); +registerOnSubscribedMessage(codingAgentBot); +registerOnMergeAction(codingAgentBot); diff --git a/lib/coding-agent/types.ts b/lib/coding-agent/types.ts new file mode 100644 index 00000000..716a0637 --- /dev/null +++ b/lib/coding-agent/types.ts @@ -0,0 +1,21 @@ +/** + * Thread state for the coding agent bot. + * Stored in Redis via Chat SDK's state adapter. + */ +export interface CodingAgentThreadState { + status: "running" | "pr_created" | "updating" | "merged" | "failed"; + prompt: string; + runId?: string; + sandboxId?: string; + snapshotId?: string; + branch?: string; + prs?: CodingAgentPR[]; + slackThreadId?: string; +} + +export interface CodingAgentPR { + repo: string; + number: number; + url: string; + baseBranch: string; +} diff --git a/lib/coding-agent/validateCodingAgentCallback.ts b/lib/coding-agent/validateCodingAgentCallback.ts new file mode 100644 index 00000000..57989f04 --- /dev/null +++ b/lib/coding-agent/validateCodingAgentCallback.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { z } from "zod"; + +const codingAgentPRSchema = z.object({ + repo: z.string(), + number: z.number(), + url: z.string(), + baseBranch: z.string(), +}); + +export const codingAgentCallbackSchema = z.object({ + threadId: z.string({ message: "threadId is required" }).min(1, "threadId cannot be empty"), + status: z.enum(["pr_created", "no_changes", "failed", "updated"]), + branch: z.string().optional(), + snapshotId: z.string().optional(), + prs: z.array(codingAgentPRSchema).optional(), + message: z.string().optional(), + stdout: z.string().optional(), + stderr: z.string().optional(), +}); + +export type CodingAgentCallbackBody = z.infer; + +/** + * Validates the coding agent callback body against the expected schema. + * + * @param body - The parsed JSON body of the callback request. + * @returns A NextResponse with an error if validation fails, or the validated body if validation passes. + */ +export function validateCodingAgentCallback(body: unknown): NextResponse | CodingAgentCallbackBody { + const result = codingAgentCallbackSchema.safeParse(body); + + if (!result.success) { + const firstError = result.error.issues[0]; + return NextResponse.json( + { + status: "error", + missing_fields: firstError.path, + error: firstError.message, + }, + { + status: 400, + headers: getCorsHeaders(), + }, + ); + } + + return result.data; +} diff --git a/lib/trigger/triggerCodingAgent.ts b/lib/trigger/triggerCodingAgent.ts new file mode 100644 index 00000000..91fe0cb8 --- /dev/null +++ b/lib/trigger/triggerCodingAgent.ts @@ -0,0 +1,18 @@ +import { tasks } from "@trigger.dev/sdk"; + +type CodingAgentPayload = { + prompt: string; + callbackThreadId: string; +}; + +/** + * Triggers the coding-agent task to spin up a sandbox, clone the monorepo, + * run the AI agent, and create PRs for any changes. + * + * @param payload - The task payload with prompt and callback thread ID + * @returns The task handle with runId + */ +export async function triggerCodingAgent(payload: CodingAgentPayload) { + const handle = await tasks.trigger("coding-agent", payload); + return handle; +} diff --git a/lib/trigger/triggerUpdatePR.ts b/lib/trigger/triggerUpdatePR.ts new file mode 100644 index 00000000..67387c2c --- /dev/null +++ b/lib/trigger/triggerUpdatePR.ts @@ -0,0 +1,22 @@ +import { tasks } from "@trigger.dev/sdk"; +import type { CodingAgentPR } from "@/lib/coding-agent/types"; + +type UpdatePRPayload = { + feedback: string; + snapshotId: string; + branch: string; + prs: CodingAgentPR[]; + callbackThreadId: string; +}; + +/** + * Triggers the update-pr task to resume a sandbox from snapshot, + * apply feedback via the AI agent, and push updates to existing PRs. + * + * @param payload - The task payload with feedback, snapshot, branch, and PR info + * @returns The task handle with runId + */ +export async function triggerUpdatePR(payload: UpdatePRPayload) { + const handle = await tasks.trigger("update-pr", payload); + return handle; +} diff --git a/package.json b/package.json index c53bb782..e2c8ba47 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "@ai-sdk/google": "^3.0.8", "@ai-sdk/mcp": "^0.0.12", "@ai-sdk/openai": "^3.0.10", + "@chat-adapter/github": "^4.15.0", + "@chat-adapter/slack": "^4.15.0", + "@chat-adapter/state-ioredis": "^4.15.0", "@coinbase/cdp-sdk": "^1.38.6", "@coinbase/x402": "^0.7.3", "@composio/core": "^0.3.4", @@ -36,6 +39,7 @@ "autoevals": "^0.0.129", "braintrust": "^0.4.9", "bullmq": "^5.65.1", + "chat": "^4.15.0", "googleapis": "^168.0.0", "ioredis": "^5.8.2", "marked": "^15.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0b0d314..b7c591bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,18 +23,27 @@ importers: '@ai-sdk/openai': specifier: ^3.0.10 version: 3.0.10(zod@4.1.13) + '@chat-adapter/github': + specifier: ^4.15.0 + version: 4.15.0 + '@chat-adapter/slack': + specifier: ^4.15.0 + version: 4.15.0 + '@chat-adapter/state-ioredis': + specifier: ^4.15.0 + version: 4.15.0 '@coinbase/cdp-sdk': specifier: ^1.38.6 - version: 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@coinbase/x402': specifier: ^0.7.3 - version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@composio/core': specifier: ^0.3.4 - version: 0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + version: 0.3.4(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@composio/vercel': specifier: ^0.3.4 - version: 0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13)) + version: 0.3.4(@composio/core@0.3.4(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13)) '@modelcontextprotocol/sdk': specifier: ^1.24.3 version: 1.24.3(zod@4.1.13) @@ -61,13 +70,16 @@ importers: version: 1.15.7 autoevals: specifier: ^0.0.129 - version: 0.0.129(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.0.129(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) braintrust: specifier: ^0.4.9 version: 0.4.10(zod@4.1.13) bullmq: specifier: ^5.65.1 version: 5.65.1 + chat: + specifier: ^4.15.0 + version: 4.15.0 googleapis: specifier: ^168.0.0 version: 168.0.0 @@ -109,10 +121,10 @@ importers: version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13) x402-fetch: specifier: ^0.7.3 - version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) x402-next: specifier: ^0.7.3 - version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^4.1.13 version: 4.1.13 @@ -285,6 +297,18 @@ packages: '@bugsnag/cuid@3.2.1': resolution: {integrity: sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==} + '@chat-adapter/github@4.15.0': + resolution: {integrity: sha512-qoQHwSRcxsh2LiYxGf2aO7y8H2BIYn85vZmT7LXbEHEumtA5lDWqs/PrHhFenTAsW7bEhkadkpDJYeB18nkr0Q==} + + '@chat-adapter/shared@4.15.0': + resolution: {integrity: sha512-otIS9zf0wY3DMc5WGK1sOJiASSeMHcdEqmuxDWW/6tK7gdRE7FOVE+dfo/74ykj9QZwv+a9cByh1cimaeHbiAw==} + + '@chat-adapter/slack@4.15.0': + resolution: {integrity: sha512-DE2puxMXitt2AVLBvMAldZ4KKUKpKWxSd62nWLDX6xXtOo6z9uXAggZD3FnMOGITqw+oQypdFopvm/Wu6OxeFg==} + + '@chat-adapter/state-ioredis@4.15.0': + resolution: {integrity: sha512-1892j1GCaxwDDMBc7gzUQsbohtf6SArlnr0PYLSIXeSguNl/V9X36WBLbE3ExgjBhyqCBfsDzicGA8e0HIco+A==} + '@coinbase/cdp-sdk@1.38.6': resolution: {integrity: sha512-l9gGGZqhCryuD3nfqB4Y+i8kfBtsnPJoKB5jxx5lKgXhVJw7/BPhgscKkVhP81115Srq3bFegD1IBwUkJ0JFMw==} @@ -1169,6 +1193,88 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@octokit/auth-app@7.2.2': + resolution: {integrity: sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==} + engines: {node: '>= 18'} + + '@octokit/auth-oauth-app@8.1.4': + resolution: {integrity: sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==} + engines: {node: '>= 18'} + + '@octokit/auth-oauth-device@7.1.5': + resolution: {integrity: sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==} + engines: {node: '>= 18'} + + '@octokit/auth-oauth-user@5.1.6': + resolution: {integrity: sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==} + engines: {node: '>= 18'} + + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.6': + resolution: {integrity: sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} + engines: {node: '>= 18'} + + '@octokit/oauth-authorization-url@7.1.1': + resolution: {integrity: sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==} + engines: {node: '>= 18'} + + '@octokit/oauth-methods@5.1.5': + resolution: {integrity: sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.4': + resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@opentelemetry/api-logs@0.203.0': resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} engines: {node: '>=8.0.0'} @@ -1543,6 +1649,18 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@slack/logger@4.0.0': + resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/types@2.20.0': + resolution: {integrity: sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/web-api@7.14.1': + resolution: {integrity: sha512-RoygyteJeFswxDPJjUMESn9dldWVMD2xUcHHd9DenVavSfVC6FeVnSdDerOO7m8LLvw4Q132nQM4hX8JiF7dng==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -2143,6 +2261,9 @@ packages: '@types/lodash@4.17.21': resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -2172,9 +2293,15 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@8.3.4': resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} @@ -2514,6 +2641,9 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + '@workflow/serde@4.1.0-beta.2': + resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + abitype@1.0.6: resolution: {integrity: sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==} peerDependencies: @@ -2762,6 +2892,9 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2774,6 +2907,9 @@ packages: react-native-b4a: optional: true + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2801,6 +2937,9 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} @@ -2920,6 +3059,9 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -2932,9 +3074,15 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chat@4.15.0: + resolution: {integrity: sha512-fE1m3UEiKQoiYkhXWT0rpH1+ZhKqJGZYkS+1gNCZWNkLv0QWTZhzdzBM5qYWe5A/Y0wJ8yLVjaqvGO4zZFxZqg==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -3170,6 +3318,9 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -3231,6 +3382,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -3366,6 +3520,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-next@15.1.7: resolution: {integrity: sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw==} peerDependencies: @@ -3536,6 +3694,9 @@ packages: eventemitter3@3.1.2: resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -3598,6 +3759,9 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4032,6 +4196,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4072,6 +4239,10 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -4320,6 +4491,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4341,6 +4515,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@15.0.12: resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} engines: {node: '>= 18'} @@ -4363,6 +4540,39 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4392,6 +4602,90 @@ packages: micro-ftch@0.3.1: resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -4736,6 +5030,10 @@ packages: typescript: optional: true + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4752,6 +5050,18 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -5104,6 +5414,15 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + request-promise-core@1.1.3: resolution: {integrity: sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==} engines: {node: '>=0.10.0'} @@ -5573,6 +5892,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -5592,6 +5915,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -5689,6 +6015,27 @@ packages: resolution: {integrity: sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==} engines: {node: '>=20.18.1'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universal-github-app-jwt@2.2.2: + resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -5850,6 +6197,12 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + viem@2.23.2: resolution: {integrity: sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==} peerDependencies: @@ -6197,6 +6550,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -6302,9 +6658,9 @@ snapshots: '@babel/runtime@7.28.4': {} - '@base-org/account@2.4.0(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 @@ -6335,11 +6691,42 @@ snapshots: '@bugsnag/cuid@3.2.1': {} - '@coinbase/cdp-sdk@1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@chat-adapter/github@4.15.0': + dependencies: + '@chat-adapter/shared': 4.15.0 + '@octokit/auth-app': 7.2.2 + '@octokit/rest': 21.1.1 + chat: 4.15.0 + transitivePeerDependencies: + - supports-color + + '@chat-adapter/shared@4.15.0': + dependencies: + chat: 4.15.0 + transitivePeerDependencies: + - supports-color + + '@chat-adapter/slack@4.15.0': dependencies: - '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@chat-adapter/shared': 4.15.0 + '@slack/web-api': 7.14.1 + chat: 4.15.0 + transitivePeerDependencies: + - debug + - supports-color + + '@chat-adapter/state-ioredis@4.15.0': + dependencies: + chat: 4.15.0 + ioredis: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@coinbase/cdp-sdk@1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.2 @@ -6392,11 +6779,11 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -6439,13 +6826,13 @@ snapshots: '@composio/client@0.1.0-alpha.52': {} - '@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': + '@composio/core@0.3.4(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: '@composio/client': 0.1.0-alpha.52 '@composio/json-schema-to-zod': 0.1.19(zod@4.1.13) '@types/json-schema': 7.0.15 chalk: 4.1.2 - openai: 5.23.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + openai: 5.23.2(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) pusher-js: 8.4.0 semver: 7.7.3 uuid: 13.0.0 @@ -6458,9 +6845,9 @@ snapshots: dependencies: zod: 4.1.13 - '@composio/vercel@0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13))': + '@composio/vercel@0.3.4(@composio/core@0.3.4(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13))': dependencies: - '@composio/core': 0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + '@composio/core': 0.3.4(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) ai: 6.0.0-beta.122(zod@4.1.13) '@crawlee/types@3.15.3': @@ -7231,6 +7618,117 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@octokit/auth-app@7.2.2': + dependencies: + '@octokit/auth-oauth-app': 8.1.4 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + toad-cache: 3.7.0 + universal-github-app-jwt: 2.2.2 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-app@8.1.4': + dependencies: + '@octokit/auth-oauth-device': 7.1.5 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-device@7.1.5': + dependencies: + '@octokit/oauth-methods': 5.1.5 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-user@5.1.6': + dependencies: + '@octokit/auth-oauth-device': 7.1.5 + '@octokit/oauth-methods': 5.1.5 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.6': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@8.2.2': + dependencies: + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/oauth-authorization-url@7.1.1': {} + + '@octokit/oauth-methods@5.1.5': + dependencies: + '@octokit/oauth-authorization-url': 7.1.1 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/openapi-types@25.1.0': {} + + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.1.0 + + '@octokit/request@9.2.4': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.3 + + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.6) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.6) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.6) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@opentelemetry/api-logs@0.203.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -7806,28 +8304,51 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@slack/logger@4.0.0': + dependencies: + '@types/node': 20.19.25 + + '@slack/types@2.20.0': {} + + '@slack/web-api@7.14.1': + dependencies: + '@slack/logger': 4.0.0 + '@slack/types': 2.20.0 + '@types/node': 20.19.25 + '@types/retry': 0.12.0 + axios: 1.13.6 + eventemitter3: 5.0.1 + form-data: 4.0.5 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + '@socket.io/component-emitter@3.1.2': {} - '@solana-program/compute-budget@0.11.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/compute-budget@0.11.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token-2022@0.6.1(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + '@solana-program/token-2022@0.6.1(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': dependencies: - '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: @@ -8064,7 +8585,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8078,11 +8599,11 @@ snapshots: '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 @@ -8090,7 +8611,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8104,11 +8625,11 @@ snapshots: '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-parsed-types': 5.0.0(typescript@5.9.3) '@solana/rpc-spec-types': 5.0.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/signers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 @@ -8258,23 +8779,23 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) '@solana/functional': 3.0.3(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@5.0.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@5.0.0(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 5.0.0(typescript@5.9.3) '@solana/functional': 5.0.0(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.3) '@solana/subscribable': 5.0.0(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@solana/rpc-subscriptions-spec@3.0.3(typescript@5.9.3)': dependencies: @@ -8292,7 +8813,7 @@ snapshots: '@solana/subscribable': 5.0.0(typescript@5.9.3) typescript: 5.9.3 - '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) @@ -8300,7 +8821,7 @@ snapshots: '@solana/promises': 3.0.3(typescript@5.9.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8310,7 +8831,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/rpc-subscriptions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 5.0.0(typescript@5.9.3) '@solana/fast-stable-stringify': 5.0.0(typescript@5.9.3) @@ -8318,7 +8839,7 @@ snapshots: '@solana/promises': 5.0.0(typescript@5.9.3) '@solana/rpc-spec-types': 5.0.0(typescript@5.9.3) '@solana/rpc-subscriptions-api': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 5.0.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-channel-websocket': 5.0.0(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.3) '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8478,7 +8999,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8486,7 +9007,7 @@ snapshots: '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/promises': 3.0.3(typescript@5.9.3) '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8495,7 +9016,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8503,7 +9024,7 @@ snapshots: '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/promises': 5.0.0(typescript@5.9.3) '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -8761,6 +9282,10 @@ snapshots: '@types/lodash@4.17.21': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/ms@2.1.0': {} '@types/node-fetch@2.6.13': @@ -8792,8 +9317,12 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/retry@0.12.0': {} + '@types/trusted-types@2.0.7': {} + '@types/unist@3.0.3': {} + '@types/uuid@8.3.4': {} '@types/ws@7.4.7': @@ -9018,9 +9547,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@wagmi/connectors@6.2.0(f24b5967e73156fd3352de3935505300)': + '@wagmi/connectors@6.2.0(f9b48ec37945e79979f3753b8b12d2d4)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.7)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -9029,7 +9558,7 @@ snapshots: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.7)(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13) optionalDependencies: typescript: 5.9.3 @@ -9634,6 +10163,8 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 + '@workflow/serde@4.1.0-beta.2': {} + abitype@1.0.6(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 @@ -9897,7 +10428,7 @@ snapshots: atomic-sleep@1.0.0: {} - autoevals@0.0.129(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + autoevals@0.0.129(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@braintrust/core': 0.0.87 ajv: 8.17.1 @@ -9906,7 +10437,7 @@ snapshots: js-yaml: 4.1.1 linear-sum-assignment: 1.0.9 mustache: 4.2.0 - openai: 4.104.0(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + openai: 4.104.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) zod: 3.25.76 zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: @@ -9936,10 +10467,20 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} b4a@1.7.3: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} bare-events@2.8.2: {} @@ -9958,6 +10499,8 @@ snapshots: dependencies: tweetnacl: 0.14.5 + before-after-hook@3.0.2: {} + big.js@6.2.2: {} bignumber.js@9.3.1: {} @@ -10123,6 +10666,8 @@ snapshots: caseless@0.12.0: {} + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -10138,8 +10683,21 @@ snapshots: chalk@5.6.2: {} + character-entities@2.0.2: {} + charenc@0.0.2: {} + chat@4.15.0: + dependencies: + '@workflow/serde': 4.1.0-beta.2 + mdast-util-to-string: 4.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + check-error@2.1.3: {} cheminfo-types@1.10.0: {} @@ -10338,6 +10896,10 @@ snapshots: decamelize@1.2.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + decode-uri-component@0.2.2: {} deep-eql@5.0.2: {} @@ -10380,6 +10942,10 @@ snapshots: detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} dijkstrajs@1.0.3: {} @@ -10657,6 +11223,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@15.1.7(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.1.7 @@ -10922,6 +11490,8 @@ snapshots: eventemitter3@3.1.2: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} events-universal@1.0.1: @@ -11044,6 +11614,8 @@ snapshots: eyes@0.1.8: {} + fast-content-type-parse@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -11529,6 +12101,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-electron@2.2.2: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -11562,6 +12136,8 @@ snapshots: is-obj@2.0.0: {} + is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -11816,6 +12392,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -11832,6 +12410,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + marked@15.0.12: {} math-intrinsics@1.1.0: {} @@ -11851,6 +12431,108 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -11867,6 +12549,197 @@ snapshots: micro-ftch@0.3.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -12130,7 +13003,7 @@ snapshots: dependencies: mimic-fn: 4.0.0 - openai@4.104.0(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): + openai@4.104.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.13 @@ -12140,14 +13013,14 @@ snapshots: formdata-node: 4.4.1 node-fetch: 2.7.0 optionalDependencies: - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 3.25.76 transitivePeerDependencies: - encoding - openai@5.23.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): + openai@5.23.2(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): optionalDependencies: - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.13 openapi-fetch@0.13.8: @@ -12273,6 +13146,8 @@ snapshots: transitivePeerDependencies: - zod + p-finally@1.0.0: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -12289,6 +13164,20 @@ snapshots: dependencies: p-limit: 3.1.0 + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -12371,7 +13260,7 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)) hono: 4.10.7 @@ -12385,7 +13274,7 @@ snapshots: '@tanstack/react-query': 5.90.11(react@19.2.1) react: 19.2.1 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@types/react' - immer @@ -12624,6 +13513,32 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + request-promise-core@1.1.3(request@2.88.2): dependencies: lodash: 4.17.21 @@ -13305,6 +14220,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} tough-cookie@2.5.0: @@ -13325,6 +14242,8 @@ snapshots: tr46@0.0.3: {} + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -13429,6 +14348,39 @@ snapshots: undici@7.19.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universal-github-app-jwt@2.2.2: {} + + universal-user-agent@7.0.3: {} + universalify@0.2.0: {} unpipe@1.0.0: {} @@ -13541,6 +14493,16 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + viem@2.23.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.8.1 @@ -13686,10 +14648,10 @@ snapshots: - tsx - yaml - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): dependencies: '@tanstack/react-query': 5.90.11(react@19.2.1) - '@wagmi/connectors': 6.2.0(f24b5967e73156fd3352de3935505300) + '@wagmi/connectors': 6.2.0(f9b48ec37945e79979f3753b8b12d2d4) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.7)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13)) react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1) @@ -13839,10 +14801,10 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402-fetch@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-fetch@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -13883,13 +14845,13 @@ snapshots: - utf-8-validate - ws - x402-next@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-next@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: - '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -13930,20 +14892,20 @@ snapshots: - utf-8-validate - ws - x402@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.11.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.6.1(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) - '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.11.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.6.1(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': 1.3.0 '@wallet-standard/app': 1.1.0 '@wallet-standard/base': 1.1.0 '@wallet-standard/features': 1.1.0 viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -14066,3 +15028,5 @@ snapshots: '@types/react': 19.2.7 react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1) + + zwitch@2.0.4: {} From a207b9370df9b76c617d9215fb8ef11e63d3d965 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Feb 2026 12:18:55 -0500 Subject: [PATCH 05/20] debug: add logging to respondToInboundEmail flow Logs at each step to identify where the email response pipeline stops: memory validation, CC check, response generation, and send. Co-Authored-By: Claude Opus 4.6 --- lib/emails/inbound/respondToInboundEmail.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/emails/inbound/respondToInboundEmail.ts b/lib/emails/inbound/respondToInboundEmail.ts index ea75fb3b..454e973e 100644 --- a/lib/emails/inbound/respondToInboundEmail.ts +++ b/lib/emails/inbound/respondToInboundEmail.ts @@ -19,6 +19,7 @@ export async function respondToInboundEmail( ): Promise { try { const original = event.data; + const emailId = original.email_id; const subject = original.subject ? `Re: ${original.subject}` : "Re: Your email"; const messageId = original.message_id; const to = original.from; @@ -26,23 +27,30 @@ export async function respondToInboundEmail( const from = getFromWithName(original.to, original.cc); const cc = original.cc?.length ? original.cc : undefined; + console.log(`[respondToInboundEmail] Processing email ${emailId} from ${to}`); + // Validate new memory and get chat request body (or early return if duplicate) const validationResult = await validateNewEmailMemory(event); if ("response" in validationResult) { + console.log(`[respondToInboundEmail] Email ${emailId} - early return from validateNewEmailMemory`); return validationResult.response; } const { chatRequestBody, emailText } = validationResult; + console.log(`[respondToInboundEmail] Email ${emailId} - memory validated, roomId=${chatRequestBody.roomId}, emailText length=${emailText.length}`); // Check if Recoup is only CC'd - use LLM to determine if reply is expected const ccValidation = await validateCcReplyExpected(original, emailText); if (ccValidation) { + console.log(`[respondToInboundEmail] Email ${emailId} - CC validation returned early (not a direct reply)`); return ccValidation.response; } const { roomId } = chatRequestBody; + console.log(`[respondToInboundEmail] Email ${emailId} - generating response...`); const { text, html } = await generateEmailResponse(chatRequestBody); + console.log(`[respondToInboundEmail] Email ${emailId} - response generated, text length=${text.length}`); const payload = { from, @@ -55,15 +63,18 @@ export async function respondToInboundEmail( }, }; + console.log(`[respondToInboundEmail] Email ${emailId} - sending reply to ${to} from ${from}`); const result = await sendEmailWithResend(payload); // Save the assistant response message await saveChatCompletion({ text, roomId }); if (result instanceof NextResponse) { + console.log(`[respondToInboundEmail] Email ${emailId} - sendEmailWithResend returned error response`); return result; } + console.log(`[respondToInboundEmail] Email ${emailId} - reply sent successfully`); return NextResponse.json(result); } catch (error) { console.error("[respondToInboundEmail] Failed to respond to inbound email", error); From c810896186485056dab37be1ae83607493425dad Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 13:52:07 -0500 Subject: [PATCH 06/20] fix: connect Redis before passing to Chat SDK state adapter ioredis is configured with lazyConnect: true, so the state adapter's connect() hangs forever waiting for a "ready" event that never fires. Explicitly call redis.connect() when status is "wait" to unblock Chat SDK initialization and Slack webhook handling. Co-Authored-By: Claude Opus 4.6 --- lib/coding-agent/bot.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/coding-agent/bot.ts b/lib/coding-agent/bot.ts index 920df1e6..b86d57ab 100644 --- a/lib/coding-agent/bot.ts +++ b/lib/coding-agent/bot.ts @@ -11,6 +11,12 @@ const logger = new ConsoleLogger(); * Creates a new Chat bot instance configured with Slack and GitHub adapters. */ export function createCodingAgentBot() { + // ioredis is configured with lazyConnect: true, so we must + // explicitly connect before the state adapter listens for "ready". + if (redis.status === "wait") { + redis.connect().catch(err => console.error("[coding-agent] Redis connect error:", err)); + } + const state = createIoRedisState({ client: redis, keyPrefix: "coding-agent", From 488afd3d66f7b64bccd6cd4ca2f5ea0a747bcde3 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 13:59:39 -0500 Subject: [PATCH 07/20] fix: handle Slack url_verification before bot initialization The Slack challenge request was timing out because bot initialization blocks on Redis connection. Now the route handles url_verification challenges immediately before loading the bot. Also adds debug logging and lazy imports to isolate initialization errors. Co-Authored-By: Claude Opus 4.6 --- app/api/coding-agent/[platform]/route.ts | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/app/api/coding-agent/[platform]/route.ts b/app/api/coding-agent/[platform]/route.ts index b6e55d0e..6abb5145 100644 --- a/app/api/coding-agent/[platform]/route.ts +++ b/app/api/coding-agent/[platform]/route.ts @@ -1,7 +1,5 @@ import type { NextRequest } from "next/server"; import { after } from "next/server"; -import { codingAgentBot } from "@/lib/coding-agent/bot"; -import "@/lib/coding-agent/handlers/registerHandlers"; /** * POST /api/coding-agent/[platform] @@ -18,11 +16,34 @@ export async function POST( { params }: { params: Promise<{ platform: string }> }, ) { const { platform } = await params; - const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks]; + console.log(`[coding-agent] POST /api/coding-agent/${platform}`); - if (!handler) { - return new Response("Unknown platform", { status: 404 }); + // Handle Slack url_verification challenge before loading the bot. + // This avoids blocking on Redis/adapter initialization during setup. + if (platform === "slack") { + const body = await request.clone().json().catch(() => null); + if (body?.type === "url_verification" && body?.challenge) { + console.log("[coding-agent] Responding to Slack url_verification challenge"); + return Response.json({ challenge: body.challenge }); + } } - return handler(request, { waitUntil: p => after(() => p) }); + try { + // Lazy-import bot to isolate initialization errors + const { codingAgentBot } = await import("@/lib/coding-agent/bot"); + await import("@/lib/coding-agent/handlers/registerHandlers"); + + const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks]; + + if (!handler) { + console.log(`[coding-agent] Unknown platform: ${platform}`); + return new Response("Unknown platform", { status: 404 }); + } + + console.log(`[coding-agent] Delegating to ${platform} webhook handler`); + return handler(request, { waitUntil: p => after(() => p) }); + } catch (error) { + console.error("[coding-agent] Failed to initialize bot:", error); + return new Response("Internal server error", { status: 500 }); + } } From 5ad50e25809d37fcb666362f9d545b8e37670a30 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 14:00:21 -0500 Subject: [PATCH 08/20] test: add route tests for coding-agent webhook endpoint - Verify Slack url_verification challenge responds immediately - Verify 404 for unknown platforms - Verify non-challenge requests delegate to bot webhook handler Co-Authored-By: Claude Opus 4.6 --- app/api/coding-agent/__tests__/route.test.ts | 86 ++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 app/api/coding-agent/__tests__/route.test.ts diff --git a/app/api/coding-agent/__tests__/route.test.ts b/app/api/coding-agent/__tests__/route.test.ts new file mode 100644 index 00000000..9c5188d3 --- /dev/null +++ b/app/api/coding-agent/__tests__/route.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest } from "next/server"; + +vi.mock("next/server", async () => { + const actual = await vi.importActual("next/server"); + return { + ...actual, + after: vi.fn((fn: () => void) => fn()), + }; +}); + +vi.mock("@/lib/coding-agent/bot", () => ({ + codingAgentBot: { + webhooks: { + slack: vi.fn().mockResolvedValue(new Response("ok", { status: 200 })), + github: vi.fn().mockResolvedValue(new Response("ok", { status: 200 })), + }, + }, +})); + +vi.mock("@/lib/coding-agent/handlers/registerHandlers", () => ({})); + +const { POST } = await import("../[platform]/route"); + +describe("POST /api/coding-agent/[platform]", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("responds to Slack url_verification challenge without loading bot", async () => { + const body = JSON.stringify({ + type: "url_verification", + challenge: "test_challenge_value", + }); + + const request = new NextRequest("https://example.com/api/coding-agent/slack", { + method: "POST", + body, + headers: { "content-type": "application/json" }, + }); + + const response = await POST(request, { + params: Promise.resolve({ platform: "slack" }), + }); + + expect(response.status).toBe(200); + const json = await response.json(); + expect(json.challenge).toBe("test_challenge_value"); + }); + + it("returns 404 for unknown platforms", async () => { + const request = new NextRequest("https://example.com/api/coding-agent/unknown", { + method: "POST", + body: JSON.stringify({}), + headers: { "content-type": "application/json" }, + }); + + const response = await POST(request, { + params: Promise.resolve({ platform: "unknown" }), + }); + + expect(response.status).toBe(404); + }); + + it("delegates non-challenge Slack requests to bot webhook handler", async () => { + const { codingAgentBot } = await import("@/lib/coding-agent/bot"); + + const body = JSON.stringify({ + type: "event_callback", + event: { type: "app_mention", text: "hello" }, + }); + + const request = new NextRequest("https://example.com/api/coding-agent/slack", { + method: "POST", + body, + headers: { "content-type": "application/json" }, + }); + + const response = await POST(request, { + params: Promise.resolve({ platform: "slack" }), + }); + + expect(response.status).toBe(200); + expect(codingAgentBot.webhooks.slack).toHaveBeenCalled(); + }); +}); From 9e795ebc409feb34d0c2bc894bf02cfb9300afcf Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 14:10:49 -0500 Subject: [PATCH 09/20] chore: redeploy with updated env vars From fc7c5afa27ce99a8358c30cf1ab57f9bf6141861 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 14:15:02 -0500 Subject: [PATCH 10/20] debug: add logging to onNewMention handler Trace handler execution to diagnose why bot isn't replying: - Log when handler fires with thread/author info - Log channel/user allowlist rejections - Log each step: subscribe, post, trigger task - Catch and log errors Co-Authored-By: Claude Opus 4.6 --- lib/coding-agent/handlers/onNewMention.ts | 40 ++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/coding-agent/handlers/onNewMention.ts b/lib/coding-agent/handlers/onNewMention.ts index df2ef07e..d890da10 100644 --- a/lib/coding-agent/handlers/onNewMention.ts +++ b/lib/coding-agent/handlers/onNewMention.ts @@ -12,12 +12,19 @@ import type { CodingAgentThreadState } from "../types"; */ export function registerOnNewMention(bot: CodingAgentBot) { bot.onNewMention(async (thread, message) => { + console.log("[coding-agent] onNewMention fired", { + threadId: thread.id, + text: message.text.slice(0, 50), + author: message.author.userId, + }); + const allowedChannels = getAllowedChannelIds(); const allowedUsers = getAllowedUserIds(); if (allowedChannels.length > 0) { const channelId = thread.id.split(":")[1]; if (!allowedChannels.includes(channelId)) { + console.log("[coding-agent] Channel not allowed", { channelId, allowedChannels }); return; } } @@ -25,25 +32,34 @@ export function registerOnNewMention(bot: CodingAgentBot) { if (allowedUsers.length > 0) { const userId = message.author.userId; if (!allowedUsers.includes(userId)) { + console.log("[coding-agent] User not allowed", { userId, allowedUsers }); return; } } const prompt = message.text; - await thread.subscribe(); - await thread.post(`Starting work on: "${prompt}"\n\nI'll reply here when done.`); + try { + await thread.subscribe(); + console.log("[coding-agent] Subscribed to thread"); - const handle = await triggerCodingAgent({ - prompt, - callbackThreadId: thread.id, - }); + await thread.post(`Starting work on: "${prompt}"\n\nI'll reply here when done.`); + console.log("[coding-agent] Posted acknowledgment"); - await thread.setState({ - status: "running", - prompt, - runId: handle.id, - slackThreadId: thread.id, - } as Partial); + const handle = await triggerCodingAgent({ + prompt, + callbackThreadId: thread.id, + }); + console.log("[coding-agent] Triggered coding agent task", { runId: handle.id }); + + await thread.setState({ + status: "running", + prompt, + runId: handle.id, + slackThreadId: thread.id, + } as Partial); + } catch (error) { + console.error("[coding-agent] onNewMention error:", error); + } }); } From c94de5701ca7cfdf1b9fbc0c1267974e89883d50 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 14:19:45 -0500 Subject: [PATCH 11/20] feat: validate required env vars for coding agent bot Throws a clear error listing all missing env vars when the bot initializes. Required vars: - SLACK_BOT_TOKEN - SLACK_SIGNING_SECRET - GITHUB_TOKEN - GITHUB_WEBHOOK_SECRET - GITHUB_BOT_USERNAME - REDIS_URL - CODING_AGENT_CALLBACK_SECRET Co-Authored-By: Claude Opus 4.6 --- .../__tests__/validateEnv.test.ts | 48 +++++++++++++++++++ lib/coding-agent/bot.ts | 2 + lib/coding-agent/validateEnv.ts | 23 +++++++++ 3 files changed, 73 insertions(+) create mode 100644 lib/coding-agent/__tests__/validateEnv.test.ts create mode 100644 lib/coding-agent/validateEnv.ts diff --git a/lib/coding-agent/__tests__/validateEnv.test.ts b/lib/coding-agent/__tests__/validateEnv.test.ts new file mode 100644 index 00000000..9b21a828 --- /dev/null +++ b/lib/coding-agent/__tests__/validateEnv.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +const REQUIRED_VARS = [ + "SLACK_BOT_TOKEN", + "SLACK_SIGNING_SECRET", + "GITHUB_TOKEN", + "GITHUB_WEBHOOK_SECRET", + "GITHUB_BOT_USERNAME", + "REDIS_URL", + "CODING_AGENT_CALLBACK_SECRET", +]; + +describe("validateCodingAgentEnv", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + vi.resetModules(); + for (const key of REQUIRED_VARS) { + process.env[key] = "test-value"; + } + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it("does not throw when all required vars are set", async () => { + const { validateCodingAgentEnv } = await import("../validateEnv"); + expect(() => validateCodingAgentEnv()).not.toThrow(); + }); + + it("throws listing all missing vars when none are set", async () => { + for (const key of REQUIRED_VARS) { + delete process.env[key]; + } + + const { validateCodingAgentEnv } = await import("../validateEnv"); + expect(() => validateCodingAgentEnv()).toThrow("SLACK_BOT_TOKEN"); + expect(() => validateCodingAgentEnv()).toThrow("REDIS_URL"); + }); + + it("throws listing only the missing var", async () => { + delete process.env.SLACK_BOT_TOKEN; + + const { validateCodingAgentEnv } = await import("../validateEnv"); + expect(() => validateCodingAgentEnv()).toThrow("SLACK_BOT_TOKEN"); + }); +}); diff --git a/lib/coding-agent/bot.ts b/lib/coding-agent/bot.ts index b86d57ab..de826b37 100644 --- a/lib/coding-agent/bot.ts +++ b/lib/coding-agent/bot.ts @@ -4,6 +4,7 @@ import { GitHubAdapter } from "@chat-adapter/github"; import { createIoRedisState } from "@chat-adapter/state-ioredis"; import redis from "@/lib/redis/connection"; import type { CodingAgentThreadState } from "./types"; +import { validateCodingAgentEnv } from "./validateEnv"; const logger = new ConsoleLogger(); @@ -11,6 +12,7 @@ const logger = new ConsoleLogger(); * Creates a new Chat bot instance configured with Slack and GitHub adapters. */ export function createCodingAgentBot() { + validateCodingAgentEnv(); // ioredis is configured with lazyConnect: true, so we must // explicitly connect before the state adapter listens for "ready". if (redis.status === "wait") { diff --git a/lib/coding-agent/validateEnv.ts b/lib/coding-agent/validateEnv.ts new file mode 100644 index 00000000..d5c67662 --- /dev/null +++ b/lib/coding-agent/validateEnv.ts @@ -0,0 +1,23 @@ +const REQUIRED_ENV_VARS = [ + "SLACK_BOT_TOKEN", + "SLACK_SIGNING_SECRET", + "GITHUB_TOKEN", + "GITHUB_WEBHOOK_SECRET", + "GITHUB_BOT_USERNAME", + "REDIS_URL", + "CODING_AGENT_CALLBACK_SECRET", +] as const; + +/** + * Validates that all required environment variables for the coding agent are set. + * Throws an error listing all missing variables. + */ +export function validateCodingAgentEnv(): void { + const missing = REQUIRED_ENV_VARS.filter(name => !process.env[name]); + + if (missing.length > 0) { + throw new Error( + `[coding-agent] Missing required environment variables:\n${missing.map(v => ` - ${v}`).join("\n")}`, + ); + } +} From f599676bb8f6e8ff25b7d959d9d6a010d19150e5 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 14:29:09 -0500 Subject: [PATCH 12/20] chore: redeploy with all required env vars From 586d8845057c9972e101349a4781aa8d954898a8 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 14:37:57 -0500 Subject: [PATCH 13/20] refactor: remove GitHub adapter code, scope PR to Slack-only Split GitHub adapter (GitHubAdapter, onSubscribedMessage, onMergeAction, triggerUpdatePR, SUBMODULE_CONFIG) into separate PR (MYC-4431). Co-Authored-By: Claude Opus 4.6 --- app/api/coding-agent/[platform]/route.ts | 2 +- lib/coding-agent/__tests__/bot.test.ts | 30 +------- lib/coding-agent/__tests__/config.test.ts | 29 ------- lib/coding-agent/__tests__/handlers.test.ts | 75 ------------------- .../__tests__/validateEnv.test.ts | 3 - lib/coding-agent/bot.ts | 14 +--- lib/coding-agent/config.ts | 16 ---- lib/coding-agent/handlers/onMergeAction.ts | 54 ------------- lib/coding-agent/handlers/onNewMention.ts | 3 +- .../handlers/onSubscribedMessage.ts | 37 --------- lib/coding-agent/handlers/registerHandlers.ts | 4 - lib/coding-agent/types.ts | 13 +--- lib/coding-agent/validateEnv.ts | 3 - lib/trigger/triggerUpdatePR.ts | 21 ------ 14 files changed, 10 insertions(+), 294 deletions(-) delete mode 100644 lib/coding-agent/handlers/onMergeAction.ts delete mode 100644 lib/coding-agent/handlers/onSubscribedMessage.ts delete mode 100644 lib/trigger/triggerUpdatePR.ts diff --git a/app/api/coding-agent/[platform]/route.ts b/app/api/coding-agent/[platform]/route.ts index 6abb5145..cc8a3c4e 100644 --- a/app/api/coding-agent/[platform]/route.ts +++ b/app/api/coding-agent/[platform]/route.ts @@ -5,7 +5,7 @@ import { after } from "next/server"; * POST /api/coding-agent/[platform] * * Webhook endpoint for the coding agent bot. - * Handles both Slack and GitHub webhooks via dynamic [platform] segment. + * Handles Slack webhooks via dynamic [platform] segment. * * @param request - The incoming webhook request * @param params.params diff --git a/lib/coding-agent/__tests__/bot.test.ts b/lib/coding-agent/__tests__/bot.test.ts index 29695539..3d320f1b 100644 --- a/lib/coding-agent/__tests__/bot.test.ts +++ b/lib/coding-agent/__tests__/bot.test.ts @@ -6,12 +6,6 @@ vi.mock("@chat-adapter/slack", () => ({ })), })); -vi.mock("@chat-adapter/github", () => ({ - GitHubAdapter: vi.fn().mockImplementation(() => ({ - name: "github", - })), -})); - vi.mock("@chat-adapter/state-ioredis", () => ({ createIoRedisState: vi.fn().mockReturnValue({ connect: vi.fn(), @@ -36,6 +30,7 @@ vi.mock("chat", () => ({ instance.registerSingleton = vi.fn().mockReturnValue(instance); return instance; }), + ConsoleLogger: vi.fn(), })); describe("createCodingAgentBot", () => { @@ -43,12 +38,11 @@ describe("createCodingAgentBot", () => { vi.clearAllMocks(); process.env.SLACK_BOT_TOKEN = "xoxb-test"; process.env.SLACK_SIGNING_SECRET = "test-signing-secret"; - process.env.GITHUB_TOKEN = "ghp_test"; - process.env.GITHUB_WEBHOOK_SECRET = "test-webhook-secret"; - process.env.GITHUB_BOT_USERNAME = "recoup-bot"; + process.env.REDIS_URL = "redis://localhost:6379"; + process.env.CODING_AGENT_CALLBACK_SECRET = "test-callback-secret"; }); - it("creates a Chat instance with slack and github adapters", async () => { + it("creates a Chat instance with slack adapter", async () => { const { Chat } = await import("chat"); const { createCodingAgentBot } = await import("../bot"); @@ -58,7 +52,6 @@ describe("createCodingAgentBot", () => { const lastCall = vi.mocked(Chat).mock.calls.at(-1)!; const config = lastCall[0]; expect(config.adapters).toHaveProperty("slack"); - expect(config.adapters).toHaveProperty("github"); }); it("creates a SlackAdapter with correct config", async () => { @@ -75,21 +68,6 @@ describe("createCodingAgentBot", () => { ); }); - it("creates a GitHubAdapter with correct config", async () => { - const { GitHubAdapter } = await import("@chat-adapter/github"); - const { createCodingAgentBot } = await import("../bot"); - - createCodingAgentBot(); - - expect(GitHubAdapter).toHaveBeenCalledWith( - expect.objectContaining({ - token: "ghp_test", - webhookSecret: "test-webhook-secret", - userName: "recoup-bot", - }), - ); - }); - it("uses ioredis state adapter with existing Redis client", async () => { const { createIoRedisState } = await import("@chat-adapter/state-ioredis"); const redis = (await import("@/lib/redis/connection")).default; diff --git a/lib/coding-agent/__tests__/config.test.ts b/lib/coding-agent/__tests__/config.test.ts index 14dc2ed3..fb0a2180 100644 --- a/lib/coding-agent/__tests__/config.test.ts +++ b/lib/coding-agent/__tests__/config.test.ts @@ -12,35 +12,6 @@ describe("config", () => { process.env = originalEnv; }); - describe("SUBMODULE_CONFIG", () => { - it("maps api to test base branch", async () => { - const { SUBMODULE_CONFIG } = await import("../config"); - expect(SUBMODULE_CONFIG.api.baseBranch).toBe("test"); - }); - - it("maps chat to test base branch", async () => { - const { SUBMODULE_CONFIG } = await import("../config"); - expect(SUBMODULE_CONFIG.chat.baseBranch).toBe("test"); - }); - - it("maps tasks to main base branch", async () => { - const { SUBMODULE_CONFIG } = await import("../config"); - expect(SUBMODULE_CONFIG.tasks.baseBranch).toBe("main"); - }); - - it("maps docs to main base branch", async () => { - const { SUBMODULE_CONFIG } = await import("../config"); - expect(SUBMODULE_CONFIG.docs.baseBranch).toBe("main"); - }); - - it("includes repo URL for each submodule", async () => { - const { SUBMODULE_CONFIG } = await import("../config"); - expect(SUBMODULE_CONFIG.api.repo).toBe("recoupable/recoup-api"); - expect(SUBMODULE_CONFIG.chat.repo).toBe("recoupable/chat"); - expect(SUBMODULE_CONFIG.tasks.repo).toBe("recoupable/tasks"); - }); - }); - describe("getAllowedChannelIds", () => { it("parses comma-separated channel IDs from env", async () => { process.env.CODING_AGENT_CHANNELS = "C123,C456,C789"; diff --git a/lib/coding-agent/__tests__/handlers.test.ts b/lib/coding-agent/__tests__/handlers.test.ts index 18eb104f..64564af3 100644 --- a/lib/coding-agent/__tests__/handlers.test.ts +++ b/lib/coding-agent/__tests__/handlers.test.ts @@ -4,13 +4,7 @@ vi.mock("@/lib/trigger/triggerCodingAgent", () => ({ triggerCodingAgent: vi.fn().mockResolvedValue({ id: "run_123" }), })); -vi.mock("@/lib/trigger/triggerUpdatePR", () => ({ - triggerUpdatePR: vi.fn().mockResolvedValue({ id: "run_456" }), -})); - const { registerOnNewMention } = await import("../handlers/onNewMention"); -const { registerOnSubscribedMessage } = await import("../handlers/onSubscribedMessage"); -const { registerOnMergeAction } = await import("../handlers/onMergeAction"); beforeEach(() => { vi.clearAllMocks(); @@ -22,8 +16,6 @@ beforeEach(() => { function createMockBot() { return { onNewMention: vi.fn(), - onSubscribedMessage: vi.fn(), - onAction: vi.fn(), } as any; } @@ -85,70 +77,3 @@ describe("registerOnNewMention", () => { expect(mockThread.subscribe).not.toHaveBeenCalled(); }); }); - -describe("registerOnSubscribedMessage", () => { - it("registers a handler on the bot", () => { - const bot = createMockBot(); - registerOnSubscribedMessage(bot); - expect(bot.onSubscribedMessage).toHaveBeenCalledOnce(); - }); - - it("triggers update PR task when status is pr_created", async () => { - const { triggerUpdatePR } = await import("@/lib/trigger/triggerUpdatePR"); - - const bot = createMockBot(); - registerOnSubscribedMessage(bot); - const handler = bot.onSubscribedMessage.mock.calls[0][0]; - - const mockThread = { - id: "slack:C123:1234567890.123456", - state: Promise.resolve({ - status: "pr_created", - prompt: "fix bug", - snapshotId: "snap_abc", - branch: "agent/fix-bug", - prs: [{ repo: "recoupable/api", number: 1, url: "url", baseBranch: "test" }], - }), - post: vi.fn(), - setState: vi.fn(), - }; - - await handler(mockThread, { text: "make the button blue", author: { id: "U111" } }); - - expect(triggerUpdatePR).toHaveBeenCalledWith( - expect.objectContaining({ - feedback: "make the button blue", - snapshotId: "snap_abc", - repo: "recoupable/api", - }), - ); - expect(mockThread.setState).toHaveBeenCalledWith( - expect.objectContaining({ status: "updating" }), - ); - }); - - it("tells user to wait when agent is running", async () => { - const bot = createMockBot(); - registerOnSubscribedMessage(bot); - const handler = bot.onSubscribedMessage.mock.calls[0][0]; - - const mockThread = { - id: "slack:C123:1234567890.123456", - state: Promise.resolve({ status: "running", prompt: "fix bug" }), - post: vi.fn(), - setState: vi.fn(), - }; - - await handler(mockThread, { text: "hurry up", author: { id: "U111" } }); - - expect(mockThread.post).toHaveBeenCalledWith(expect.stringContaining("still working")); - }); -}); - -describe("registerOnMergeAction", () => { - it("registers merge_all_prs action handler", () => { - const bot = createMockBot(); - registerOnMergeAction(bot); - expect(bot.onAction).toHaveBeenCalledWith("merge_all_prs", expect.any(Function)); - }); -}); diff --git a/lib/coding-agent/__tests__/validateEnv.test.ts b/lib/coding-agent/__tests__/validateEnv.test.ts index 9b21a828..27391354 100644 --- a/lib/coding-agent/__tests__/validateEnv.test.ts +++ b/lib/coding-agent/__tests__/validateEnv.test.ts @@ -3,9 +3,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; const REQUIRED_VARS = [ "SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET", - "GITHUB_TOKEN", - "GITHUB_WEBHOOK_SECRET", - "GITHUB_BOT_USERNAME", "REDIS_URL", "CODING_AGENT_CALLBACK_SECRET", ]; diff --git a/lib/coding-agent/bot.ts b/lib/coding-agent/bot.ts index de826b37..0f4d7f13 100644 --- a/lib/coding-agent/bot.ts +++ b/lib/coding-agent/bot.ts @@ -1,6 +1,5 @@ import { Chat, ConsoleLogger } from "chat"; import { SlackAdapter } from "@chat-adapter/slack"; -import { GitHubAdapter } from "@chat-adapter/github"; import { createIoRedisState } from "@chat-adapter/state-ioredis"; import redis from "@/lib/redis/connection"; import type { CodingAgentThreadState } from "./types"; @@ -9,7 +8,7 @@ import { validateCodingAgentEnv } from "./validateEnv"; const logger = new ConsoleLogger(); /** - * Creates a new Chat bot instance configured with Slack and GitHub adapters. + * Creates a new Chat bot instance configured with the Slack adapter. */ export function createCodingAgentBot() { validateCodingAgentEnv(); @@ -31,16 +30,9 @@ export function createCodingAgentBot() { logger, }); - const github = new GitHubAdapter({ - token: process.env.GITHUB_TOKEN!, - webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!, - userName: process.env.GITHUB_BOT_USERNAME!, - logger, - }); - - return new Chat<{ slack: SlackAdapter; github: GitHubAdapter }, CodingAgentThreadState>({ + return new Chat<{ slack: SlackAdapter }, CodingAgentThreadState>({ userName: "Recoup Agent", - adapters: { slack, github }, + adapters: { slack }, state, }); } diff --git a/lib/coding-agent/config.ts b/lib/coding-agent/config.ts index 7ca48f41..c9f23324 100644 --- a/lib/coding-agent/config.ts +++ b/lib/coding-agent/config.ts @@ -1,19 +1,3 @@ -/** - * Submodule configuration mapping for PR creation. - * Defines the GitHub repo and base branch for each submodule. - */ -export const SUBMODULE_CONFIG: Record = { - api: { repo: "recoupable/recoup-api", baseBranch: "test" }, - chat: { repo: "recoupable/chat", baseBranch: "test" }, - tasks: { repo: "recoupable/tasks", baseBranch: "main" }, - docs: { repo: "recoupable/docs", baseBranch: "main" }, - database: { repo: "recoupable/database", baseBranch: "main" }, - remotion: { repo: "recoupable/remotion", baseBranch: "main" }, - bash: { repo: "recoupable/bash", baseBranch: "main" }, - skills: { repo: "recoupable/skills", baseBranch: "main" }, - cli: { repo: "recoupable/cli", baseBranch: "main" }, -}; - /** * Returns the list of allowed Slack channel IDs from the environment. */ diff --git a/lib/coding-agent/handlers/onMergeAction.ts b/lib/coding-agent/handlers/onMergeAction.ts deleted file mode 100644 index 5e883db4..00000000 --- a/lib/coding-agent/handlers/onMergeAction.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { CodingAgentBot } from "../bot"; -import type { CodingAgentThreadState } from "../types"; - -/** - * Registers the "Merge All PRs" button action handler on the bot. - * Squash-merges each PR via the GitHub API. - * - * @param bot - */ -export function registerOnMergeAction(bot: CodingAgentBot) { - bot.onAction("merge_all_prs", async event => { - const thread = event.thread; - const state = (await thread.state) as CodingAgentThreadState | null; - - if (!state?.prs?.length) { - await thread.post("No PRs to merge."); - return; - } - - const token = process.env.GITHUB_TOKEN; - if (!token) { - await thread.post("Missing GITHUB_TOKEN — cannot merge PRs."); - return; - } - - const results: string[] = []; - - for (const pr of state.prs) { - const [owner, repo] = pr.repo.split("/"); - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}/pulls/${pr.number}/merge`, - { - method: "PUT", - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - body: JSON.stringify({ merge_method: "squash" }), - }, - ); - - if (response.ok) { - results.push(`${pr.repo}#${pr.number} merged`); - } else { - const error = await response.json(); - results.push(`${pr.repo}#${pr.number} failed: ${error.message}`); - } - } - - await thread.setState({ status: "merged" }); - await thread.post(`Merge results:\n${results.map(r => `- ${r}`).join("\n")}`); - }); -} diff --git a/lib/coding-agent/handlers/onNewMention.ts b/lib/coding-agent/handlers/onNewMention.ts index d890da10..4ca7b221 100644 --- a/lib/coding-agent/handlers/onNewMention.ts +++ b/lib/coding-agent/handlers/onNewMention.ts @@ -1,7 +1,6 @@ import type { CodingAgentBot } from "../bot"; import { getAllowedChannelIds, getAllowedUserIds } from "../config"; import { triggerCodingAgent } from "@/lib/trigger/triggerCodingAgent"; -import type { CodingAgentThreadState } from "../types"; /** * Registers the onNewMention handler on the bot. @@ -57,7 +56,7 @@ export function registerOnNewMention(bot: CodingAgentBot) { prompt, runId: handle.id, slackThreadId: thread.id, - } as Partial); + }); } catch (error) { console.error("[coding-agent] onNewMention error:", error); } diff --git a/lib/coding-agent/handlers/onSubscribedMessage.ts b/lib/coding-agent/handlers/onSubscribedMessage.ts deleted file mode 100644 index 1846c541..00000000 --- a/lib/coding-agent/handlers/onSubscribedMessage.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CodingAgentBot } from "../bot"; -import { triggerUpdatePR } from "@/lib/trigger/triggerUpdatePR"; - -/** - * Registers the onSubscribedMessage handler on the bot. - * If the agent has created PRs, treats the message as feedback and - * triggers the update-pr task. If the agent is currently working, - * tells the user to wait. - * - * @param bot - */ -export function registerOnSubscribedMessage(bot: CodingAgentBot) { - bot.onSubscribedMessage(async (thread, message) => { - const state = await thread.state; - - if (!state) return; - - if (state.status === "running" || state.status === "updating") { - await thread.post("I'm still working on this. I'll let you know when I'm done."); - return; - } - - if (state.status === "pr_created" && state.snapshotId && state.branch && state.prs?.length) { - await thread.post("Got your feedback. Updating the PRs..."); - - await thread.setState({ status: "updating" }); - - await triggerUpdatePR({ - feedback: message.text, - snapshotId: state.snapshotId, - branch: state.branch, - repo: state.prs[0].repo, - callbackThreadId: thread.id, - }); - } - }); -} diff --git a/lib/coding-agent/handlers/registerHandlers.ts b/lib/coding-agent/handlers/registerHandlers.ts index 96f24748..c6a2f6da 100644 --- a/lib/coding-agent/handlers/registerHandlers.ts +++ b/lib/coding-agent/handlers/registerHandlers.ts @@ -1,12 +1,8 @@ import { codingAgentBot } from "../bot"; import { registerOnNewMention } from "./onNewMention"; -import { registerOnSubscribedMessage } from "./onSubscribedMessage"; -import { registerOnMergeAction } from "./onMergeAction"; /** * Registers all coding agent event handlers on the bot singleton. * Import this file once to attach handlers to the bot. */ registerOnNewMention(codingAgentBot); -registerOnSubscribedMessage(codingAgentBot); -registerOnMergeAction(codingAgentBot); diff --git a/lib/coding-agent/types.ts b/lib/coding-agent/types.ts index 716a0637..081a78e6 100644 --- a/lib/coding-agent/types.ts +++ b/lib/coding-agent/types.ts @@ -3,19 +3,8 @@ * Stored in Redis via Chat SDK's state adapter. */ export interface CodingAgentThreadState { - status: "running" | "pr_created" | "updating" | "merged" | "failed"; + status: "running" | "failed"; prompt: string; runId?: string; - sandboxId?: string; - snapshotId?: string; - branch?: string; - prs?: CodingAgentPR[]; slackThreadId?: string; } - -export interface CodingAgentPR { - repo: string; - number: number; - url: string; - baseBranch: string; -} diff --git a/lib/coding-agent/validateEnv.ts b/lib/coding-agent/validateEnv.ts index d5c67662..9e9a5af6 100644 --- a/lib/coding-agent/validateEnv.ts +++ b/lib/coding-agent/validateEnv.ts @@ -1,9 +1,6 @@ const REQUIRED_ENV_VARS = [ "SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET", - "GITHUB_TOKEN", - "GITHUB_WEBHOOK_SECRET", - "GITHUB_BOT_USERNAME", "REDIS_URL", "CODING_AGENT_CALLBACK_SECRET", ] as const; diff --git a/lib/trigger/triggerUpdatePR.ts b/lib/trigger/triggerUpdatePR.ts deleted file mode 100644 index f989b576..00000000 --- a/lib/trigger/triggerUpdatePR.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { tasks } from "@trigger.dev/sdk"; - -type UpdatePRPayload = { - feedback: string; - snapshotId: string; - branch: string; - repo: string; - callbackThreadId: string; -}; - -/** - * Triggers the update-pr task to resume a sandbox from snapshot, - * apply feedback via the AI agent, and push updates to existing PRs. - * - * @param payload - The task payload with feedback, snapshot, branch, and PR info - * @returns The task handle with runId - */ -export async function triggerUpdatePR(payload: UpdatePRPayload) { - const handle = await tasks.trigger("update-pr", payload); - return handle; -} From e929fb58dde71a5ae3db88e8c96bb041e78a43c6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 15:21:55 -0500 Subject: [PATCH 14/20] chore: remove debug logging from coding agent and email handlers Co-Authored-By: Claude Opus 4.6 --- app/api/coding-agent/[platform]/route.ts | 4 ---- lib/coding-agent/handlers/onNewMention.ts | 11 ----------- lib/emails/inbound/respondToInboundEmail.ts | 11 ----------- 3 files changed, 26 deletions(-) diff --git a/app/api/coding-agent/[platform]/route.ts b/app/api/coding-agent/[platform]/route.ts index cc8a3c4e..f6d2a718 100644 --- a/app/api/coding-agent/[platform]/route.ts +++ b/app/api/coding-agent/[platform]/route.ts @@ -16,14 +16,12 @@ export async function POST( { params }: { params: Promise<{ platform: string }> }, ) { const { platform } = await params; - console.log(`[coding-agent] POST /api/coding-agent/${platform}`); // Handle Slack url_verification challenge before loading the bot. // This avoids blocking on Redis/adapter initialization during setup. if (platform === "slack") { const body = await request.clone().json().catch(() => null); if (body?.type === "url_verification" && body?.challenge) { - console.log("[coding-agent] Responding to Slack url_verification challenge"); return Response.json({ challenge: body.challenge }); } } @@ -36,11 +34,9 @@ export async function POST( const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks]; if (!handler) { - console.log(`[coding-agent] Unknown platform: ${platform}`); return new Response("Unknown platform", { status: 404 }); } - console.log(`[coding-agent] Delegating to ${platform} webhook handler`); return handler(request, { waitUntil: p => after(() => p) }); } catch (error) { console.error("[coding-agent] Failed to initialize bot:", error); diff --git a/lib/coding-agent/handlers/onNewMention.ts b/lib/coding-agent/handlers/onNewMention.ts index 4ca7b221..fd1078ed 100644 --- a/lib/coding-agent/handlers/onNewMention.ts +++ b/lib/coding-agent/handlers/onNewMention.ts @@ -11,19 +11,12 @@ import { triggerCodingAgent } from "@/lib/trigger/triggerCodingAgent"; */ export function registerOnNewMention(bot: CodingAgentBot) { bot.onNewMention(async (thread, message) => { - console.log("[coding-agent] onNewMention fired", { - threadId: thread.id, - text: message.text.slice(0, 50), - author: message.author.userId, - }); - const allowedChannels = getAllowedChannelIds(); const allowedUsers = getAllowedUserIds(); if (allowedChannels.length > 0) { const channelId = thread.id.split(":")[1]; if (!allowedChannels.includes(channelId)) { - console.log("[coding-agent] Channel not allowed", { channelId, allowedChannels }); return; } } @@ -31,7 +24,6 @@ export function registerOnNewMention(bot: CodingAgentBot) { if (allowedUsers.length > 0) { const userId = message.author.userId; if (!allowedUsers.includes(userId)) { - console.log("[coding-agent] User not allowed", { userId, allowedUsers }); return; } } @@ -40,16 +32,13 @@ export function registerOnNewMention(bot: CodingAgentBot) { try { await thread.subscribe(); - console.log("[coding-agent] Subscribed to thread"); await thread.post(`Starting work on: "${prompt}"\n\nI'll reply here when done.`); - console.log("[coding-agent] Posted acknowledgment"); const handle = await triggerCodingAgent({ prompt, callbackThreadId: thread.id, }); - console.log("[coding-agent] Triggered coding agent task", { runId: handle.id }); await thread.setState({ status: "running", diff --git a/lib/emails/inbound/respondToInboundEmail.ts b/lib/emails/inbound/respondToInboundEmail.ts index 454e973e..ea75fb3b 100644 --- a/lib/emails/inbound/respondToInboundEmail.ts +++ b/lib/emails/inbound/respondToInboundEmail.ts @@ -19,7 +19,6 @@ export async function respondToInboundEmail( ): Promise { try { const original = event.data; - const emailId = original.email_id; const subject = original.subject ? `Re: ${original.subject}` : "Re: Your email"; const messageId = original.message_id; const to = original.from; @@ -27,30 +26,23 @@ export async function respondToInboundEmail( const from = getFromWithName(original.to, original.cc); const cc = original.cc?.length ? original.cc : undefined; - console.log(`[respondToInboundEmail] Processing email ${emailId} from ${to}`); - // Validate new memory and get chat request body (or early return if duplicate) const validationResult = await validateNewEmailMemory(event); if ("response" in validationResult) { - console.log(`[respondToInboundEmail] Email ${emailId} - early return from validateNewEmailMemory`); return validationResult.response; } const { chatRequestBody, emailText } = validationResult; - console.log(`[respondToInboundEmail] Email ${emailId} - memory validated, roomId=${chatRequestBody.roomId}, emailText length=${emailText.length}`); // Check if Recoup is only CC'd - use LLM to determine if reply is expected const ccValidation = await validateCcReplyExpected(original, emailText); if (ccValidation) { - console.log(`[respondToInboundEmail] Email ${emailId} - CC validation returned early (not a direct reply)`); return ccValidation.response; } const { roomId } = chatRequestBody; - console.log(`[respondToInboundEmail] Email ${emailId} - generating response...`); const { text, html } = await generateEmailResponse(chatRequestBody); - console.log(`[respondToInboundEmail] Email ${emailId} - response generated, text length=${text.length}`); const payload = { from, @@ -63,18 +55,15 @@ export async function respondToInboundEmail( }, }; - console.log(`[respondToInboundEmail] Email ${emailId} - sending reply to ${to} from ${from}`); const result = await sendEmailWithResend(payload); // Save the assistant response message await saveChatCompletion({ text, roomId }); if (result instanceof NextResponse) { - console.log(`[respondToInboundEmail] Email ${emailId} - sendEmailWithResend returned error response`); return result; } - console.log(`[respondToInboundEmail] Email ${emailId} - reply sent successfully`); return NextResponse.json(result); } catch (error) { console.error("[respondToInboundEmail] Failed to respond to inbound email", error); From 16fa074fc42b5bcaaee354c6ece3888b7fbee22f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 15:23:04 -0500 Subject: [PATCH 15/20] refactor: use static imports instead of lazy-load in webhook route Co-Authored-By: Claude Opus 4.6 --- app/api/coding-agent/[platform]/route.ts | 21 +++++++------------- app/api/coding-agent/__tests__/route.test.ts | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/api/coding-agent/[platform]/route.ts b/app/api/coding-agent/[platform]/route.ts index f6d2a718..5e95bff8 100644 --- a/app/api/coding-agent/[platform]/route.ts +++ b/app/api/coding-agent/[platform]/route.ts @@ -1,5 +1,7 @@ import type { NextRequest } from "next/server"; import { after } from "next/server"; +import { codingAgentBot } from "@/lib/coding-agent/bot"; +import "@/lib/coding-agent/handlers/registerHandlers"; /** * POST /api/coding-agent/[platform] @@ -26,20 +28,11 @@ export async function POST( } } - try { - // Lazy-import bot to isolate initialization errors - const { codingAgentBot } = await import("@/lib/coding-agent/bot"); - await import("@/lib/coding-agent/handlers/registerHandlers"); + const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks]; - const handler = codingAgentBot.webhooks[platform as keyof typeof codingAgentBot.webhooks]; - - if (!handler) { - return new Response("Unknown platform", { status: 404 }); - } - - return handler(request, { waitUntil: p => after(() => p) }); - } catch (error) { - console.error("[coding-agent] Failed to initialize bot:", error); - return new Response("Internal server error", { status: 500 }); + if (!handler) { + return new Response("Unknown platform", { status: 404 }); } + + return handler(request, { waitUntil: p => after(() => p) }); } diff --git a/app/api/coding-agent/__tests__/route.test.ts b/app/api/coding-agent/__tests__/route.test.ts index 9c5188d3..d23eb468 100644 --- a/app/api/coding-agent/__tests__/route.test.ts +++ b/app/api/coding-agent/__tests__/route.test.ts @@ -27,7 +27,7 @@ describe("POST /api/coding-agent/[platform]", () => { vi.clearAllMocks(); }); - it("responds to Slack url_verification challenge without loading bot", async () => { + it("responds to Slack url_verification challenge", async () => { const body = JSON.stringify({ type: "url_verification", challenge: "test_challenge_value", From 757dafcae5d83e797f9633d96a1d117d7da3935c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 15:27:27 -0500 Subject: [PATCH 16/20] refactor: remove channel/user allowlist filtering All channels and users are now allowed to interact with the coding agent. Co-Authored-By: Claude Opus 4.6 --- lib/coding-agent/__tests__/config.test.ts | 48 --------------------- lib/coding-agent/__tests__/handlers.test.ts | 23 ---------- lib/coding-agent/config.ts | 23 ---------- lib/coding-agent/handlers/onNewMention.ts | 21 +-------- 4 files changed, 1 insertion(+), 114 deletions(-) delete mode 100644 lib/coding-agent/__tests__/config.test.ts delete mode 100644 lib/coding-agent/config.ts diff --git a/lib/coding-agent/__tests__/config.test.ts b/lib/coding-agent/__tests__/config.test.ts deleted file mode 100644 index fb0a2180..00000000 --- a/lib/coding-agent/__tests__/config.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; - -describe("config", () => { - const originalEnv = process.env; - - beforeEach(() => { - vi.resetModules(); - process.env = { ...originalEnv }; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - describe("getAllowedChannelIds", () => { - it("parses comma-separated channel IDs from env", async () => { - process.env.CODING_AGENT_CHANNELS = "C123,C456,C789"; - const { getAllowedChannelIds } = await import("../config"); - expect(getAllowedChannelIds()).toEqual(["C123", "C456", "C789"]); - }); - - it("returns empty array when env is not set", async () => { - delete process.env.CODING_AGENT_CHANNELS; - const { getAllowedChannelIds } = await import("../config"); - expect(getAllowedChannelIds()).toEqual([]); - }); - - it("trims whitespace from channel IDs", async () => { - process.env.CODING_AGENT_CHANNELS = " C123 , C456 "; - const { getAllowedChannelIds } = await import("../config"); - expect(getAllowedChannelIds()).toEqual(["C123", "C456"]); - }); - }); - - describe("getAllowedUserIds", () => { - it("parses comma-separated user IDs from env", async () => { - process.env.CODING_AGENT_USERS = "U111,U222"; - const { getAllowedUserIds } = await import("../config"); - expect(getAllowedUserIds()).toEqual(["U111", "U222"]); - }); - - it("returns empty array when env is not set", async () => { - delete process.env.CODING_AGENT_USERS; - const { getAllowedUserIds } = await import("../config"); - expect(getAllowedUserIds()).toEqual([]); - }); - }); -}); diff --git a/lib/coding-agent/__tests__/handlers.test.ts b/lib/coding-agent/__tests__/handlers.test.ts index 64564af3..e750d149 100644 --- a/lib/coding-agent/__tests__/handlers.test.ts +++ b/lib/coding-agent/__tests__/handlers.test.ts @@ -27,9 +27,6 @@ describe("registerOnNewMention", () => { }); it("posts acknowledgment and triggers coding agent task", async () => { - process.env.CODING_AGENT_CHANNELS = ""; - process.env.CODING_AGENT_USERS = ""; - const bot = createMockBot(); registerOnNewMention(bot); const handler = bot.onNewMention.mock.calls[0][0]; @@ -56,24 +53,4 @@ describe("registerOnNewMention", () => { }), ); }); - - it("rejects mentions from non-allowed channels", async () => { - process.env.CODING_AGENT_CHANNELS = "C999"; - process.env.CODING_AGENT_USERS = ""; - - const bot = createMockBot(); - registerOnNewMention(bot); - const handler = bot.onNewMention.mock.calls[0][0]; - - const mockThread = { - id: "slack:C123:1234567890.123456", - subscribe: vi.fn(), - post: vi.fn(), - setState: vi.fn(), - }; - - await handler(mockThread, { text: "hi", author: { id: "U111" } }); - - expect(mockThread.subscribe).not.toHaveBeenCalled(); - }); }); diff --git a/lib/coding-agent/config.ts b/lib/coding-agent/config.ts deleted file mode 100644 index c9f23324..00000000 --- a/lib/coding-agent/config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Returns the list of allowed Slack channel IDs from the environment. - */ -export function getAllowedChannelIds(): string[] { - const raw = process.env.CODING_AGENT_CHANNELS; - if (!raw) return []; - return raw - .split(",") - .map(id => id.trim()) - .filter(Boolean); -} - -/** - * Returns the list of allowed Slack user IDs from the environment. - */ -export function getAllowedUserIds(): string[] { - const raw = process.env.CODING_AGENT_USERS; - if (!raw) return []; - return raw - .split(",") - .map(id => id.trim()) - .filter(Boolean); -} diff --git a/lib/coding-agent/handlers/onNewMention.ts b/lib/coding-agent/handlers/onNewMention.ts index fd1078ed..20ecd15e 100644 --- a/lib/coding-agent/handlers/onNewMention.ts +++ b/lib/coding-agent/handlers/onNewMention.ts @@ -1,33 +1,14 @@ import type { CodingAgentBot } from "../bot"; -import { getAllowedChannelIds, getAllowedUserIds } from "../config"; import { triggerCodingAgent } from "@/lib/trigger/triggerCodingAgent"; /** * Registers the onNewMention handler on the bot. - * Validates channel/user against allowlist, subscribes to the thread, - * and triggers the coding agent Trigger.dev task. + * Subscribes to the thread and triggers the coding agent Trigger.dev task. * * @param bot */ export function registerOnNewMention(bot: CodingAgentBot) { bot.onNewMention(async (thread, message) => { - const allowedChannels = getAllowedChannelIds(); - const allowedUsers = getAllowedUserIds(); - - if (allowedChannels.length > 0) { - const channelId = thread.id.split(":")[1]; - if (!allowedChannels.includes(channelId)) { - return; - } - } - - if (allowedUsers.length > 0) { - const userId = message.author.userId; - if (!allowedUsers.includes(userId)) { - return; - } - } - const prompt = message.text; try { From 9e7c898b086aa803fc0e9fbcd69f1c674c594805 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 15:29:38 -0500 Subject: [PATCH 17/20] refactor: extract getThread and handlePRCreated into separate files (SRP) Co-Authored-By: Claude Opus 4.6 --- lib/coding-agent/__tests__/getThread.test.ts | 20 +++++++++ .../__tests__/handlePRCreated.test.ts | 33 ++++++++++++++ lib/coding-agent/getThread.ts | 17 +++++++ lib/coding-agent/handleCodingAgentCallback.ts | 44 +------------------ lib/coding-agent/handlePRCreated.ts | 26 +++++++++++ 5 files changed, 98 insertions(+), 42 deletions(-) create mode 100644 lib/coding-agent/__tests__/getThread.test.ts create mode 100644 lib/coding-agent/__tests__/handlePRCreated.test.ts create mode 100644 lib/coding-agent/getThread.ts create mode 100644 lib/coding-agent/handlePRCreated.ts diff --git a/lib/coding-agent/__tests__/getThread.test.ts b/lib/coding-agent/__tests__/getThread.test.ts new file mode 100644 index 00000000..e44198aa --- /dev/null +++ b/lib/coding-agent/__tests__/getThread.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, vi } from "vitest"; + +vi.mock("chat", () => ({ + ThreadImpl: vi.fn().mockImplementation((config: Record) => config), +})); + +describe("getThread", () => { + it("parses adapter name and channel ID from thread ID", async () => { + const { getThread } = await import("../getThread"); + const { ThreadImpl } = await import("chat"); + + getThread("slack:C123:1234567890.123456"); + + expect(ThreadImpl).toHaveBeenCalledWith({ + adapterName: "slack", + id: "slack:C123:1234567890.123456", + channelId: "slack:C123", + }); + }); +}); diff --git a/lib/coding-agent/__tests__/handlePRCreated.test.ts b/lib/coding-agent/__tests__/handlePRCreated.test.ts new file mode 100644 index 00000000..8208c8bb --- /dev/null +++ b/lib/coding-agent/__tests__/handlePRCreated.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi } from "vitest"; + +const mockThread = { + post: vi.fn(), + setState: vi.fn(), +}; + +vi.mock("../getThread", () => ({ + getThread: vi.fn(() => mockThread), +})); + +describe("handlePRCreated", () => { + it("posts PR links and updates thread state", async () => { + const { handlePRCreated } = await import("../handlePRCreated"); + + await handlePRCreated("slack:C123:ts", { + threadId: "slack:C123:ts", + status: "pr_created", + branch: "agent/fix-bug", + snapshotId: "snap_abc", + prs: [{ repo: "recoupable/api", number: 42, url: "https://github.com/recoupable/api/pull/42", baseBranch: "test" }], + }); + + expect(mockThread.post).toHaveBeenCalledWith(expect.stringContaining("recoupable/api#42")); + expect(mockThread.setState).toHaveBeenCalledWith( + expect.objectContaining({ + status: "pr_created", + branch: "agent/fix-bug", + snapshotId: "snap_abc", + }), + ); + }); +}); diff --git a/lib/coding-agent/getThread.ts b/lib/coding-agent/getThread.ts new file mode 100644 index 00000000..1322db4d --- /dev/null +++ b/lib/coding-agent/getThread.ts @@ -0,0 +1,17 @@ +import { ThreadImpl } from "chat"; +import type { CodingAgentThreadState } from "./types"; + +/** + * Reconstructs a Thread from a stored thread ID using the Chat SDK singleton. + * + * @param threadId + */ +export function getThread(threadId: string) { + const adapterName = threadId.split(":")[0]; + const channelId = `${adapterName}:${threadId.split(":")[1]}`; + return new ThreadImpl({ + adapterName, + id: threadId, + channelId, + }); +} diff --git a/lib/coding-agent/handleCodingAgentCallback.ts b/lib/coding-agent/handleCodingAgentCallback.ts index 4c370ffa..caf13c04 100644 --- a/lib/coding-agent/handleCodingAgentCallback.ts +++ b/lib/coding-agent/handleCodingAgentCallback.ts @@ -1,48 +1,8 @@ import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateCodingAgentCallback } from "./validateCodingAgentCallback"; -import type { CodingAgentCallbackBody } from "./validateCodingAgentCallback"; -import { ThreadImpl } from "chat"; -import type { CodingAgentThreadState } from "./types"; - -/** - * Reconstructs a Thread from a stored thread ID using the Chat SDK singleton. - * - * @param threadId - */ -function getThread(threadId: string) { - const adapterName = threadId.split(":")[0]; - const channelId = `${adapterName}:${threadId.split(":")[1]}`; - return new ThreadImpl({ - adapterName, - id: threadId, - channelId, - }); -} - -/** - * Handles the pr_created callback status. - * - * @param threadId - * @param body - */ -async function handlePRCreated(threadId: string, body: CodingAgentCallbackBody) { - const thread = getThread(threadId); - const prLinks = (body.prs ?? []) - .map(pr => `- [${pr.repo}#${pr.number}](${pr.url}) → \`${pr.baseBranch}\``) - .join("\n"); - - await thread.post( - `PRs created:\n${prLinks}\n\nReply in this thread to give feedback, or click Merge when ready.`, - ); - - await thread.setState({ - status: "pr_created", - branch: body.branch, - snapshotId: body.snapshotId, - prs: body.prs, - }); -} +import { getThread } from "./getThread"; +import { handlePRCreated } from "./handlePRCreated"; /** * Handles coding agent task callback from Trigger.dev. diff --git a/lib/coding-agent/handlePRCreated.ts b/lib/coding-agent/handlePRCreated.ts new file mode 100644 index 00000000..7b62f096 --- /dev/null +++ b/lib/coding-agent/handlePRCreated.ts @@ -0,0 +1,26 @@ +import { getThread } from "./getThread"; +import type { CodingAgentCallbackBody } from "./validateCodingAgentCallback"; + +/** + * Handles the pr_created callback status. + * + * @param threadId + * @param body + */ +export async function handlePRCreated(threadId: string, body: CodingAgentCallbackBody) { + const thread = getThread(threadId); + const prLinks = (body.prs ?? []) + .map(pr => `- [${pr.repo}#${pr.number}](${pr.url}) → \`${pr.baseBranch}\``) + .join("\n"); + + await thread.post( + `PRs created:\n${prLinks}\n\nReply in this thread to give feedback, or click Merge when ready.`, + ); + + await thread.setState({ + status: "pr_created", + branch: body.branch, + snapshotId: body.snapshotId, + prs: body.prs, + }); +} From 56ad22aa0e16fa421f3f04b82737c4ba1c3e3666 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 6 Mar 2026 15:33:59 -0500 Subject: [PATCH 18/20] fix: restore pr_created callback handling, remove updated status Keep pr_created flow (posts PR links to Slack thread). Remove updated status (GitHub-specific, belongs in MYC-4431). Co-Authored-By: Claude Opus 4.6 --- .../__tests__/handleCodingAgentCallback.test.ts | 13 ------------- .../__tests__/validateCodingAgentCallback.test.ts | 11 +---------- lib/coding-agent/handleCodingAgentCallback.ts | 5 ----- lib/coding-agent/types.ts | 12 +++++++++++- lib/coding-agent/validateCodingAgentCallback.ts | 2 +- 5 files changed, 13 insertions(+), 30 deletions(-) diff --git a/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts b/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts index 05dfe22a..b0173c9e 100644 --- a/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts +++ b/lib/coding-agent/__tests__/handleCodingAgentCallback.test.ts @@ -121,17 +121,4 @@ describe("handleCodingAgentCallback", () => { expect(mockPost).toHaveBeenCalledWith(expect.stringContaining("Sandbox timed out")); }); - it("posts updated confirmation for updated status", async () => { - const body = { - threadId: "slack:C123:1234567890.123456", - status: "updated", - snapshotId: "snap_new", - }; - const request = makeRequest(body); - - const response = await handleCodingAgentCallback(request); - - expect(response.status).toBe(200); - expect(mockPost).toHaveBeenCalledWith(expect.stringContaining("updated")); - }); }); diff --git a/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts b/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts index 525aa4ff..eccff62e 100644 --- a/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts +++ b/lib/coding-agent/__tests__/validateCodingAgentCallback.test.ts @@ -44,16 +44,7 @@ describe("validateCodingAgentCallback", () => { expect(result).not.toBeInstanceOf(NextResponse); }); - it("accepts updated status with new snapshotId", () => { - const body = { - threadId: "slack:C123:1234567890.123456", - status: "updated", - snapshotId: "snap_new456", - }; - const result = validateCodingAgentCallback(body); - expect(result).not.toBeInstanceOf(NextResponse); - }); - }); +}); describe("invalid payloads", () => { it("rejects missing threadId", () => { diff --git a/lib/coding-agent/handleCodingAgentCallback.ts b/lib/coding-agent/handleCodingAgentCallback.ts index caf13c04..ec2ead00 100644 --- a/lib/coding-agent/handleCodingAgentCallback.ts +++ b/lib/coding-agent/handleCodingAgentCallback.ts @@ -43,11 +43,6 @@ export async function handleCodingAgentCallback(request: Request): Promise Date: Fri, 6 Mar 2026 15:42:17 -0500 Subject: [PATCH 19/20] fix: add JSON parse error handling and fail-fast Redis connect Address CodeRabbit feedback: - handleCodingAgentCallback: catch malformed JSON, return 400 - bot.ts: throw on Redis connect failure instead of swallowing error Co-Authored-By: Claude Opus 4.6 --- lib/coding-agent/bot.ts | 4 +++- lib/coding-agent/handleCodingAgentCallback.ts | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/coding-agent/bot.ts b/lib/coding-agent/bot.ts index 0f4d7f13..7ed49712 100644 --- a/lib/coding-agent/bot.ts +++ b/lib/coding-agent/bot.ts @@ -15,7 +15,9 @@ export function createCodingAgentBot() { // ioredis is configured with lazyConnect: true, so we must // explicitly connect before the state adapter listens for "ready". if (redis.status === "wait") { - redis.connect().catch(err => console.error("[coding-agent] Redis connect error:", err)); + redis.connect().catch(() => { + throw new Error("[coding-agent] Redis failed to connect"); + }); } const state = createIoRedisState({ diff --git a/lib/coding-agent/handleCodingAgentCallback.ts b/lib/coding-agent/handleCodingAgentCallback.ts index ec2ead00..3796780d 100644 --- a/lib/coding-agent/handleCodingAgentCallback.ts +++ b/lib/coding-agent/handleCodingAgentCallback.ts @@ -22,7 +22,16 @@ export async function handleCodingAgentCallback(request: Request): Promise Date: Fri, 6 Mar 2026 15:44:02 -0500 Subject: [PATCH 20/20] feat: add onSubscribedMessage handler for feedback loop When a user replies in a thread with pr_created status, triggers the update-pr task with their feedback. Tells user to wait if agent is still running. Co-Authored-By: Claude Opus 4.6 --- .../__tests__/onSubscribedMessage.test.ts | 75 +++++++++++++++++++ .../handlers/onSubscribedMessage.ts | 37 +++++++++ lib/coding-agent/handlers/registerHandlers.ts | 2 + lib/coding-agent/types.ts | 2 +- lib/trigger/triggerUpdatePR.ts | 21 ++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 lib/coding-agent/__tests__/onSubscribedMessage.test.ts create mode 100644 lib/coding-agent/handlers/onSubscribedMessage.ts create mode 100644 lib/trigger/triggerUpdatePR.ts diff --git a/lib/coding-agent/__tests__/onSubscribedMessage.test.ts b/lib/coding-agent/__tests__/onSubscribedMessage.test.ts new file mode 100644 index 00000000..f4647420 --- /dev/null +++ b/lib/coding-agent/__tests__/onSubscribedMessage.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@/lib/trigger/triggerUpdatePR", () => ({ + triggerUpdatePR: vi.fn().mockResolvedValue({ id: "run_456" }), +})); + +const { registerOnSubscribedMessage } = await import("../handlers/onSubscribedMessage"); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +function createMockBot() { + return { + onSubscribedMessage: vi.fn(), + } as any; +} + +describe("registerOnSubscribedMessage", () => { + it("registers a handler on the bot", () => { + const bot = createMockBot(); + registerOnSubscribedMessage(bot); + expect(bot.onSubscribedMessage).toHaveBeenCalledOnce(); + }); + + it("triggers update PR task when status is pr_created", async () => { + const { triggerUpdatePR } = await import("@/lib/trigger/triggerUpdatePR"); + + const bot = createMockBot(); + registerOnSubscribedMessage(bot); + const handler = bot.onSubscribedMessage.mock.calls[0][0]; + + const mockThread = { + id: "slack:C123:1234567890.123456", + state: Promise.resolve({ + status: "pr_created", + prompt: "fix bug", + snapshotId: "snap_abc", + branch: "agent/fix-bug", + prs: [{ repo: "recoupable/api", number: 1, url: "url", baseBranch: "test" }], + }), + post: vi.fn(), + setState: vi.fn(), + }; + + await handler(mockThread, { text: "make the button blue", author: { userId: "U111" } }); + + expect(mockThread.post).toHaveBeenCalledWith(expect.stringContaining("feedback")); + expect(mockThread.setState).toHaveBeenCalledWith(expect.objectContaining({ status: "updating" })); + expect(triggerUpdatePR).toHaveBeenCalledWith( + expect.objectContaining({ + feedback: "make the button blue", + snapshotId: "snap_abc", + repo: "recoupable/api", + }), + ); + }); + + it("tells user to wait when agent is running", async () => { + const bot = createMockBot(); + registerOnSubscribedMessage(bot); + const handler = bot.onSubscribedMessage.mock.calls[0][0]; + + const mockThread = { + id: "slack:C123:1234567890.123456", + state: Promise.resolve({ status: "running", prompt: "fix bug" }), + post: vi.fn(), + setState: vi.fn(), + }; + + await handler(mockThread, { text: "hurry up", author: { userId: "U111" } }); + + expect(mockThread.post).toHaveBeenCalledWith(expect.stringContaining("still working")); + }); +}); diff --git a/lib/coding-agent/handlers/onSubscribedMessage.ts b/lib/coding-agent/handlers/onSubscribedMessage.ts new file mode 100644 index 00000000..1846c541 --- /dev/null +++ b/lib/coding-agent/handlers/onSubscribedMessage.ts @@ -0,0 +1,37 @@ +import type { CodingAgentBot } from "../bot"; +import { triggerUpdatePR } from "@/lib/trigger/triggerUpdatePR"; + +/** + * Registers the onSubscribedMessage handler on the bot. + * If the agent has created PRs, treats the message as feedback and + * triggers the update-pr task. If the agent is currently working, + * tells the user to wait. + * + * @param bot + */ +export function registerOnSubscribedMessage(bot: CodingAgentBot) { + bot.onSubscribedMessage(async (thread, message) => { + const state = await thread.state; + + if (!state) return; + + if (state.status === "running" || state.status === "updating") { + await thread.post("I'm still working on this. I'll let you know when I'm done."); + return; + } + + if (state.status === "pr_created" && state.snapshotId && state.branch && state.prs?.length) { + await thread.post("Got your feedback. Updating the PRs..."); + + await thread.setState({ status: "updating" }); + + await triggerUpdatePR({ + feedback: message.text, + snapshotId: state.snapshotId, + branch: state.branch, + repo: state.prs[0].repo, + callbackThreadId: thread.id, + }); + } + }); +} diff --git a/lib/coding-agent/handlers/registerHandlers.ts b/lib/coding-agent/handlers/registerHandlers.ts index c6a2f6da..788e4e46 100644 --- a/lib/coding-agent/handlers/registerHandlers.ts +++ b/lib/coding-agent/handlers/registerHandlers.ts @@ -1,8 +1,10 @@ import { codingAgentBot } from "../bot"; import { registerOnNewMention } from "./onNewMention"; +import { registerOnSubscribedMessage } from "./onSubscribedMessage"; /** * Registers all coding agent event handlers on the bot singleton. * Import this file once to attach handlers to the bot. */ registerOnNewMention(codingAgentBot); +registerOnSubscribedMessage(codingAgentBot); diff --git a/lib/coding-agent/types.ts b/lib/coding-agent/types.ts index 75398c97..28a583fe 100644 --- a/lib/coding-agent/types.ts +++ b/lib/coding-agent/types.ts @@ -3,7 +3,7 @@ * Stored in Redis via Chat SDK's state adapter. */ export interface CodingAgentThreadState { - status: "running" | "pr_created" | "failed"; + status: "running" | "pr_created" | "updating" | "failed"; prompt: string; runId?: string; slackThreadId?: string; diff --git a/lib/trigger/triggerUpdatePR.ts b/lib/trigger/triggerUpdatePR.ts new file mode 100644 index 00000000..f989b576 --- /dev/null +++ b/lib/trigger/triggerUpdatePR.ts @@ -0,0 +1,21 @@ +import { tasks } from "@trigger.dev/sdk"; + +type UpdatePRPayload = { + feedback: string; + snapshotId: string; + branch: string; + repo: string; + callbackThreadId: string; +}; + +/** + * Triggers the update-pr task to resume a sandbox from snapshot, + * apply feedback via the AI agent, and push updates to existing PRs. + * + * @param payload - The task payload with feedback, snapshot, branch, and PR info + * @returns The task handle with runId + */ +export async function triggerUpdatePR(payload: UpdatePRPayload) { + const handle = await tasks.trigger("update-pr", payload); + return handle; +}