feat: add 100+ official TPMJS tools

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>
This commit is contained in:
Ajax Davis 2025-12-31 22:55:56 +10:00
parent 2d9b06020d
commit 5d2096fb5d
499 changed files with 50782 additions and 238 deletions

View file

@ -0,0 +1,366 @@
/**
* 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;