/**
* CMS Item Management Tools
*
* CRUD operations for managing items in CMS collections via the dashboard API.
*/
import {
apiRequest,
isApiError,
needsAuthentication,
needsAuthError,
resolveProjectId
} from '../lib/api-client';
import { ensureAuthenticated } from '../lib/device-flow';
/**
* CMS Item structure returned from the API
*/
interface CmsItem {
id: string;
name: string;
slug: string | null;
data: Record<string, unknown>;
publishedAt: string | null;
createdAt: string;
updatedAt: string;
order: number;
}
/**
* CMS Collection structure
*/
interface CmsCollection {
id: string;
slug: string;
name: string;
}
/**
* Helper to ensure authentication and resolve project ID
*/
async function prepareRequest(projectId: string): Promise<
| { success: true; tenantId: string }
| { success: false; message: string }
> {
// Check if we need to authenticate
if (await needsAuthentication()) {
const authResult = await ensureAuthenticated();
if (!authResult.authenticated) {
return { success: false, message: authResult.message };
}
}
// Resolve project ID
const resolved = await resolveProjectId(projectId);
if ('error' in resolved) {
return { success: false, message: `# Project Not Found\n\n${resolved.error}` };
}
return { success: true, tenantId: resolved.tenantId };
}
/**
* Helper to get collection ID from slug
* Dashboard API uses collection IDs, not slugs
*/
async function getCollectionId(
tenantId: string,
collectionSlug: string
): Promise<{ collectionId: string } | { error: string }> {
const response = await apiRequest<CmsCollection[]>('/api/collections', { tenantId });
if (isApiError(response)) {
return { error: `Failed to fetch collections: ${response.error}` };
}
const collection = response.data.find(c => c.slug === collectionSlug);
if (!collection) {
const available = response.data.map(c => `- ${c.slug} (${c.name})`).join('\n');
return {
error: `Collection "${collectionSlug}" not found.\n\nAvailable collections:\n${available || 'None'}`
};
}
return { collectionId: collection.id };
}
/**
* Create a new item in a CMS collection
*/
export async function createCmsItem(params: {
projectId: string;
collectionSlug: string;
name: string;
slug?: string;
data: Record<string, unknown>;
publishedAt?: string;
}): Promise<string> {
const prep = await prepareRequest(params.projectId);
if (!prep.success) return prep.message;
// Get collection ID from slug
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
if ('error' in collectionResult) {
return `# Error\n\n${collectionResult.error}`;
}
const response = await apiRequest<CmsItem>(
`/api/collections/${collectionResult.collectionId}/items`,
{
tenantId: prep.tenantId,
method: 'POST',
body: {
name: params.name,
slug: params.slug,
data: params.data,
publishedAt: params.publishedAt || new Date().toISOString(),
},
}
);
if (isApiError(response)) {
if (needsAuthError(response)) {
const authResult = await ensureAuthenticated();
if (!authResult.authenticated) return authResult.message;
// Retry
const retry = await apiRequest<CmsItem>(
`/api/collections/${collectionResult.collectionId}/items`,
{
tenantId: prep.tenantId,
method: 'POST',
body: {
name: params.name,
slug: params.slug,
data: params.data,
publishedAt: params.publishedAt || new Date().toISOString(),
},
}
);
if (isApiError(retry)) {
return `# Error Creating Item\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
}
return formatCreatedItem(retry.data, params.collectionSlug);
}
return `# Error Creating Item\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
}
return formatCreatedItem(response.data, params.collectionSlug);
}
function formatCreatedItem(item: CmsItem, collectionSlug: string): string {
return `# Item Created Successfully
**Collection:** ${collectionSlug}
**Name:** ${item.name}
**Slug:** ${item.slug || 'N/A'}
**ID:** ${item.id}
**Published:** ${item.publishedAt || 'Draft'}
## Item Data
\`\`\`json
${JSON.stringify(item.data, null, 2)}
\`\`\`
`;
}
/**
* List items in a CMS collection
*/
export async function listCmsItems(params: {
projectId: string;
collectionSlug: string;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
}): Promise<string> {
const prep = await prepareRequest(params.projectId);
if (!prep.success) return prep.message;
// Get collection ID from slug
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
if ('error' in collectionResult) {
return `# Error\n\n${collectionResult.error}`;
}
// Build query params
const queryParams = new URLSearchParams();
if (params.limit) queryParams.set('limit', String(params.limit));
if (params.sort) queryParams.set('sort', params.sort);
if (params.order) queryParams.set('order', params.order);
const queryString = queryParams.toString();
const endpoint = `/api/collections/${collectionResult.collectionId}/items${queryString ? '?' + queryString : ''}`;
const response = await apiRequest<CmsItem[]>(
endpoint,
{ tenantId: prep.tenantId, method: 'GET' }
);
if (isApiError(response)) {
if (needsAuthError(response)) {
const authResult = await ensureAuthenticated();
if (!authResult.authenticated) return authResult.message;
const retry = await apiRequest<CmsItem[]>(
endpoint,
{ tenantId: prep.tenantId, method: 'GET' }
);
if (isApiError(retry)) {
return `# Error Listing Items\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
}
return formatItemsList(retry.data, params.collectionSlug);
}
return `# Error Listing Items\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
}
return formatItemsList(response.data, params.collectionSlug);
}
function formatItemsList(items: CmsItem[], collectionSlug: string): string {
if (!items || items.length === 0) {
return `# No Items Found
Collection **${collectionSlug}** is empty.
Use \`create_cms_item\` to add items to this collection.
`;
}
let output = `# Items in ${collectionSlug}
Found ${items.length} item${items.length !== 1 ? 's' : ''}:
| Name | Slug | Published | ID |
|------|------|-----------|-----|
`;
for (const item of items) {
const published = item.publishedAt ? new Date(item.publishedAt).toLocaleDateString() : 'Draft';
output += `| ${item.name} | ${item.slug || '-'} | ${published} | \`${item.id.slice(0, 8)}...\` |\n`;
}
output += `
---
## Item Details
`;
for (const item of items) {
output += `### ${item.name}
- **Slug:** \`${item.slug || 'N/A'}\`
- **ID:** \`${item.id}\`
- **Data:** ${Object.keys(item.data).length} fields
`;
}
return output;
}
/**
* Get a single item by slug
*/
export async function getCmsItem(params: {
projectId: string;
collectionSlug: string;
itemSlug: string;
}): Promise<string> {
const prep = await prepareRequest(params.projectId);
if (!prep.success) return prep.message;
// Get collection ID from slug
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
if ('error' in collectionResult) {
return `# Error\n\n${collectionResult.error}`;
}
// List items and find by slug (dashboard API uses item IDs, not slugs)
const response = await apiRequest<CmsItem[]>(
`/api/collections/${collectionResult.collectionId}/items`,
{ tenantId: prep.tenantId, method: 'GET' }
);
if (isApiError(response)) {
if (needsAuthError(response)) {
const authResult = await ensureAuthenticated();
if (!authResult.authenticated) return authResult.message;
const retry = await apiRequest<CmsItem[]>(
`/api/collections/${collectionResult.collectionId}/items`,
{ tenantId: prep.tenantId, method: 'GET' }
);
if (isApiError(retry)) {
return `# Error Fetching Item\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
}
const item = retry.data.find(i => i.slug === params.itemSlug);
if (!item) {
return `# Item Not Found\n\nNo item with slug "${params.itemSlug}" in collection "${params.collectionSlug}".`;
}
return formatSingleItem(item, params.collectionSlug);
}
return `# Error Fetching Item\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
}
const item = response.data.find(i => i.slug === params.itemSlug);
if (!item) {
return `# Item Not Found\n\nNo item with slug "${params.itemSlug}" in collection "${params.collectionSlug}".`;
}
return formatSingleItem(item, params.collectionSlug);
}
function formatSingleItem(item: CmsItem, collectionSlug: string): string {
return `# ${item.name}
**Collection:** ${collectionSlug}
**Slug:** \`${item.slug || 'N/A'}\`
**ID:** \`${item.id}\`
**Published:** ${item.publishedAt ? new Date(item.publishedAt).toLocaleString() : 'Draft'}
**Created:** ${new Date(item.createdAt).toLocaleString()}
**Updated:** ${new Date(item.updatedAt).toLocaleString()}
## Item Data
\`\`\`json
${JSON.stringify(item.data, null, 2)}
\`\`\`
`;
}
/**
* Update an existing item
*/
export async function updateCmsItem(params: {
projectId: string;
collectionSlug: string;
itemSlug: string;
name?: string;
data?: Record<string, unknown>;
publishedAt?: string | null;
}): Promise<string> {
const prep = await prepareRequest(params.projectId);
if (!prep.success) return prep.message;
// Get collection ID from slug
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
if ('error' in collectionResult) {
return `# Error\n\n${collectionResult.error}`;
}
// Find item ID by slug
const itemsResponse = await apiRequest<CmsItem[]>(
`/api/collections/${collectionResult.collectionId}/items`,
{ tenantId: prep.tenantId, method: 'GET' }
);
if (isApiError(itemsResponse)) {
return `# Error Finding Item\n\n${itemsResponse.error}\n\n**Status:** ${itemsResponse.statusCode}`;
}
const existingItem = itemsResponse.data.find(i => i.slug === params.itemSlug);
if (!existingItem) {
return `# Item Not Found\n\nNo item with slug "${params.itemSlug}" in collection "${params.collectionSlug}".`;
}
// Build the update body with only provided fields
// IMPORTANT: Merge new data with existing data to preserve fields not being updated
const updateBody: Record<string, unknown> = {};
if (params.name !== undefined) updateBody.name = params.name;
if (params.data !== undefined) {
// Merge with existing data - new fields override, existing fields preserved
const existingData = existingItem.data as Record<string, unknown>;
updateBody.data = { ...existingData, ...params.data };
}
if (params.publishedAt !== undefined) updateBody.publishedAt = params.publishedAt;
if (Object.keys(updateBody).length === 0) {
return `# No Updates Provided
You must provide at least one of: \`name\`, \`data\`, or \`publishedAt\` to update.
`;
}
const response = await apiRequest<CmsItem>(
`/api/collections/${collectionResult.collectionId}/items/${existingItem.id}`,
{
tenantId: prep.tenantId,
method: 'PUT',
body: updateBody,
}
);
if (isApiError(response)) {
if (needsAuthError(response)) {
const authResult = await ensureAuthenticated();
if (!authResult.authenticated) return authResult.message;
const retry = await apiRequest<CmsItem>(
`/api/collections/${collectionResult.collectionId}/items/${existingItem.id}`,
{
tenantId: prep.tenantId,
method: 'PUT',
body: updateBody,
}
);
if (isApiError(retry)) {
return `# Error Updating Item\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
}
return formatUpdatedItem(retry.data, params.collectionSlug);
}
return `# Error Updating Item\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
}
return formatUpdatedItem(response.data, params.collectionSlug);
}
function formatUpdatedItem(item: CmsItem, collectionSlug: string): string {
return `# Item Updated Successfully
**Collection:** ${collectionSlug}
**Name:** ${item.name}
**Slug:** ${item.slug || 'N/A'}
**ID:** ${item.id}
**Updated:** ${new Date(item.updatedAt).toLocaleString()}
## Updated Data
\`\`\`json
${JSON.stringify(item.data, null, 2)}
\`\`\`
`;
}
/**
* Delete an item from a collection
* REQUIRES confirmDelete: true to execute
*/
export async function deleteCmsItem(params: {
projectId: string;
collectionSlug: string;
itemSlug: string;
confirmDelete: boolean;
}): Promise<string> {
// Safety check: require explicit confirmation
if (params.confirmDelete !== true) {
return `# Confirmation Required
**You must get explicit permission from the user before deleting items.**
To delete the item "${params.itemSlug}" from collection "${params.collectionSlug}":
1. Ask the user: "Are you sure you want to delete [item name]? This cannot be undone."
2. Only after the user confirms, call this tool again with \`confirmDelete: true\`
**Never delete without user confirmation.**
`;
}
const prep = await prepareRequest(params.projectId);
if (!prep.success) return prep.message;
// Get collection ID from slug
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
if ('error' in collectionResult) {
return `# Error\n\n${collectionResult.error}`;
}
// Find item ID by slug
const itemsResponse = await apiRequest<CmsItem[]>(
`/api/collections/${collectionResult.collectionId}/items`,
{ tenantId: prep.tenantId, method: 'GET' }
);
if (isApiError(itemsResponse)) {
return `# Error Finding Item\n\n${itemsResponse.error}\n\n**Status:** ${itemsResponse.statusCode}`;
}
const existingItem = itemsResponse.data.find(i => i.slug === params.itemSlug);
if (!existingItem) {
return `# Item Not Found\n\nNo item with slug "${params.itemSlug}" in collection "${params.collectionSlug}".`;
}
const response = await apiRequest<{ success: boolean }>(
`/api/collections/${collectionResult.collectionId}/items/${existingItem.id}`,
{ tenantId: prep.tenantId, method: 'DELETE' }
);
if (isApiError(response)) {
if (needsAuthError(response)) {
const authResult = await ensureAuthenticated();
if (!authResult.authenticated) return authResult.message;
const retry = await apiRequest<{ success: boolean }>(
`/api/collections/${collectionResult.collectionId}/items/${existingItem.id}`,
{ tenantId: prep.tenantId, method: 'DELETE' }
);
if (isApiError(retry)) {
return `# Error Deleting Item\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
}
return `# Item Deleted
Successfully deleted item "${params.itemSlug}" from collection "${params.collectionSlug}".
`;
}
return `# Error Deleting Item\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
}
return `# Item Deleted
Successfully deleted item "${params.itemSlug}" from collection "${params.collectionSlug}".
`;
}