Implements a comprehensive suite of AI SDK v6 tools across multiple categories: - Research (5): page-brief, compare-pages, source-credibility, claim-checklist, timeline-from-text - Web (10): fetch-text, links-catalog, extract-meta, extract-json-ld, redirect-trace, sitemap-read, rss-read, table-extract, robots-policy, url-normalize - Data (15): csv-parse, csv-stringify, json-repair, json-schema-validate, yaml-parse, yaml-stringify, text-chunk, normalize-whitespace, dedupe-by-key, pivot, rows-filter, rows-sort, rows-group-aggregate, rows-join, schema-infer - Doc (12): toc-generate, glossary-build, faq-from-text, executive-brief, decision-record-adr, prd-outline, acceptance-criteria, style-rewrite - Eng (12): diff-text-unified, env-var-docs-generate, dependency-audit-lite, conventional-commit-suggest, markdown-lint-basic, test-case-generate, stacktrace-parse, release-notes, changelog-entry, release-checklist - Security (7): redact-secrets, secret-scan-text, url-risk-heuristic, csp-compose, hardening-checklist-web, access-control-matrix, data-classification-heuristic - Stats (9): effect-size-suite, bootstrap-ci, permutation-test, multiple-testing-adjust, linear-regression-ols, logistic-regression, time-series-decompose-lite, anomaly-detect-mad - Ops (7): slo-draft, runbook-draft, postmortem-draft, postmortem-action-extractor, error-log-triage, coverage-tracker, monitoring-gap-analysis - Agent (15): prompt-to-workflow-skeleton, workflow-validate-io, workflow-explain, workflow-cost-estimate, tool-call-accuracy-score, eval-fixture-build, guardrail-policy-draft, workflow-auto-repair, tool-selection-plan, novelty-score-workflow, workflow-variant-generate, config-normalize, recipe-* - Utility (8): base64-encode, base64-decode, hash-text, regex-extract, template-render, date-parse, json-path-query, url-parse - HTML (3): html-sanitize, html-to-markdown, markdown-to-html All tools follow AI SDK v6 pattern with tool() and jsonSchema<T>(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
366 lines
8.8 KiB
TypeScript
366 lines
8.8 KiB
TypeScript
/**
|
|
* Markdown Lint Basic Tool for TPMJS
|
|
* Basic markdown linting to check for common issues
|
|
*
|
|
* @requires ai@6.x (Vercel AI SDK)
|
|
*/
|
|
|
|
import { jsonSchema, tool } from 'ai';
|
|
|
|
/**
|
|
* Represents a linting issue found in the markdown
|
|
*/
|
|
export interface LintIssue {
|
|
rule: string;
|
|
severity: 'error' | 'warning';
|
|
line: number;
|
|
column?: number;
|
|
message: string;
|
|
context?: string;
|
|
}
|
|
|
|
/**
|
|
* Output interface for the lint result
|
|
*/
|
|
export interface LintResult {
|
|
issues: LintIssue[];
|
|
passed: boolean;
|
|
summary: {
|
|
errors: number;
|
|
warnings: number;
|
|
totalLines: number;
|
|
rulesChecked: string[];
|
|
};
|
|
}
|
|
|
|
type MarkdownLintInput = {
|
|
markdown: string;
|
|
};
|
|
|
|
/**
|
|
* Check for trailing whitespace
|
|
*/
|
|
function checkTrailingWhitespace(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
if (line.length > 0 && /\s+$/.test(line)) {
|
|
issues.push({
|
|
rule: 'no-trailing-spaces',
|
|
severity: 'warning',
|
|
line: i + 1,
|
|
message: 'Line has trailing whitespace',
|
|
context: line.substring(Math.max(0, line.length - 20)),
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check for multiple consecutive blank lines
|
|
*/
|
|
function checkMultipleBlankLines(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
let blankCount = 0;
|
|
let blankStartLine = -1;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line || line.trim() === '') {
|
|
if (blankCount === 0) {
|
|
blankStartLine = i + 1;
|
|
}
|
|
blankCount++;
|
|
} else {
|
|
if (blankCount > 2) {
|
|
issues.push({
|
|
rule: 'no-multiple-blanks',
|
|
severity: 'warning',
|
|
line: blankStartLine,
|
|
message: `Found ${blankCount} consecutive blank lines (max: 2)`,
|
|
});
|
|
}
|
|
blankCount = 0;
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check heading hierarchy (no skipping levels)
|
|
*/
|
|
function checkHeadingHierarchy(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
let previousLevel = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
const match = line.match(/^(#{1,6})\s+(.+)/);
|
|
|
|
if (match?.[1]) {
|
|
const level = match[1].length;
|
|
|
|
// Check if we skipped a level
|
|
if (previousLevel > 0 && level > previousLevel + 1) {
|
|
issues.push({
|
|
rule: 'heading-increment',
|
|
severity: 'error',
|
|
line: i + 1,
|
|
message: `Heading level ${level} skips level ${previousLevel + 1}`,
|
|
context: line,
|
|
});
|
|
}
|
|
|
|
previousLevel = level;
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check for missing blank lines around headings
|
|
*/
|
|
function checkBlankLinesAroundHeadings(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
const isHeading = /^#{1,6}\s+/.test(line);
|
|
|
|
if (isHeading) {
|
|
// Check for blank line before heading (except first line)
|
|
const prevLine = lines[i - 1];
|
|
if (i > 0 && prevLine && prevLine.trim() !== '') {
|
|
issues.push({
|
|
rule: 'blanks-around-headings',
|
|
severity: 'warning',
|
|
line: i + 1,
|
|
message: 'Heading should have a blank line before it',
|
|
context: line,
|
|
});
|
|
}
|
|
|
|
// Check for blank line after heading (except last line)
|
|
const nextLine = lines[i + 1];
|
|
if (i < lines.length - 1 && nextLine && nextLine.trim() !== '') {
|
|
// Allow list items immediately after headings
|
|
if (!/^[-*+]\s/.test(nextLine) && !/^\d+\.\s/.test(nextLine)) {
|
|
issues.push({
|
|
rule: 'blanks-around-headings',
|
|
severity: 'warning',
|
|
line: i + 1,
|
|
message: 'Heading should have a blank line after it',
|
|
context: line,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check for inconsistent list marker style
|
|
*/
|
|
function checkListMarkerStyle(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
const unorderedMarkers = new Set<string>();
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
const match = line.match(/^(\s*)([-*+])\s/);
|
|
|
|
if (match?.[2]) {
|
|
const marker = match[2];
|
|
unorderedMarkers.add(marker);
|
|
|
|
if (unorderedMarkers.size > 1) {
|
|
issues.push({
|
|
rule: 'list-marker-style',
|
|
severity: 'warning',
|
|
line: i + 1,
|
|
message: `Inconsistent list markers found. Use one style: ${Array.from(unorderedMarkers).join(', ')}`,
|
|
context: line,
|
|
});
|
|
break; // Only report once
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check for hard tabs (should use spaces)
|
|
*/
|
|
function checkHardTabs(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line?.includes('\t')) {
|
|
issues.push({
|
|
rule: 'no-hard-tabs',
|
|
severity: 'error',
|
|
line: i + 1,
|
|
message: 'Hard tabs found. Use spaces for indentation.',
|
|
context: line.substring(0, 40),
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check for missing language in fenced code blocks
|
|
*/
|
|
function checkCodeBlockLanguage(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
// Check for opening fence without language
|
|
if (/^```\s*$/.test(line)) {
|
|
issues.push({
|
|
rule: 'fenced-code-language',
|
|
severity: 'warning',
|
|
line: i + 1,
|
|
message: 'Code block should specify a language',
|
|
context: line,
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check for proper link formatting
|
|
*/
|
|
function checkLinkFormatting(lines: string[]): LintIssue[] {
|
|
const issues: LintIssue[] = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
|
|
// Check for bare URLs (not in links or code)
|
|
const bareUrlMatch = line.match(/(?<![\(\[`])(https?:\/\/[^\s\)]+)(?![\)\]`])/);
|
|
if (bareUrlMatch?.[1]) {
|
|
const url = bareUrlMatch[1];
|
|
// Make sure it's not in a code block or already a link
|
|
if (!line.includes(`](${url})`) && !line.includes('`')) {
|
|
issues.push({
|
|
rule: 'no-bare-urls',
|
|
severity: 'warning',
|
|
line: i + 1,
|
|
message: 'URLs should be formatted as markdown links',
|
|
context: line,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for empty links
|
|
if (/\[\]\(/.test(line)) {
|
|
issues.push({
|
|
rule: 'no-empty-links',
|
|
severity: 'error',
|
|
line: i + 1,
|
|
message: 'Link text cannot be empty',
|
|
context: line,
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Markdown Lint Basic Tool
|
|
* Checks markdown for common formatting and style issues
|
|
*/
|
|
export const markdownLintBasicTool = tool({
|
|
description:
|
|
'Perform basic markdown linting to check for common issues like trailing whitespace, multiple blank lines, heading hierarchy problems, and formatting inconsistencies. Returns a list of issues with severity levels.',
|
|
inputSchema: jsonSchema<MarkdownLintInput>({
|
|
type: 'object',
|
|
properties: {
|
|
markdown: {
|
|
type: 'string',
|
|
description: 'The markdown content to lint',
|
|
},
|
|
},
|
|
required: ['markdown'],
|
|
additionalProperties: false,
|
|
}),
|
|
async execute({ markdown }): Promise<LintResult> {
|
|
// Validate input
|
|
if (!markdown || typeof markdown !== 'string') {
|
|
throw new Error('Markdown content is required and must be a string');
|
|
}
|
|
|
|
const lines = markdown.split('\n');
|
|
const allIssues: LintIssue[] = [];
|
|
|
|
// Run all lint checks
|
|
const checks = [
|
|
checkTrailingWhitespace,
|
|
checkMultipleBlankLines,
|
|
checkHeadingHierarchy,
|
|
checkBlankLinesAroundHeadings,
|
|
checkListMarkerStyle,
|
|
checkHardTabs,
|
|
checkCodeBlockLanguage,
|
|
checkLinkFormatting,
|
|
];
|
|
|
|
for (const check of checks) {
|
|
allIssues.push(...check(lines));
|
|
}
|
|
|
|
// Sort issues by line number
|
|
allIssues.sort((a, b) => a.line - b.line);
|
|
|
|
// Count errors and warnings
|
|
const errors = allIssues.filter((i) => i.severity === 'error').length;
|
|
const warnings = allIssues.filter((i) => i.severity === 'warning').length;
|
|
|
|
// Extract rule names
|
|
const rulesChecked = [
|
|
'no-trailing-spaces',
|
|
'no-multiple-blanks',
|
|
'heading-increment',
|
|
'blanks-around-headings',
|
|
'list-marker-style',
|
|
'no-hard-tabs',
|
|
'fenced-code-language',
|
|
'no-bare-urls',
|
|
'no-empty-links',
|
|
];
|
|
|
|
return {
|
|
issues: allIssues,
|
|
passed: errors === 0,
|
|
summary: {
|
|
errors,
|
|
warnings,
|
|
totalLines: lines.length,
|
|
rulesChecked,
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
export default markdownLintBasicTool;
|