Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 48 additions & 27 deletions lib/steps/create-ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import "server-only";

import { LinearClient } from "@linear/sdk";
import { fetchCredentials } from "../credential-fetcher";
import { getErrorMessage } from "../utils";

type CreateTicketResult =
| { success: true; id: string; url: string; title: string }
| { success: false; error: string };

export async function createTicketStep(input: {
integrationId?: string;
ticketTitle: string;
ticketDescription: string;
}) {
}): Promise<CreateTicketResult> {
"use step";

const credentials = input.integrationId
Expand All @@ -24,38 +29,54 @@ export async function createTicketStep(input: {
const teamId = credentials.LINEAR_TEAM_ID;

if (!apiKey) {
throw new Error(
"LINEAR_API_KEY is not configured. Please add it in Project Integrations."
);
return {
success: false,
error:
"LINEAR_API_KEY is not configured. Please add it in Project Integrations.",
};
}

const linear = new LinearClient({ apiKey });
try {
const linear = new LinearClient({ apiKey });

let targetTeamId = teamId;
if (!targetTeamId) {
const teams = await linear.teams();
const firstTeam = teams.nodes[0];
if (!firstTeam) {
throw new Error("No teams found in Linear workspace");
let targetTeamId = teamId;
if (!targetTeamId) {
const teams = await linear.teams();
const firstTeam = teams.nodes[0];
if (!firstTeam) {
return {
success: false,
error: "No teams found in Linear workspace",
};
}
targetTeamId = firstTeam.id;
}
targetTeamId = firstTeam.id;
}

const issuePayload = await linear.createIssue({
title: input.ticketTitle,
description: input.ticketDescription,
teamId: targetTeamId,
});
const issuePayload = await linear.createIssue({
title: input.ticketTitle,
description: input.ticketDescription,
teamId: targetTeamId,
});

const issue = await issuePayload.issue;
const issue = await issuePayload.issue;

if (!issue) {
throw new Error("Failed to create issue");
}
if (!issue) {
return {
success: false,
error: "Failed to create issue",
};
}

return {
id: issue.id,
url: issue.url,
title: issue.title,
};
return {
success: true,
id: issue.id,
url: issue.url,
title: issue.title,
};
} catch (error) {
return {
success: false,
error: `Failed to create ticket: ${getErrorMessage(error)}`,
};
}
}
24 changes: 9 additions & 15 deletions lib/steps/database-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ type DatabaseQueryInput = {
query?: string;
};

type DatabaseQueryResult = {
status: string;
rows?: unknown;
count?: number;
error?: string;
};
type DatabaseQueryResult =
| { success: true; rows: unknown; count: number }
| { success: false; error: string };

function validateInput(input: DatabaseQueryInput): string | null {
const queryString = input.dbQuery || input.query;
Expand Down Expand Up @@ -50,7 +47,7 @@ async function executeQuery(
return await db.execute(sql.raw(queryString));
}

function getErrorMessage(error: unknown): string {
function getDatabaseErrorMessage(error: unknown): string {
if (!(error instanceof Error)) {
return "Unknown database error";
}
Expand Down Expand Up @@ -90,10 +87,7 @@ export async function databaseQueryStep(

const validationError = validateInput(input);
if (validationError) {
return {
status: "error",
error: validationError,
};
return { success: false, error: validationError };
}

const credentials = input.integrationId
Expand All @@ -104,7 +98,7 @@ export async function databaseQueryStep(

if (!databaseUrl) {
return {
status: "error",
success: false,
error:
"DATABASE_URL is not configured. Please add it in Project Integrations.",
};
Expand All @@ -119,15 +113,15 @@ export async function databaseQueryStep(
await client.end();

return {
status: "success",
success: true,
rows: result,
count: Array.isArray(result) ? result.length : 0,
};
} catch (error) {
await cleanupClient(client);
return {
status: "error",
error: getErrorMessage(error),
success: false,
error: `Database query failed: ${getDatabaseErrorMessage(error)}`,
};
}
}
70 changes: 45 additions & 25 deletions lib/steps/generate-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
*/
import "server-only";

import { experimental_generateImage as generateImage } from "ai";
import type { ImageModelV2 } from "@ai-sdk/provider";
import { createGateway, experimental_generateImage as generateImage } from "ai";
import { fetchCredentials } from "../credential-fetcher";
import { getErrorMessageAsync } from "../utils";

type GenerateImageResult =
| { success: true; base64: string }
| { success: false; error: string };

export async function generateImageStep(input: {
integrationId?: string;
model: string;
prompt: string;
}): Promise<{ base64: string | undefined }> {
imageModel: ImageModelV2;
imagePrompt: string;
}): Promise<GenerateImageResult> {
"use step";

const credentials = input.integrationId
Expand All @@ -23,29 +29,43 @@ export async function generateImageStep(input: {
const apiKey = credentials.AI_GATEWAY_API_KEY;

if (!apiKey) {
throw new Error(
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations."
);
return {
success: false,
error:
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.",
};
}

const result = await generateImage({
// biome-ignore lint/suspicious/noExplicitAny: model string needs type coercion for ai package
model: input.model as any,
prompt: input.prompt,
size: "1024x1024",
providerOptions: {
openai: {
apiKey,
},
},
});

if (!result.image) {
throw new Error("Failed to generate image");
}
try {
const gateway = createGateway({
apiKey,
});

// biome-ignore lint/suspicious/noExplicitAny: AI gateway model ID is dynamic
const modelId = (input.imageModel ?? "bfl/flux-2-pro") as any;
const result = await generateImage({
model: gateway.imageModel(modelId),
prompt: input.imagePrompt,
size: "1024x1024",
});

// Convert the GeneratedFile to base64 string
const base64 = result.image.toString();
if (!result.image) {
return {
success: false,
error: "Failed to generate image: No image returned",
};
}

return { base64 };
// Convert the GeneratedFile to base64 string
const base64 = result.image.toString();

return { success: true, base64 };
} catch (error) {
// Extract meaningful error message from AI SDK errors
const message = await getErrorMessageAsync(error);
return {
success: false,
error: `Image generation failed: ${message}`,
};
}
}
56 changes: 36 additions & 20 deletions lib/steps/generate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import "server-only";
import { generateObject, generateText } from "ai";
import { z } from "zod";
import { fetchCredentials } from "../credential-fetcher";
import { getErrorMessageAsync } from "../utils";

type SchemaField = {
name: string;
type: string;
};

type GenerateTextResult =
| { success: true; text: string }
| { success: true; object: Record<string, unknown> }
| { success: false; error: string };

/**
* Determines the provider from the model ID
*/
Expand Down Expand Up @@ -56,7 +62,7 @@ export async function generateTextStep(input: {
aiPrompt?: string;
aiFormat?: string;
aiSchema?: string;
}): Promise<{ text: string } | Record<string, unknown>> {
}): Promise<GenerateTextResult> {
"use step";

const credentials = input.integrationId
Expand All @@ -66,23 +72,28 @@ export async function generateTextStep(input: {
const apiKey = credentials.AI_GATEWAY_API_KEY;

if (!apiKey) {
throw new Error(
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations."
);
return {
success: false,
error:
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.",
};
}

const modelId = input.aiModel || "gpt-5";
const promptText = input.aiPrompt || "";

if (!promptText || promptText.trim() === "") {
throw new Error("Prompt is required for text generation");
return {
success: false,
error: "Prompt is required for text generation",
};
}

const providerName = getProviderFromModel(modelId);
const modelString = `${providerName}/${modelId}`;

if (input.aiFormat === "object" && input.aiSchema) {
try {
try {
if (input.aiFormat === "object" && input.aiSchema) {
const schema = JSON.parse(input.aiSchema) as SchemaField[];
const zodSchema = buildZodSchema(schema);

Expand All @@ -95,19 +106,24 @@ export async function generateTextStep(input: {
},
});

return object;
} catch {
// If structured output fails, fall back to text generation
return { success: true, object };
}
}

const { text } = await generateText({
model: modelString,
prompt: promptText,
headers: {
Authorization: `Bearer ${apiKey}`,
},
});

return { text };
const { text } = await generateText({
model: modelString,
prompt: promptText,
headers: {
Authorization: `Bearer ${apiKey}`,
},
});

return { success: true, text };
} catch (error) {
// Extract meaningful error message from AI SDK errors
const message = await getErrorMessageAsync(error);
return {
success: false,
error: `Text generation failed: ${message}`,
};
}
}
Loading