Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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)}`,
};
}
}
18 changes: 5 additions & 13 deletions lib/steps/database-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,7 @@ export async function databaseQueryStep(

const validationError = validateInput(input);
if (validationError) {
return {
status: "error",
error: validationError,
};
throw new Error(validationError);
}

const credentials = input.integrationId
Expand All @@ -103,11 +100,9 @@ export async function databaseQueryStep(
const databaseUrl = credentials.DATABASE_URL;

if (!databaseUrl) {
return {
status: "error",
error:
"DATABASE_URL is not configured. Please add it in Project Integrations.",
};
throw new Error(
"DATABASE_URL is not configured. Please add it in Project Integrations."
);
}

const queryString = (input.dbQuery || input.query) as string;
Expand All @@ -125,9 +120,6 @@ export async function databaseQueryStep(
};
} catch (error) {
await cleanupClient(client);
return {
status: "error",
error: getErrorMessage(error),
};
throw new Error(getErrorMessage(error));
}
}
63 changes: 41 additions & 22 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 type { ImageModelV2 } from "@ai-sdk/provider";
import { 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,42 @@ 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,
try {
const result = await generateImage({
model: input.imageModel ?? "bfl/flux-2-pro",
prompt: input.imagePrompt,
size: "1024x1024",
providerOptions: {
openai: {
apiKey,
},
},
},
});
});

if (!result.image) {
throw new Error("Failed to generate image");
}
if (!result.image) {
return {
success: false,
error: "Failed to generate image: No image returned",
};
}

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

return { base64 };
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}`,
};
}
}
38 changes: 30 additions & 8 deletions lib/steps/http-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,42 @@
*/
import "server-only";

import { getErrorMessage } from "../utils";

type HttpRequestResult =
| { success: true; data: unknown; status: number }
| { success: false; error: string; status?: number };

export async function httpRequestStep(input: {
url: string;
method: string;
headers: Record<string, string>;
body: unknown;
}): Promise<unknown> {
}): Promise<HttpRequestResult> {
"use step";

const response = await fetch(input.url, {
method: input.method,
headers: input.headers,
body: input.body ? JSON.stringify(input.body) : undefined,
});
try {
const response = await fetch(input.url, {
method: input.method,
headers: input.headers,
body: input.body ? JSON.stringify(input.body) : undefined,
});

if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error");
return {
success: false,
error: `HTTP request failed with status ${response.status}: ${errorText}`,
status: response.status,
};
}

const data = await response.json();
return data;
const data = await response.json();
return { success: true, data, status: response.status };
} catch (error) {
return {
success: false,
error: `HTTP request failed: ${getErrorMessage(error)}`,
};
}
}
Loading