Add test tool package to verify sync system can discover and process tools: Package Features: - Creates structured blog posts with frontmatter and metadata - Supports both Markdown and MDX output formats - Automatic slug generation from title - Word count and reading time calculation - SEO-friendly metadata generation TPMJS Integration: - Rich-tier tpmjs field with all optional metadata - Tagged with 'tpmjs-tool' keyword for NPM discovery - Complete parameter and return type documentation - AI agent guidance for optimal LLM usage - Pricing, authentication, and framework metadata Implementation: - Full TypeScript implementation with exported types - Proper tsconfig and tsup build configuration - Comprehensive README with usage examples - Changeset for publishing workflow - Added packages/tools/* to pnpm workspace This package serves as an end-to-end test of the sync workers to verify: 1. NPM keyword search discovers the package 2. Changes feed picks up updates 3. tpmjs field validation works correctly 4. Rich-tier metadata is properly extracted 5. Quality score calculation functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
147 lines
3.1 KiB
TypeScript
147 lines
3.1 KiB
TypeScript
/**
|
|
* Blog Post Creation Tool for TPMJS
|
|
* Creates structured blog posts with frontmatter and metadata
|
|
*/
|
|
|
|
export interface BlogPostOptions {
|
|
title: string;
|
|
author: string;
|
|
content: string;
|
|
tags?: string[];
|
|
format?: 'markdown' | 'mdx';
|
|
excerpt?: string;
|
|
publishDate?: Date;
|
|
}
|
|
|
|
export interface BlogPost {
|
|
frontmatter: {
|
|
title: string;
|
|
author: string;
|
|
date: string;
|
|
tags: string[];
|
|
excerpt?: string;
|
|
slug: string;
|
|
wordCount: number;
|
|
readingTime: number;
|
|
};
|
|
content: string;
|
|
formattedOutput: string;
|
|
}
|
|
|
|
/**
|
|
* Creates a slug from a title
|
|
*/
|
|
function createSlug(title: string): string {
|
|
return title
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^\w\s-]/g, '')
|
|
.replace(/[\s_-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
/**
|
|
* Calculates reading time based on word count
|
|
* Assumes average reading speed of 200 words per minute
|
|
*/
|
|
function calculateReadingTime(wordCount: number): number {
|
|
return Math.ceil(wordCount / 200);
|
|
}
|
|
|
|
/**
|
|
* Counts words in content
|
|
*/
|
|
function countWords(content: string): number {
|
|
return content.trim().split(/\s+/).length;
|
|
}
|
|
|
|
/**
|
|
* Formats frontmatter as YAML
|
|
*/
|
|
function formatFrontmatter(
|
|
frontmatter: BlogPost['frontmatter'],
|
|
format: 'markdown' | 'mdx'
|
|
): string {
|
|
const delimiter = format === 'mdx' ? '---' : '---';
|
|
const lines = [
|
|
delimiter,
|
|
`title: "${frontmatter.title}"`,
|
|
`author: ${frontmatter.author}`,
|
|
`date: ${frontmatter.date}`,
|
|
`slug: ${frontmatter.slug}`,
|
|
`tags: [${frontmatter.tags.map((tag) => `"${tag}"`).join(', ')}]`,
|
|
];
|
|
|
|
if (frontmatter.excerpt) {
|
|
lines.push(`excerpt: "${frontmatter.excerpt}"`);
|
|
}
|
|
|
|
lines.push(`wordCount: ${frontmatter.wordCount}`);
|
|
lines.push(`readingTime: ${frontmatter.readingTime}`);
|
|
lines.push(delimiter);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Creates a structured blog post with frontmatter and metadata
|
|
*/
|
|
export async function createBlogPost(options: BlogPostOptions): Promise<BlogPost> {
|
|
const {
|
|
title,
|
|
author,
|
|
content,
|
|
tags = [],
|
|
format = 'markdown',
|
|
excerpt,
|
|
publishDate = new Date(),
|
|
} = options;
|
|
|
|
// Validate required fields
|
|
if (!title || title.trim().length === 0) {
|
|
throw new Error('Title is required');
|
|
}
|
|
|
|
if (!author || author.trim().length === 0) {
|
|
throw new Error('Author is required');
|
|
}
|
|
|
|
if (!content || content.trim().length === 0) {
|
|
throw new Error('Content is required');
|
|
}
|
|
|
|
// Calculate metadata
|
|
const slug = createSlug(title);
|
|
const wordCount = countWords(content);
|
|
const readingTime = calculateReadingTime(wordCount);
|
|
|
|
// Build frontmatter
|
|
const frontmatter: BlogPost['frontmatter'] = {
|
|
title,
|
|
author,
|
|
date: publishDate.toISOString().split('T')[0] || '',
|
|
tags,
|
|
slug,
|
|
wordCount,
|
|
readingTime,
|
|
};
|
|
|
|
if (excerpt) {
|
|
frontmatter.excerpt = excerpt;
|
|
}
|
|
|
|
// Format the complete blog post
|
|
const formattedFrontmatter = formatFrontmatter(frontmatter, format);
|
|
const formattedOutput = `${formattedFrontmatter}\n\n${content}`;
|
|
|
|
return {
|
|
frontmatter,
|
|
content,
|
|
formattedOutput,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export default for convenience
|
|
*/
|
|
export default createBlogPost;
|