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
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"www",
"seo",
"seo-www",
"contact"
"seo-contact"
]
}
4 changes: 2 additions & 2 deletions apps/seo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"cf-typegen": "wrangler types --env-interface Env"
},
"dependencies": {
"@ai-sdk/react": "^2.0.98",
"@ai-sdk/react": "^3.0.88",
"@rectangular-labs/api-seo": "workspace:*",
"@rectangular-labs/api-user-vm": "workspace:*",
"@rectangular-labs/auth": "workspace:*",
Expand All @@ -40,7 +40,7 @@
"@tanstack/react-router-ssr-query": "^1.146.2",
"@tanstack/react-start": "^1.146.2",
"@tanstack/react-table": "^8.21.3",
"ai": "^5.0.112",
"ai": "^6.0.86",
"arktype": "^2.1.29",
"fflate": "^0.8.2",
"idb-keyval": "^6.2.2",
Expand Down
2 changes: 1 addition & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.15/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
"assist": {
"actions": {
"source": { "organizeImports": "on", "useSortedAttributes": "on" }
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"sso": "aws sso login --sso-session=rectangular-labs"
},
"devDependencies": {
"@biomejs/biome": "2.3.15",
"@biomejs/biome": "2.4.0",
"@changesets/cli": "^2.29.7",
"@dotenvx/dotenvx": "^1.51.1",
"@rectangular-labs/typescript": "workspace:*",
Expand Down
8 changes: 4 additions & 4 deletions packages/api-seo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
"typescript": "^5.9.3"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.56",
"@ai-sdk/google": "^2.0.46",
"@ai-sdk/openai": "^2.0.85",
"@ai-sdk/anthropic": "^3.0.44",
"@ai-sdk/google": "^3.0.29",
"@ai-sdk/openai": "^3.0.29",
"@jimp/core": "^1.6.0",
"@jimp/wasm-webp": "^1.6.0",
"@orpc/client": "^1.13.4",
Expand All @@ -71,7 +71,7 @@
"@rectangular-labs/result": "workspace:*",
"@rectangular-labs/task": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"ai": "^5.0.112",
"ai": "^6.0.86",
"arktype": "^2.1.29",
"aws4fetch": "^1.0.20",
"cloudflare": "^5.2.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/api-seo/src/lib/ai/strategist-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Output requirements:
- If proposing edits to existing content, describe them clearly
*/

export function createStrategistAgent({
export async function createStrategistAgent({
messages,
gscProperty,
project,
Expand All @@ -86,7 +86,7 @@ export function createStrategistAgent({
project: NonNullable<ChatContext["cache"]["project"]>;
context: ChatContext;
currentPage: ProjectChatCurrentPage;
}): Parameters<typeof streamText>[0] {
}): Promise<Parameters<typeof streamText>[0]> {
const hasGsc = !!(
gscProperty?.accessToken &&
gscProperty?.config.domain &&
Expand Down Expand Up @@ -201,7 +201,7 @@ ${skillsSection}
} satisfies OpenAIResponsesProviderOptions,
},
system: systemPrompt,
messages: convertToModelMessages(messages),
messages: await convertToModelMessages(messages),
tools: {
...createSkillTools({ toolDefinitions: skillDefinitions }),
...plannerTools.tools,
Expand Down
6 changes: 2 additions & 4 deletions packages/api-seo/src/lib/ai/tools/create-article-tool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { articleTypeSchema } from "@rectangular-labs/core/schemas/content-parsers";
import type { DB, schema } from "@rectangular-labs/db";
import { type JSONSchema7, jsonSchema, tool } from "ai";
import { tool } from "ai";
import { type } from "arktype";
import { normalizeContentSlug } from "../../content/normalize-content-slug";
import { writeContentDraft } from "../../content/write-content-draft";
Expand Down Expand Up @@ -50,9 +50,7 @@ export function createCreateArticleToolWithMetadata({
const createArticle = tool({
description:
"Create a new article draft for a primary keyword, optionally including title, description, outline, article type, and notes. Use this when the user wants to create a new article.",
inputSchema: jsonSchema<typeof createArticleInputSchema.infer>(
createArticleInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: createArticleInputSchema,
async execute({
primaryKeyword,
slug: rawSlug,
Expand Down
6 changes: 2 additions & 4 deletions packages/api-seo/src/lib/ai/tools/data-analysis-agent-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { openai } from "@ai-sdk/openai";
import { NO_SEARCH_CONSOLE_ERROR_MESSAGE } from "@rectangular-labs/core/schemas/gsc-property-parsers";
import type { GscConfig } from "@rectangular-labs/core/schemas/integration-parsers";
import type { schema } from "@rectangular-labs/db";
import { generateText, type JSONSchema7, jsonSchema, tool } from "ai";
import { generateText, tool } from "ai";
import { type } from "arktype";
import type { InitialContext } from "../../../types";
import { createDataforseoToolWithMetadata } from "./dataforseo-tool";
Expand Down Expand Up @@ -104,9 +104,7 @@ ${NO_SEARCH_CONSOLE_ERROR_MESSAGE}`
const seoDataAnalysisAgent = tool({
description:
"Run SEO data analysis using Google Search Console and keyword research data source tools. This agent specializes in analyzing historical performance, CTR optimization, content decay, and keyword opportunities. If Google Search Console is not connected, it will guide you to connect it first.",
inputSchema: jsonSchema<typeof dataAnalysisAgentInputSchema.infer>(
dataAnalysisAgentInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: dataAnalysisAgentInputSchema,
async execute({ question }) {
const result = await generateText({
model: openai("gpt-5.1"),
Expand Down
22 changes: 6 additions & 16 deletions packages/api-seo/src/lib/ai/tools/dataforseo-tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { schema } from "@rectangular-labs/db";
import { type JSONSchema7, jsonSchema, tool } from "ai";
import { tool } from "ai";
import { type } from "arktype";
import type { InitialContext } from "../../../types";
import {
Expand Down Expand Up @@ -117,9 +117,7 @@ export function createDataforseoToolWithMetadata(

const getRankedKeywordsForSite = tool({
description: "Fetch keywords that the site currently ranks for.",
inputSchema: jsonSchema<typeof rankedKeywordsInputSchema.infer>(
rankedKeywordsInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: rankedKeywordsInputSchema,
async execute({
hostname,
positionFrom,
Expand Down Expand Up @@ -189,9 +187,7 @@ export function createDataforseoToolWithMetadata(
// Ranked Pages tool
const getRankedPagesForSite = tool({
description: "Fetch pages of the site that are currently ranked.",
inputSchema: jsonSchema<typeof rankedPagesInputSchema.infer>(
rankedPagesInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: rankedPagesInputSchema,
async execute({
hostname,
limit,
Expand Down Expand Up @@ -229,9 +225,7 @@ export function createDataforseoToolWithMetadata(
// Keyword Suggestions tool
const getKeywordSuggestions = tool({
description: "Generate keyword suggestions based of a seed keyword.",
inputSchema: jsonSchema<typeof keywordSuggestionsInputSchema.infer>(
keywordSuggestionsInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: keywordSuggestionsInputSchema,
async execute({
seedKeyword,
limit,
Expand Down Expand Up @@ -293,9 +287,7 @@ export function createDataforseoToolWithMetadata(
// Keywords Overview tool
const getKeywordOverview = tool({
description: "Fetch data for a list of keywords.",
inputSchema: jsonSchema<typeof keywordsOverviewInputSchema.infer>(
keywordsOverviewInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: keywordsOverviewInputSchema,
async execute({ keywords, includeGenderAndAgeDistribution }) {
console.log("fetchKeywordsOverview", {
keywords,
Expand Down Expand Up @@ -345,9 +337,7 @@ export function createDataforseoToolWithMetadata(
// SERP (Advanced) tool
const getSerpForKeyword = tool({
description: "Fetch search engine results page (SERP) for a keyword.",
inputSchema: jsonSchema<typeof serpInputSchema.infer>(
serpInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: serpInputSchema,
async execute({ keyword, depth, device, os }) {
console.log("fetchSerp", { keyword, depth, device, os });
const result = await fetchSerpWithCache({
Expand Down
22 changes: 6 additions & 16 deletions packages/api-seo/src/lib/ai/tools/file-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
contentStatusSchema,
} from "@rectangular-labs/core/schemas/content-parsers";
import type { DB } from "@rectangular-labs/db";
import { type JSONSchema7, jsonSchema, tool } from "ai";
import { tool } from "ai";
import { type } from "arktype";
import {
deleteDraftForSlug,
Expand Down Expand Up @@ -78,9 +78,7 @@ export function createFileToolsWithMetadata(args: {
const ls = tool({
description:
"List files and directories in the virtual workspace filesystem, similar to `ls`.",
inputSchema: jsonSchema<typeof lsInputSchema.infer>(
lsInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: lsInputSchema,
async execute({ slug }) {
return await listContentTree({
db: args.db,
Expand All @@ -95,9 +93,7 @@ export function createFileToolsWithMetadata(args: {
const cat = tool({
description:
"Read the contents of a file in the virtual workspace filesystem, similar to `cat` (supports line ranges).",
inputSchema: jsonSchema<typeof catInputSchema.infer>(
catInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: catInputSchema,
async execute({ slug, startLine, endLine }) {
const result = await getContentForSlug({
db: args.db,
Expand Down Expand Up @@ -129,9 +125,7 @@ export function createFileToolsWithMetadata(args: {
const rm = tool({
description:
"Delete a file in the virtual workspace filesystem, similar to `rm`.",
inputSchema: jsonSchema<typeof rmInputSchema.infer>(
rmInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: rmInputSchema,
async execute({ slug }) {
const result = await deleteDraftForSlug({
db: args.db,
Expand All @@ -149,9 +143,7 @@ export function createFileToolsWithMetadata(args: {
// const mv = tool({
// description:
// "Move or rename a file or directory in the virtual workspace filesystem, similar to `mv`.",
// inputSchema: jsonSchema<typeof mvInputSchema.infer>(
// mvInputSchema.toJsonSchema() as JSONSchema7,
// ),
// inputSchema: mvInputSchema,
// async execute({ fromSlug, toSlug }) {
// const from = normalizeContentSlug(fromSlug);
// const to = normalizeContentSlug(toSlug);
Expand All @@ -173,9 +165,7 @@ export function createFileToolsWithMetadata(args: {
const writeFile = tool({
description:
"Create or update a file in the virtual workspace filesystem by slug (content, title, description, notes, outline, etc.).",
inputSchema: jsonSchema<typeof writeFileInputSchema.infer>(
writeFileInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: writeFileInputSchema,
async execute({
id,
slug,
Expand Down
30 changes: 9 additions & 21 deletions packages/api-seo/src/lib/ai/tools/image-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import { type GoogleGenerativeAIProviderOptions, google } from "@ai-sdk/google";
import { getExtensionFromMimeType } from "@rectangular-labs/core/project/get-extension-from-mimetype";
import type { imageSettingsSchema } from "@rectangular-labs/core/schemas/project-parsers";
import { uuidv7 } from "@rectangular-labs/db";
import {
generateObject,
generateText,
type JSONSchema7,
jsonSchema,
tool,
} from "ai";
import { generateText, Output, tool } from "ai";
import { type } from "arktype";
import { apiEnv } from "../../../env";
import type { InitialContext } from "../../../types";
Expand Down Expand Up @@ -56,11 +50,11 @@ async function selectBestStockImageIndex(args: {
candidates: StockImageCandidate[];
}): Promise<number> {
const selectionSchema = type({ index: "number.integer >= -1" });
const { object } = await generateObject({
const { output } = await generateText({
model: google("gemini-3-flash-preview"),
schema: jsonSchema<typeof selectionSchema.infer>(
selectionSchema.toJsonSchema() as JSONSchema7,
),
output: Output.object({
schema: selectionSchema,
}),
system:
"You pick the best matching image for a query. Return JSON only with an integer `index` corresponding to the index of the best matching image in the list of candidates. Return -1 if none of the pictures match the query well.",
messages: [
Expand All @@ -81,7 +75,7 @@ Attached are ${args.candidates.length} images. Return {"index": N} where N is -1
],
});

const index = typeof object.index === "number" ? object.index : -1;
const index = output.index;
if (index < -1 || index >= args.candidates.length) return -1;
return index;
}
Expand All @@ -94,9 +88,7 @@ export function createImageToolsWithMetadata(args: {
}) {
const generateImage = tool({
description: "Generate an image based on a prompt.",
inputSchema: jsonSchema<typeof imageAgentInputSchema.infer>(
imageAgentInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: imageAgentInputSchema,
execute: async ({ prompt }) => {
const { imageSettings } = args;
if (!imageSettings) {
Expand Down Expand Up @@ -198,9 +190,7 @@ export function createImageToolsWithMetadata(args: {
const findStockImage = tool({
description:
"Find a royalty-free stock image based on a search query. Returns the best match with attribution details. You must put the attribution as the image caption.",
inputSchema: jsonSchema<typeof stockImageInputSchema.infer>(
stockImageInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: stockImageInputSchema,
execute: async ({ searchQuery, orientation }) => {
const providers: (typeof imageSettingsSchema.infer)["stockImageProviders"] =
args.imageSettings?.stockImageProviders ?? [
Expand Down Expand Up @@ -292,9 +282,7 @@ export function createImageToolsWithMetadata(args: {
const captureScreenshotTool = tool({
description:
"Capture a rendered screenshot of a given website URL using ScreenshotOne. Blocks ads, cookie banners, and common overlays. Stores the screenshot in the public bucket (optionally optimized to WebP).",
inputSchema: jsonSchema<typeof screenshotInputSchema.infer>(
screenshotInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: screenshotInputSchema,
async execute({ url, viewportWidth, viewportHeight, fullPage }) {
const result = await captureScreenshotOne({
url,
Expand Down
6 changes: 2 additions & 4 deletions packages/api-seo/src/lib/ai/tools/internal-links-tool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { COUNTRY_CODE_MAP } from "@rectangular-labs/core/schemas/project-parsers";
import { fetchSerp } from "@rectangular-labs/dataforseo";
import { type JSONSchema7, jsonSchema, tool } from "ai";
import { tool } from "ai";
import { type } from "arktype";
import { configureDataForSeoClient } from "../../dataforseo/utils";
import type { AgentToolDefinition } from "./utils";
Expand All @@ -23,9 +23,7 @@ export function createInternalLinksToolWithMetadata(targetUrl: string) {
const internalLinks = tool({
description:
"Query SERP results for a query, constrained to a target URL, and return the organic results.",
inputSchema: jsonSchema<typeof internalLinksInputSchema.infer>(
internalLinksInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: internalLinksInputSchema,
execute: async (input: InternalLinksInput) => {
configureDataForSeoClient();

Expand Down
10 changes: 3 additions & 7 deletions packages/api-seo/src/lib/ai/tools/planner-tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type JSONSchema7, jsonSchema, tool } from "ai";
import { tool } from "ai";
import { type } from "arktype";
import type { AgentToolDefinition } from "./utils";

Expand Down Expand Up @@ -35,9 +35,7 @@ export function createPlannerToolsWithMetadata() {
const askQuestions = tool({
description:
'Ask the user clarification questions to help provide clarity to the request. Returns immediately while waiting for the user\'s response. By default the "other" option is always added to each question so you can omit that option from the options array.',
inputSchema: jsonSchema<typeof askQuestionInputSchema.infer>(
askQuestionInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: askQuestionInputSchema,
async execute() {
return await Promise.resolve({
success: true,
Expand All @@ -49,9 +47,7 @@ export function createPlannerToolsWithMetadata() {
const createPlan = tool({
description:
"Create or update a plan artifact for the SEO/GEO task (overview + steps/todos).",
inputSchema: jsonSchema<typeof createPlanInputSchema.infer>(
createPlanInputSchema.toJsonSchema() as JSONSchema7,
),
inputSchema: createPlanInputSchema,
async execute() {
return await Promise.resolve({
success: true,
Expand Down
Loading
Loading