Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e41dd84
feat: add modular content creation primitive endpoints
sidneyswift Apr 2, 2026
330bfe0
fix: inline route segment config (Next.js 16 requires static analysis)
sidneyswift Apr 2, 2026
b2de86a
fix: address CodeRabbit review comments
sidneyswift Apr 2, 2026
db2aeb7
fix: resolve all lint errors in new primitive files
sidneyswift Apr 2, 2026
ac5e069
refactor: make primitives run inline instead of triggering tasks
sidneyswift Apr 2, 2026
9b7ee89
fix: enforce validateAuthContext at handler level in content primitives
sidneyswift Apr 2, 2026
b44f94c
feat: add POST /api/content/create/analyze (Twelve Labs video analysis)
sidneyswift Apr 2, 2026
619a04f
fix: rename content/create/analyze to content/analyze
sidneyswift Apr 2, 2026
36ea2b6
refactor: rename content primitive routes to verb-qualifier pattern
sidneyswift Apr 2, 2026
10e4e84
refactor: make content primitives generic + replace render with edit
sidneyswift Apr 2, 2026
5aba6f8
fix: address CodeRabbit review — split text handler, DRY route factory
sidneyswift Apr 2, 2026
ff6f467
fix: make image_url optional in generate-video, add prompt field
sidneyswift Apr 2, 2026
9a170df
chore: redeploy with FAL_KEY
sidneyswift Apr 2, 2026
45075e5
chore: redeploy with updated FAL_KEY
sidneyswift Apr 2, 2026
6ee56f0
fix: upgrade to nano-banana-2, auto-select t2i vs edit model
sidneyswift Apr 2, 2026
6288ac3
feat: add num_images, aspect_ratio, resolution to generate-image
sidneyswift Apr 2, 2026
aa0f359
feat: set optimal internal defaults for image generation
sidneyswift Apr 2, 2026
9139b21
feat: auto-select Veo 3.1 model variant based on inputs
sidneyswift Apr 2, 2026
c366ef4
feat: full generate-video upgrade — extend mode, duration, resolution…
sidneyswift Apr 2, 2026
9790f35
feat: expose missing params for transcribe + upscale, fix generate_au…
sidneyswift Apr 2, 2026
3b542e7
feat: add mode param to generate-video with 6 modes
sidneyswift Apr 2, 2026
5ac87a6
fix: correct text-to-video model ID (fal-ai/veo3.1, not /text-to-video)
sidneyswift Apr 2, 2026
99fb840
fix: correct fal field mappings for reference and first-last modes
sidneyswift Apr 2, 2026
e729077
refactor: simplify endpoint paths per code review
sidneyswift Apr 2, 2026
cf6a0b4
chore: redeploy with updated RECOUP_API_KEY
sidneyswift Apr 2, 2026
9b6616e
chore: redeploy with new RECOUP_API_KEY for caption
sidneyswift Apr 2, 2026
7c09401
fix: always send prompt field to fal (LTX lipsync requires it even wh…
sidneyswift Apr 2, 2026
99914f4
refactor: caption handler calls AI SDK directly instead of HTTP self-…
sidneyswift Apr 2, 2026
64284c4
refactor: extract configureFal and buildFalInput (DRY + SRP)
sidneyswift Apr 2, 2026
d29d08c
fix: remove unused stream from analyze schema, DRY video OPTIONS handler
sidneyswift Apr 3, 2026
74795ac
feat: add template support to all content primitives
sidneyswift Apr 3, 2026
4ca9588
feat: content V2 — edit route, template detail, malleable mode, MCP t…
sidneyswift Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/accounts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function OPTIONS() {
* - id (required): The unique identifier of the account (UUID)
*
* @param request - The request object
* @param params.params
* @param params - Route params containing the account ID
* @returns A NextResponse with account data
*/
Expand Down
2 changes: 2 additions & 0 deletions app/api/admins/coding/slack/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { getSlackTagsHandler } from "@/lib/admins/slack/getSlackTagsHandler";
* Pulls directly from the Slack API as the source of truth.
* Supports period filtering: all (default), daily, weekly, monthly.
* Requires admin authentication.
*
* @param request
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getSlackTagsHandler(request);
Expand Down
5 changes: 5 additions & 0 deletions app/api/admins/privy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { getPrivyLoginsHandler } from "@/lib/admins/privy/getPrivyLoginsHandler"
* Returns Privy login statistics for the requested time period.
* Supports daily (last 24h), weekly (last 7 days), and monthly (last 30 days) periods.
* Requires admin authentication.
*
* @param request
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getPrivyLoginsHandler(request);
}

/**
*
*/
export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}
13 changes: 13 additions & 0 deletions app/api/content/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createAnalyzeHandler } from "@/lib/content/primitives/createAnalyzeHandler";
import { createPrimitiveRoute } from "@/lib/content/primitives/createPrimitiveRoute";

/**
* POST /api/content/analyze
*
* Analyze a video and generate text based on its content.
*/
export const { OPTIONS, POST } = createPrimitiveRoute(createAnalyzeHandler);

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
13 changes: 13 additions & 0 deletions app/api/content/caption/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createTextHandler } from "@/lib/content/primitives/createTextHandler";
import { createPrimitiveRoute } from "@/lib/content/primitives/createPrimitiveRoute";

/**
* POST /api/content/caption
*
* Generate on-screen caption text for a social video.
*/
export const { OPTIONS, POST } = createPrimitiveRoute(createTextHandler);

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
13 changes: 13 additions & 0 deletions app/api/content/image/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createImageHandler } from "@/lib/content/primitives/createImageHandler";
import { createPrimitiveRoute } from "@/lib/content/primitives/createPrimitiveRoute";

/**
* POST /api/content/image
*
* Generate an image from a prompt and optional reference image.
*/
export const { OPTIONS, POST } = createPrimitiveRoute(createImageHandler);

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
15 changes: 15 additions & 0 deletions app/api/content/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { editHandler } from "@/lib/content/primitives/editHandler";
import { primitiveOptionsHandler } from "@/lib/content/primitives/createPrimitiveRoute";

export { primitiveOptionsHandler as OPTIONS };

/**
* PATCH /api/content
*
* Edit media with operations or a template preset.
*/
export { editHandler as PATCH };

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
26 changes: 26 additions & 0 deletions app/api/content/templates/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getContentTemplateDetailHandler } from "@/lib/content/getContentTemplateDetailHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns Empty 204 response with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: getCorsHeaders(),
});
}

/**
* GET /api/content/templates/[id]
*
* Returns the full template configuration for a given template id.
*/
export { getContentTemplateDetailHandler as GET };

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
13 changes: 13 additions & 0 deletions app/api/content/transcribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createAudioHandler } from "@/lib/content/primitives/createAudioHandler";
import { createPrimitiveRoute } from "@/lib/content/primitives/createPrimitiveRoute";

/**
* POST /api/content/transcribe
*
* Transcribe a song into timestamped lyrics.
*/
export const { OPTIONS, POST } = createPrimitiveRoute(createAudioHandler);

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
13 changes: 13 additions & 0 deletions app/api/content/upscale/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createUpscaleHandler } from "@/lib/content/primitives/createUpscaleHandler";
import { createPrimitiveRoute } from "@/lib/content/primitives/createPrimitiveRoute";

/**
* POST /api/content/upscale
*
* Upscale an image or video to higher resolution.
*/
export const { OPTIONS, POST } = createPrimitiveRoute(createUpscaleHandler);

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
15 changes: 15 additions & 0 deletions app/api/content/video/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createVideoHandler } from "@/lib/content/primitives/createVideoHandler";
import { primitiveOptionsHandler } from "@/lib/content/primitives/createPrimitiveRoute";

export { primitiveOptionsHandler as OPTIONS };

/**
* POST /api/content/video
*
* Generate a video from a prompt, image, or existing video.
*/
export { createVideoHandler as POST };

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
1 change: 1 addition & 0 deletions app/api/songs/analyze/presets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function OPTIONS() {
* - status: "success"
* - presets: Array of { name, label, description, requiresAudio, responseFormat }
*
* @param request
* @returns A NextResponse with the list of available presets
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
Expand Down
4 changes: 4 additions & 0 deletions app/api/transcribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
import { processAudioTranscription } from "@/lib/transcribe/processAudioTranscription";
import { formatTranscriptionError } from "@/lib/transcribe/types";

/**
*
* @param req
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ vi.mock("@/lib/admins/validateAdminAuth", () => ({
validateAdminAuth: vi.fn(),
}));

/**
*
* @param url
*/
function createMockRequest(url: string): NextRequest {
return {
url,
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/pr/__tests__/getPrMergedStatusHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ vi.mock("@/lib/github/fetchGithubPrStatus", () => ({
const PR_URL_1 = "https://github.com/recoupable/api/pull/42";
const PR_URL_2 = "https://github.com/recoupable/chat/pull/100";

/**
*
* @param urls
*/
function makeRequest(urls: string[] = [PR_URL_1]) {
const params = new URLSearchParams();
urls.forEach(url => params.append("pull_requests", url));
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/pr/getPrStatusHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { fetchGithubPrStatus } from "@/lib/github/fetchGithubPrStatus";
* Uses the GitHub REST API to check each PR's state.
*
* Requires admin authentication.
*
* @param request
*/
export async function getPrStatusHandler(request: NextRequest): Promise<NextResponse> {
try {
Expand Down
3 changes: 3 additions & 0 deletions lib/admins/privy/countNewAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { getCutoffMs } from "./getCutoffMs";

/**
* Counts how many users in the list were created within the cutoff period.
*
* @param users
* @param period
*/
export function countNewAccounts(users: User[], period: PrivyLoginsPeriod): number {
const cutoffMs = getCutoffMs(period);
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/privy/fetchPrivyLogins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type FetchPrivyLoginsResult = {
totalPrivyUsers: number;
};

/**
*
* @param period
*/
export async function fetchPrivyLogins(period: PrivyLoginsPeriod): Promise<FetchPrivyLoginsResult> {
const isAll = period === "all";
const cutoffMs = getCutoffMs(period);
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getCutoffMs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { PERIOD_DAYS } from "./periodDays";
* Returns the cutoff timestamp in milliseconds for a given period.
* Uses midnight UTC calendar day boundaries to match Privy dashboard behavior.
* Returns 0 for "all" (no cutoff).
*
* @param period
*/
export function getCutoffMs(period: PrivyLoginsPeriod): number {
if (period === "all") return 0;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getLatestVerifiedAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { User } from "@privy-io/node";
/**
* Returns the most recent latest_verified_at (in ms) across all linked_accounts for a Privy user.
* Returns null if no linked account has a latest_verified_at.
*
* @param user
*/
export function getLatestVerifiedAt(user: User): number | null {
const linkedAccounts = user.linked_accounts;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/toMs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Normalizes a Privy timestamp to milliseconds.
* Privy docs say milliseconds but examples show seconds (10 digits).
*
* @param timestamp
*/
export function toMs(timestamp: number): number {
return timestamp > 1e12 ? timestamp : timestamp * 1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ describe("handleContentAgentCallback", () => {
});

describe("completed callback with videos", () => {
/**
*
* @param body
*/
function makeAuthRequest(body: object) {
return new Request("http://localhost/api/content-agent/callback", {
method: "POST",
Expand All @@ -92,6 +96,9 @@ describe("handleContentAgentCallback", () => {
});
}

/**
*
*/
function mockThread() {
const thread = {
post: vi.fn().mockResolvedValue(undefined),
Expand Down
1 change: 1 addition & 0 deletions lib/ai/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";

/**
* Returns a specific model by its ID from the list of available models.
*
* @param modelId - The ID of the model to find
* @returns The matching model or undefined if not found
*/
Expand Down
2 changes: 2 additions & 0 deletions lib/ai/isEmbedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";
/**
* Determines if a model is an embedding model (not suitable for chat).
* Embed models typically have 0 output pricing since they only produce embeddings.
*
* @param m
*/
export const isEmbedModel = (m: GatewayLanguageModelEntry): boolean => {
const pricing = m.pricing;
Expand Down
5 changes: 5 additions & 0 deletions lib/artists/__tests__/createArtistPostHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: (...args: unknown[]) => mockValidateAuthContext(...args),
}));

/**
*
* @param body
* @param headers
*/
function createRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
const defaultHeaders: Record<string, string> = {
"Content-Type": "application/json",
Expand Down
5 changes: 5 additions & 0 deletions lib/artists/__tests__/validateCreateArtistBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: (...args: unknown[]) => mockValidateAuthContext(...args),
}));

/**
*
* @param body
* @param headers
*/
function createRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
const defaultHeaders: Record<string, string> = { "Content-Type": "application/json" };
return new NextRequest("http://localhost/api/artists", {
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/__tests__/validateAuthContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const mockGetAuthenticatedAccountId = vi.mocked(getAuthenticatedAccountId);
const mockValidateOrganizationAccess = vi.mocked(validateOrganizationAccess);
const mockCanAccessAccount = vi.mocked(canAccessAccount);

/**
*
* @param headers
*/
function createMockRequest(headers: Record<string, string> = {}): Request {
return {
headers: {
Expand Down
2 changes: 2 additions & 0 deletions lib/catalog/formatCatalogSongsAsCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { CatalogSong } from "./getCatalogSongs";

/**
* Formats catalog songs into the CSV-like format expected by the scorer
*
* @param songs
*/
export function formatCatalogSongsAsCSV(songs: CatalogSong[]): string {
const csvLines = songs.map(song => {
Expand Down
2 changes: 2 additions & 0 deletions lib/catalog/getCatalogDataAsCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { formatCatalogSongsAsCSV } from "./formatCatalogSongsAsCSV";

/**
* Gets all catalog songs and formats them as CSV for the scorer
*
* @param catalogId
*/
export async function getCatalogDataAsCSV(catalogId: string): Promise<string> {
const allSongs: CatalogSong[] = [];
Expand Down
7 changes: 7 additions & 0 deletions lib/catalog/getCatalogSongs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export interface CatalogSongsResponse {
error?: string;
}

/**
*
* @param catalogId
* @param pageSize
* @param page
* @param artistName
*/
export async function getCatalogSongs(
catalogId: string,
pageSize: number = 100,
Expand Down
4 changes: 4 additions & 0 deletions lib/catalog/getCatalogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export interface CatalogsResponse {
error?: string;
}

/**
*
* @param accountId
*/
export async function getCatalogs(accountId: string): Promise<CatalogsResponse> {
try {
const response = await fetch(
Expand Down
5 changes: 5 additions & 0 deletions lib/chat/__tests__/integration/chatEndToEnd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ const mockDeductCredits = vi.mocked(deductCredits);
const mockGenerateChatTitle = vi.mocked(generateChatTitle);

// Helper to create mock NextRequest
/**
*
* @param body
* @param headers
*/
function createMockRequest(body: unknown, headers: Record<string, string> = {}): Request {
return {
json: () => Promise.resolve(body),
Expand Down
2 changes: 2 additions & 0 deletions lib/chat/toolChains/getPrepareStepResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type PrepareStepOptions = {
/**
* Returns the next tool to run based on timeline progression through tool chains.
* Uses toolCallsContent to track exact execution order and position in sequence.
*
* @param options
*/
const getPrepareStepResult = (options: PrepareStepOptions): PrepareStepResult | undefined => {
const { steps } = options;
Expand Down
Loading
Loading