shapelessai-ai-sdk
Version:
ShapelessAI tools for the Vercel AI SDK: list connected social accounts, schedule or publish posts, read post status.
84 lines (83 loc) • 4.84 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/docs/api).');
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/docs/api.');
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, publishedAt: p.publishedAt, url: p.publishedLocation, error: p.error,
});
/** 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) };
}