fix contrib system

This commit is contained in:
overcuriousity 2025-07-23 21:41:21 +02:00
parent 3d42fcef79
commit 3c55742dfa
3 changed files with 350 additions and 258 deletions

View File

@ -37,7 +37,7 @@ const ContributionRequestSchema = z.object({
}), }),
tool: ContributionToolSchema, tool: ContributionToolSchema,
metadata: z.object({ metadata: z.object({
reason: z.string().max(500, 'Reason too long').optional() reason: z.string().transform(val => val.trim() === '' ? undefined : val).optional()
}).optional().default({}) }).optional().default({})
}); });

View File

@ -412,6 +412,8 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Contribute New Tool';
domainAgnosticSoftware, domainAgnosticSoftware,
existingConcepts: existingTools.filter(t => t.type === 'concept') existingConcepts: existingTools.filter(t => t.type === 'concept')
}}> }}>
// REPLACE the JavaScript section at the bottom of tool.astro with this:
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contribution-form'); const form = document.getElementById('contribution-form');
const typeSelect = document.getElementById('tool-type'); const typeSelect = document.getElementById('tool-type');
@ -447,23 +449,28 @@ document.addEventListener('DOMContentLoaded', () => {
const descriptionCount = document.getElementById('description-count'); const descriptionCount = document.getElementById('description-count');
const reasonCount = document.getElementById('reason-count'); const reasonCount = document.getElementById('reason-count');
if (descriptionTextarea && descriptionCount) {
descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000)); descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000));
reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500));
// Initial counter update
updateCharacterCounter(descriptionTextarea, descriptionCount, 1000); updateCharacterCounter(descriptionTextarea, descriptionCount, 1000);
}
if (reasonTextarea && reasonCount) {
reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500));
updateCharacterCounter(reasonTextarea, reasonCount, 500); updateCharacterCounter(reasonTextarea, reasonCount, 500);
}
// Handle type-specific field visibility // Handle type-specific field visibility
function updateFieldVisibility() { function updateFieldVisibility() {
const selectedType = typeSelect.value; const selectedType = typeSelect.value;
// Hide all type-specific fields // Hide all type-specific fields
softwareFields.style.display = 'none'; if (softwareFields) softwareFields.style.display = 'none';
relatedConceptsField.style.display = 'none'; if (relatedConceptsField) relatedConceptsField.style.display = 'none';
// Show project URL for software only // Show project URL for software only
if (projectUrlField) {
projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none'; projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none';
}
// Handle required fields // Handle required fields
const platformsCheckboxes = document.querySelectorAll('input[name="platforms"]'); const platformsCheckboxes = document.querySelectorAll('input[name="platforms"]');
@ -471,34 +478,38 @@ document.addEventListener('DOMContentLoaded', () => {
if (selectedType === 'software') { if (selectedType === 'software') {
// Show software-specific fields // Show software-specific fields
softwareFields.style.display = 'block'; if (softwareFields) softwareFields.style.display = 'block';
relatedConceptsField.style.display = 'block'; if (relatedConceptsField) relatedConceptsField.style.display = 'block';
// Make platforms and license required // Make platforms and license required
platformsRequired.style.display = 'inline'; if (platformsRequired) platformsRequired.style.display = 'inline';
licenseRequired.style.display = 'inline'; if (licenseRequired) licenseRequired.style.display = 'inline';
platformsCheckboxes.forEach(cb => cb.setAttribute('required', 'required')); platformsCheckboxes.forEach(cb => cb.setAttribute('required', 'required'));
licenseSelect.setAttribute('required', 'required'); if (licenseSelect) licenseSelect.setAttribute('required', 'required');
} else { } else {
// Hide required indicators and remove requirements // Hide required indicators and remove requirements
platformsRequired.style.display = 'none'; if (platformsRequired) platformsRequired.style.display = 'none';
licenseRequired.style.display = 'none'; if (licenseRequired) licenseRequired.style.display = 'none';
platformsCheckboxes.forEach(cb => cb.removeAttribute('required')); platformsCheckboxes.forEach(cb => cb.removeAttribute('required'));
licenseSelect.removeAttribute('required'); if (licenseSelect) licenseSelect.removeAttribute('required');
// Show related concepts for methods // Show related concepts for methods
if (selectedType === 'method') { if (selectedType === 'method' && relatedConceptsField) {
relatedConceptsField.style.display = 'block'; relatedConceptsField.style.display = 'block';
} }
} }
// Update YAML preview // Update YAML preview
if (typeof updateYAMLPreview === 'function') {
updateYAMLPreview(); updateYAMLPreview();
} }
}
// Generate YAML preview // Generate YAML preview
function updateYAMLPreview() { function updateYAMLPreview() {
if (!yamlPreview) return;
try { try {
const formData = new FormData(form); const formData = new FormData(form);
const toolData = { const toolData = {
@ -533,62 +544,64 @@ document.addEventListener('DOMContentLoaded', () => {
// Handle related concepts // Handle related concepts
if (toolData.type !== 'concept') { if (toolData.type !== 'concept') {
toolData.related_concepts = formData.getAll('relatedConcepts') || null; const relatedConcepts = formData.getAll('relatedConcepts');
toolData.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null;
} }
// Convert to YAML-like format for preview // Generate YAML
let yamlContent = `- name: "${toolData.name}"\n`; yamlPreview.textContent = `name: "${toolData.name}"
if (toolData.icon) yamlContent += ` icon: "${toolData.icon}"\n`; ${toolData.icon ? `icon: "${toolData.icon}"` : '# icon: "📦"'}
yamlContent += ` type: ${toolData.type}\n`; type: ${toolData.type}
yamlContent += ` description: >\n ${toolData.description}\n`; description: "${toolData.description}"
if (toolData.domains.length > 0) { domains: [${toolData.domains.map(d => `"${d}"`).join(', ')}]
yamlContent += ` domains:\n${toolData.domains.map(d => ` - ${d}`).join('\n')}\n`; phases: [${toolData.phases.map(p => `"${p}"`).join(', ')}]
} skillLevel: ${toolData.skillLevel}
if (toolData.phases.length > 0) { url: "${toolData.url}"${toolData.platforms.length > 0 ? `
yamlContent += ` phases:\n${toolData.phases.map(p => ` - ${p}`).join('\n')}\n`; platforms: [${toolData.platforms.map(p => `"${p}"`).join(', ')}]` : ''}${toolData.license ? `
} license: "${toolData.license}"` : ''}${toolData.accessType ? `
if (toolData.platforms.length > 0) { accessType: ${toolData.accessType}` : ''}${toolData.projectUrl ? `
yamlContent += ` platforms:\n${toolData.platforms.map(p => ` - ${p}`).join('\n')}\n`; projectUrl: "${toolData.projectUrl}"` : ''}${toolData.knowledgebase ? `
} knowledgebase: true` : ''}${toolData.tags.length > 0 ? `
yamlContent += ` skillLevel: ${toolData.skillLevel}\n`; tags: [${toolData.tags.map(t => `"${t}"`).join(', ')}]` : ''}${toolData.related_concepts ? `
if (toolData.accessType) yamlContent += ` accessType: ${toolData.accessType}\n`; related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]` : ''}`;
yamlContent += ` url: ${toolData.url}\n`;
if (toolData.projectUrl) yamlContent += ` projectUrl: ${toolData.projectUrl}\n`;
if (toolData.license) yamlContent += ` license: ${toolData.license}\n`;
if (toolData.knowledgebase) yamlContent += ` knowledgebase: ${toolData.knowledgebase}\n`;
if (toolData.related_concepts && toolData.related_concepts.length > 0) {
yamlContent += ` related_concepts:\n${toolData.related_concepts.map(c => ` - ${c}`).join('\n')}\n`;
}
if (toolData.tags.length > 0) {
yamlContent += ` tags:\n${toolData.tags.map(t => ` - ${t}`).join('\n')}\n`;
}
yamlPreview.textContent = yamlContent;
} catch (error) { } catch (error) {
yamlPreview.textContent = `# Error generating preview: ${error.message}`; yamlPreview.textContent = '# Error generating preview';
console.error('YAML preview error:', error);
} }
} }
// Form validation // Form validation
function validateForm() { function validateForm() {
const errors = []; const errors = [];
const formData = new FormData(form);
const selectedType = typeSelect.value;
// Basic validation // Basic validation
if (!nameInput.value.trim()) errors.push('Name is required'); if (!formData.get('name')?.trim()) {
if (!descriptionTextarea.value.trim() || descriptionTextarea.value.length < 10) { errors.push('Tool name is required');
errors.push('Description must be at least 10 characters');
} }
const selectedType = typeSelect.value; if (!formData.get('description')?.trim()) {
errors.push('Description is required');
}
if (!formData.get('url')?.trim()) {
errors.push('URL is required');
}
if (!formData.get('skillLevel')) {
errors.push('Skill level is required');
}
// Type-specific validation // Type-specific validation
if (selectedType === 'software') { if (selectedType === 'software') {
const platforms = new FormData(form).getAll('platforms'); const platforms = formData.getAll('platforms');
if (platforms.length === 0) { if (platforms.length === 0) {
errors.push('At least one platform is required for software'); errors.push('At least one platform is required for software');
} }
if (!document.getElementById('tool-license').value.trim()) { if (!document.getElementById('tool-license')?.value?.trim()) {
errors.push('License is required for software'); errors.push('License is required for software');
} }
} }
@ -597,14 +610,22 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// Event listeners // Event listeners
if (typeSelect) {
typeSelect.addEventListener('change', updateFieldVisibility); typeSelect.addEventListener('change', updateFieldVisibility);
}
if (refreshPreviewBtn) {
refreshPreviewBtn.addEventListener('click', updateYAMLPreview); refreshPreviewBtn.addEventListener('click', updateYAMLPreview);
}
// Update preview on form changes // Update preview on form changes
if (form) {
form.addEventListener('input', debounce(updateYAMLPreview, 500)); form.addEventListener('input', debounce(updateYAMLPreview, 500));
form.addEventListener('change', updateYAMLPreview); form.addEventListener('change', updateYAMLPreview);
}
// Form submission // Form submission
if (form) {
form.addEventListener('submit', async (e) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
@ -615,14 +636,14 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// Show loading state // Show loading state
submitBtn.disabled = true; if (submitBtn) submitBtn.disabled = true;
submitText.textContent = isEdit ? 'Updating...' : 'Submitting...'; if (submitText) submitText.textContent = isEdit ? 'Updating...' : 'Submitting...';
submitSpinner.style.display = 'inline-block'; if (submitSpinner) submitSpinner.style.display = 'inline-block';
try { try {
const formData = new FormData(form); const formData = new FormData(form);
// Prepare submission data // FIXED: Prepare submission data with proper metadata.reason handling
const submissionData = { const submissionData = {
action: isEdit ? 'edit' : 'add', action: isEdit ? 'edit' : 'add',
tool: { tool: {
@ -637,16 +658,17 @@ document.addEventListener('DOMContentLoaded', () => {
tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : [] tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : []
}, },
metadata: { metadata: {
reason: formData.get('reason') || null // FIXED: Always provide a string, never null
reason: formData.get('reason')?.trim() || ''
} }
}; };
// Add type-specific fields // Add type-specific fields
if (submissionData.tool.type === 'software') { if (submissionData.tool.type === 'software') {
submissionData.tool.platforms = formData.getAll('platforms'); submissionData.tool.platforms = formData.getAll('platforms');
submissionData.tool.license = formData.get('license').trim(); submissionData.tool.license = formData.get('license')?.trim() || null;
submissionData.tool.accessType = formData.get('accessType'); submissionData.tool.accessType = formData.get('accessType') || null;
submissionData.tool.projectUrl = formData.get('projectUrl') || null; submissionData.tool.projectUrl = formData.get('projectUrl')?.trim() || null;
} }
// Add optional fields // Add optional fields
@ -657,6 +679,8 @@ document.addEventListener('DOMContentLoaded', () => {
submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null; submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null;
} }
console.log('Submitting:', submissionData); // Debug log
const response = await fetch('/api/contribute/tool', { const response = await fetch('/api/contribute/tool', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -669,16 +693,22 @@ document.addEventListener('DOMContentLoaded', () => {
if (result.success) { if (result.success) {
// Show success modal // Show success modal
document.getElementById('success-message').textContent = if (successModal) {
`Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`; const successMessage = document.getElementById('success-message');
if (successMessage) {
successMessage.textContent = `Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`;
}
if (result.prUrl) { if (result.prUrl) {
const prLink = document.getElementById('pr-link'); const prLink = document.getElementById('pr-link');
if (prLink) {
prLink.href = result.prUrl; prLink.href = result.prUrl;
prLink.style.display = 'inline-flex'; prLink.style.display = 'inline-flex';
} }
}
successModal.style.display = 'flex'; successModal.style.display = 'flex';
}
} else { } else {
let errorMessage = result.error || 'Submission failed'; let errorMessage = result.error || 'Submission failed';
if (result.details && Array.isArray(result.details)) { if (result.details && Array.isArray(result.details)) {
@ -691,30 +721,60 @@ document.addEventListener('DOMContentLoaded', () => {
alert('An error occurred while submitting your contribution. Please try again.'); alert('An error occurred while submitting your contribution. Please try again.');
} finally { } finally {
// Reset loading state // Reset loading state
submitBtn.disabled = false; if (submitBtn) submitBtn.disabled = false;
submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution'; if (submitText) submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution';
submitSpinner.style.display = 'none'; if (submitSpinner) submitSpinner.style.display = 'none';
} }
}); });
}
// Initialize form // FIXED: Initialize form with proper order
function initializeForm() {
if (isEdit && editTool) { if (isEdit && editTool) {
// Pre-fill edit form console.log('Initializing edit form for:', editTool.name);
// Set basic fields first
if (typeSelect) {
typeSelect.value = editTool.type; typeSelect.value = editTool.type;
}
// Update field visibility FIRST
updateFieldVisibility(); updateFieldVisibility();
// Set checkboxes for platforms // THEN set the platform checkboxes and access type after a brief delay
setTimeout(() => {
// FIXED: Set platform checkboxes with more specific selector
if (editTool.platforms) { if (editTool.platforms) {
editTool.platforms.forEach(platform => { editTool.platforms.forEach(platform => {
const checkbox = document.querySelector(`input[value="${platform}"]`); const checkbox = document.querySelector(`input[name="platforms"][value="${platform}"]`);
if (checkbox) checkbox.checked = true; if (checkbox) {
checkbox.checked = true;
console.log('Set platform checkbox:', platform);
} else {
console.warn('Platform checkbox not found:', platform);
}
}); });
} }
// FIXED: Set access type value
if (editTool.accessType) {
const accessTypeSelect = document.getElementById('access-type');
if (accessTypeSelect) {
accessTypeSelect.value = editTool.accessType;
console.log('Set access type:', editTool.accessType);
}
}
// Update YAML preview after all values are set
updateYAMLPreview(); updateYAMLPreview();
}, 100);
} else { } else {
updateFieldVisibility(); updateFieldVisibility();
} }
}
// Initialize form
initializeForm();
// Debounce utility // Debounce utility
function debounce(func, wait) { function debounce(func, wait) {

View File

@ -285,73 +285,80 @@ export class GitContributionManager {
} }
} }
// Original tool contribution methods (unchanged)
async submitContribution(data: ContributionData): Promise<GitOperationResult> { private generateToolYAML(tool: any): string {
// Clean up the tool object - remove null/undefined values
const cleanTool: any = {
name: tool.name,
type: tool.type,
description: tool.description,
domains: tool.domains || [],
phases: tool.phases || [],
skillLevel: tool.skillLevel,
url: tool.url
};
// Add optional fields only if they have values
if (tool.icon) cleanTool.icon = tool.icon;
if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms;
if (tool.license) cleanTool.license = tool.license;
if (tool.accessType) cleanTool.accessType = tool.accessType;
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
if (tool.related_concepts && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts;
if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags;
// Generate clean YAML
return dump(cleanTool, {
lineWidth: -1,
noRefs: true,
quotingType: '"',
forceQuotes: false,
indent: 2
});
}
async submitContribution(data: ContributionData): Promise<GitOperationResult> {
const branchName = `tool-${data.type}-${Date.now()}`; const branchName = `tool-${data.type}-${Date.now()}`;
try { try {
// Create branch // Create branch
await this.createBranch(branchName); await this.createBranch(branchName);
// Load current tools.yaml // FIXED: Don't modify tools.yaml at all - just create the tool data file
const toolsYamlPath = 'src/data/tools.yaml'; const toolYaml = this.generateToolYAML(data.tool);
const content = await this.readFile(toolsYamlPath); const contributionFileName = `contribution-${data.type}-${data.tool.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.yaml`;
const yamlData = load(content) as any;
if (!yamlData.tools) { // Create contributions directory if it doesn't exist and write the tool data
yamlData.tools = []; try {
await this.readFile('contributions/.gitkeep');
} catch {
// Directory doesn't exist, create it
await this.writeFile('contributions/.gitkeep', '# Contribution files directory\n');
} }
// Apply changes // Write the tool data as a separate file for maintainers to review
if (data.type === 'add') { await this.writeFile(`contributions/${contributionFileName}`, toolYaml);
// Check if tool already exists
const existing = yamlData.tools.find((t: any) => t.name === data.tool.name);
if (existing) {
throw new Error(`Tool "${data.tool.name}" already exists`);
}
yamlData.tools.push(data.tool);
} else if (data.type === 'edit') {
const index = yamlData.tools.findIndex((t: any) => t.name === data.tool.name);
if (index === -1) {
throw new Error(`Tool "${data.tool.name}" not found`);
}
yamlData.tools[index] = { ...yamlData.tools[index], ...data.tool };
}
// Sort tools alphabetically
yamlData.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
// Generate updated YAML
const updatedYaml = dump(yamlData, {
lineWidth: -1,
noRefs: true,
quotingType: '"',
forceQuotes: false
});
// Write updated file
await this.writeFile(toolsYamlPath, updatedYaml);
// Commit changes // Commit changes
const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name} const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}
Contributed by: ${data.metadata.submitter} Contributed by: ${data.metadata.submitter}
Type: ${data.tool.type} Type: ${data.tool.type}
${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`; ${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}
This contribution contains the raw tool data for manual review and integration.`;
await this.commitChanges(commitMessage); await this.commitChanges(commitMessage);
// Push branch // Push branch
await this.pushBranch(branchName); await this.pushBranch(branchName);
// Create pull request // Create pull request with enhanced description
const prUrl = await this.createPullRequest( const prUrl = await this.createPullRequest(
branchName, branchName,
`${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`, `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`,
this.generatePRDescription(data) this.generateEnhancedPRDescription(data, toolYaml)
); );
return { return {
@ -371,34 +378,59 @@ ${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
throw error; throw error;
} }
} }
private generatePRDescription(data: ContributionData): string {
private generateEnhancedPRDescription(data: ContributionData, toolYaml: string): string {
return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name} return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name}
**Type:** ${data.tool.type} **Type:** ${data.tool.type}
**Submitted by:** ${data.metadata.submitter} **Submitted by:** ${data.metadata.submitter}
**Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'}
### Tool Details ### Tool Details
- **Description:** ${data.tool.description} - **Name:** ${data.tool.name}
- **Domains:** ${data.tool.domains.join(', ')} - **Description:** ${data.tool.description}
- **Phases:** ${data.tool.phases.join(', ')} - **URL:** ${data.tool.url}
- **Skill Level:** ${data.tool.skillLevel} - **Skill Level:** ${data.tool.skillLevel}
- **License:** ${data.tool.license || 'Not specified'} ${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''}
- **URL:** ${data.tool.url} ${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''}
${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''}
- **Domains:** ${data.tool.domains.join(', ')}
- **Phases:** ${data.tool.phases.join(', ')}
${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''}
${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
${data.metadata.reason ? `### Reason for Contribution\n${data.metadata.reason}` : ''} ${data.metadata.reason ? `### Reason for Contribution
${data.metadata.reason}
### Review Checklist ` : ''}### Raw Tool Data (Copy & Paste Ready)
- [ ] Tool information is accurate and complete
- [ ] Description is clear and informative
- [ ] Domains and phases are correctly assigned
- [ ] Tags are relevant and consistent
- [ ] License information is correct
- [ ] URLs are valid and accessible
--- \`\`\`yaml
*This contribution was submitted via the CC24-Hub web interface.*`; ${toolYaml}\`\`\`
### For Maintainers
**To add this tool to tools.yaml:**
1. Copy the YAML data above
2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'}
3. Verify all fields are correct
4. Test that the tool displays properly
5. Close this PR
### Review Checklist
- [ ] Tool information is accurate and complete
- [ ] Description is clear and informative
- [ ] Domains and phases are correctly assigned
- [ ] Tags are relevant and consistent with existing tools
- [ ] License information is correct (for software)
- [ ] URLs are valid and accessible
- [ ] No duplicate tool entries
- [ ] YAML syntax is valid
---
*This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
} }
async checkHealth(): Promise<{healthy: boolean, issues?: string[]}> { async checkHealth(): Promise<{healthy: boolean, issues?: string[]}> {