diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a6707f7..1f41de77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: npm i npm run build - name: Publish package for testing branch - run: npx pkg-pr-new publish || echo "Have you set up pkg-pr-new for this repo?" + run: npx pkg-pr-new publish || echo "pkg-pr-new failed" - name: Test run: | npm run test diff --git a/eslint.config.js b/eslint.config.js index e4fde5a2..9ddaf779 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import pluginJs from "@eslint/js"; import tseslint from "typescript-eslint"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import convexPlugin from "@convex-dev/eslint-plugin"; export default [ { @@ -51,7 +52,12 @@ export default [ languageOptions: { globals: globals.worker, }, + plugins: { + "@convex-dev": convexPlugin, + }, rules: { + ...convexPlugin.configs.recommended[0].rules, + "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-explicit-any": "off", "no-unused-vars": "off", diff --git a/example/convex/approval.test.ts b/example/convex/approval.test.ts index b48e8795..e7994e57 100644 --- a/example/convex/approval.test.ts +++ b/example/convex/approval.test.ts @@ -2,7 +2,11 @@ import { describe, expect, test } from "vitest"; import { Agent, createTool, stepCountIs, mockModel } from "@convex-dev/agent"; import { anyApi, actionGeneric, mutationGeneric } from "convex/server"; -import type { ApiFromModules, ActionBuilder, MutationBuilder } from "convex/server"; +import type { + ApiFromModules, + ActionBuilder, + MutationBuilder, +} from "convex/server"; import { v } from "convex/values"; import { components } from "./_generated/api.js"; import { initConvexTest } from "./setup.test.js"; @@ -153,9 +157,7 @@ export const testApproveE2E = action({ const approvalMsg = result1.savedMessages?.find( (m) => Array.isArray(m.message?.content) && - m.message!.content.some( - (p: any) => p.type === "tool-approval-request", - ), + m.message!.content.some((p: any) => p.type === "tool-approval-request"), ); if (!approvalMsg) throw new Error("No approval request found in saved messages"); @@ -209,9 +211,7 @@ export const testDenyE2E = action({ const approvalMsg = result1.savedMessages?.find( (m) => Array.isArray(m.message?.content) && - m.message!.content.some( - (p: any) => p.type === "tool-approval-request", - ), + m.message!.content.some((p: any) => p.type === "tool-approval-request"), ); if (!approvalMsg) throw new Error("No approval request found"); const approvalPart = (approvalMsg.message!.content as any[]).find( @@ -323,7 +323,11 @@ export const submitApprovalForTestApprovalAgent = mutation({ reason: v.optional(v.string()), }, handler: async (ctx, { threadId, approvalId, reason }) => { - return testApprovalAgent.approveToolCall(ctx, { threadId, approvalId, reason }); + return testApprovalAgent.approveToolCall(ctx, { + threadId, + approvalId, + reason, + }); }, }); @@ -403,9 +407,7 @@ describe("Example Approval E2E (exercises usageHandler + insertRawUsage)", () => const t = initConvexTest(); const result = await t.action(testApi.testDenyE2E, {}); - expect(result.secondText).toBe( - "Understood, I won't delete that file.", - ); + expect(result.secondText).toBe("Understood, I won't delete that file."); expect(result.totalThreadMessages).toBeGreaterThanOrEqual(4); const rawUsageDocs = await t.run(async (ctx) => { diff --git a/example/convex/chat/approval.ts b/example/convex/chat/approval.ts index 0fb279ec..edf55b92 100644 --- a/example/convex/chat/approval.ts +++ b/example/convex/chat/approval.ts @@ -8,11 +8,7 @@ // 5. AI SDK automatically handles the approval: executes tool (if approved) // or creates execution-denied result (if denied), then continues generation import { paginationOptsValidator } from "convex/server"; -import { - listUIMessages, - syncStreams, - vStreamArgs, -} from "@convex-dev/agent"; +import { listUIMessages, syncStreams, vStreamArgs } from "@convex-dev/agent"; import { components, internal } from "../_generated/api"; import { internalAction, mutation, query } from "../_generated/server"; import { v } from "convex/values"; @@ -74,7 +70,11 @@ export const submitApproval = mutation({ handler: async (ctx, { threadId, approvalId, approved, reason }) => { await authorizeThreadAccess(ctx, threadId); const { messageId } = approved - ? await approvalAgent.approveToolCall(ctx, { threadId, approvalId, reason }) + ? await approvalAgent.approveToolCall(ctx, { + threadId, + approvalId, + reason, + }) : await approvalAgent.denyToolCall(ctx, { threadId, approvalId, reason }); return { messageId }; }, diff --git a/example/convex/usage_tracking/invoicing.ts b/example/convex/usage_tracking/invoicing.ts index a1cfdf14..366a1ec0 100644 --- a/example/convex/usage_tracking/invoicing.ts +++ b/example/convex/usage_tracking/invoicing.ts @@ -145,6 +145,7 @@ async function createInvoice( .withIndex("billingPeriod_userId", (q) => q.eq("billingPeriod", billingPeriod).eq("userId", invoice.userId), ) + // eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many failed invoices .filter((q) => q.neq(q.field("status"), "failed")) .first(); if (existingInvoice) { diff --git a/example/ui/chat/ChatApproval.tsx b/example/ui/chat/ChatApproval.tsx index 12a6656a..25bfdc11 100644 --- a/example/ui/chat/ChatApproval.tsx +++ b/example/ui/chat/ChatApproval.tsx @@ -34,7 +34,11 @@ export default function ChatApproval() { } function Chat({ threadId, reset }: { threadId: string; reset: () => void }) { - const { results: messages, status, loadMore } = useUIMessages( + const { + results: messages, + status, + loadMore, + } = useUIMessages( api.chat.approval.listThreadMessages, { threadId }, { initialNumItems: 10, stream: true }, @@ -47,7 +51,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) { ); const submitApproval = useMutation(api.chat.approval.submitApproval); - const triggerContinuation = useMutation(api.chat.approval.triggerContinuation); + const triggerContinuation = useMutation( + api.chat.approval.triggerContinuation, + ); // Track the last approval messageId so we can use it for continuation. const lastApprovalMessageIdRef = useRef(null); @@ -56,7 +62,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) { const hasPendingApprovals = messages.some((m) => m.parts.some( - (p) => p.type.startsWith("tool-") && (p as ToolUIPart).state === "approval-requested", + (p) => + p.type.startsWith("tool-") && + (p as ToolUIPart).state === "approval-requested", ), ); @@ -90,7 +98,9 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) { lastApprovalMessageIdRef.current = messageId; } - const [prompt, setPrompt] = useState("Delete the file important.txt and transfer $500 to account savings-123"); + const [prompt, setPrompt] = useState( + "Delete the file important.txt and transfer $500 to account savings-123", + ); const messagesEndRef = useRef(null); useEffect(() => { @@ -142,7 +152,11 @@ function Chat({ threadId, reset }: { threadId: string; reset: () => void }) { value={prompt} onChange={(e) => setPrompt(e.target.value)} className="flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" - placeholder={hasPendingApprovals ? "Respond to pending approvals first..." : "Ask the agent to do something..."} + placeholder={ + hasPendingApprovals + ? "Respond to pending approvals first..." + : "Ask the agent to do something..." + } disabled={hasPendingApprovals} />