UNPKG

shapelessai-ai-sdk

Version:

ShapelessAI tools for the Vercel AI SDK: list connected social accounts, schedule or publish posts, read post status.

91 lines (90 loc) • 4.92 kB
import { tool } from "ai"; import { z } from "zod"; function client(opts = {}) { const base = (opts.baseURL ?? "https://shapelessai.com").replace(/\/$/, ""); return async function call(path, init = {}) { const key = opts.apiKey ?? process.env.SHAPELESSAI_API_KEY; if (!key) throw new Error("Set SHAPELESSAI_API_KEY (make one at https://shapelessai.com/studio/api-keys)."); const res = await fetch(`${base}${path}`, { ...init, headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...init.headers }, }); if (res.status === 401) throw new Error("ShapelessAI refused the API key. Make a new one at https://shapelessai.com/studio/api-keys."); const body = (await res.json().catch(() => ({}))); if (!res.ok) throw new Error(body.error || `ShapelessAI answered ${res.status}`); return body; }; } const slim = (p) => ({ id: p.id, connectionId: p.connectionId, platform: p.platform, status: p.status, text: p.postText, scheduledAt: p.scheduledAt ?? null, publishedAt: p.publishedAt ?? null, url: p.publishedLocation ?? null, error: p.error ?? null, }); /** The social accounts the key's owner connected, the ones a post can go to. */ export function listAccounts(opts = {}) { const call = client(opts); return tool({ description: "List the social media accounts connected to ShapelessAI that can be posted to (X, LinkedIn, Instagram, TikTok, YouTube, Threads, Bluesky, Facebook and more). Call this first to get a connectionId.", inputSchema: z.object({ platform: z.string().optional().describe('Only accounts on this platform, e.g. "linkedin".') }), execute: async ({ platform }) => { const { connections } = await call("/api/connections"); return connections .filter((c) => c.mode === "oauth" && c.status === "active" && (!platform || c.platform === platform.toLowerCase())) .map((c) => ({ connectionId: c.id, platform: c.platform, handle: c.handle, name: c.displayName ?? c.handle })); }, }); } /** Schedule a post to the account's next free queue slot, at a set time, or now. */ export function createPost(opts = {}) { const call = client(opts); return tool({ description: "Create a social media post on one connected account through ShapelessAI. With no time it goes to the account's next free queue slot; set scheduledAt for a time, or publishNow only when the user asked to post right now.", inputSchema: z.object({ connectionId: z.string().describe("From listAccounts."), text: z.string().min(1).describe("The post text, exactly as it should appear."), scheduledAt: z.string().optional().describe("ISO 8601 time to publish at."), publishNow: z.boolean().optional().describe("True only when the user asked to post right now."), }), needsApproval: opts.needsApproval ?? true, execute: async ({ connectionId, text, scheduledAt, publishNow }) => { const body = { connectionId, postText: text }; // No time and no "now" means the next queue slot, so nothing posts at an odd hour by accident. if (scheduledAt) body.scheduledAt = scheduledAt; else if (!publishNow) body.queue = true; // The answer is { id, status, scheduledAt } when queued, { id, status, publishedLocation, error } when posted now. const res = await call("/api/posts", { method: "POST", body: JSON.stringify(body) }); return { id: res.id, status: res.status, scheduledAt: res.scheduledAt ?? null, url: res.publishedLocation ?? null, error: res.error ?? null }; }, }); } /** Recent posts and their status: scheduled, published (with its URL) or failed (with the reason). */ export function listPosts(opts = {}) { const call = client(opts); return tool({ description: "List recent ShapelessAI posts with their status, scheduled time, live URL once published, and the error if one failed.", inputSchema: z.object({ status: z.enum(["proposed", "scheduled", "publishing", "published", "handoff", "failed", "canceled"]).optional(), limit: z.number().int().min(1).max(50).optional(), }), execute: async ({ status, limit }) => { const q = new URLSearchParams({ limit: String(limit ?? 20), ...(status ? { status } : {}) }); const { posts } = await call(`/api/posts?${q}`); return posts.map(slim); }, }); } /** All three tools, ready to spread into generateText({ tools }). */ export function shapelessaiTools(opts = {}) { return { listAccounts: listAccounts(opts), createPost: createPost(opts), listPosts: listPosts(opts) }; }