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
JavaScript
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) };
}