mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat: implement auto-update TOOLS.md system with real runtime tool discovery
Replace hardcoded mock data (4 tools) with dynamic discovery system that generates TOOLS.md from actual V8 skills runtime (152+ tools). Key Features: - Real-time tool discovery via runtime_all_tools() command - Auto-generates comprehensive documentation with examples - Throttled updates (10s limit) to prevent excessive calls - Vite file watching exclusion to prevent reload loops - Manual update capability via dev mode button - OpenClaw-compliant formatting for AI context injection Technical Implementation: - New auto-update.ts module with discovery, grouping & markdown generation - Enhanced Tauri write_ai_config_file command for secure file operations - Removed auto-trigger from skill activation for better performance - Added ai/ directory to Vite ignore list Result: TOOLS.md now shows real tools from telegram (48), gmail (7), notion (25), github (72) instead of 4 hardcoded examples. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+3779
-180
File diff suppressed because it is too large
Load Diff
@@ -44,11 +44,52 @@ fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
/// Write AI configuration files to the project's ./ai/ directory
|
||||
#[tauri::command]
|
||||
async fn write_ai_config_file(filename: String, content: String) -> Result<bool, String> {
|
||||
use std::env;
|
||||
|
||||
// Get the project root directory (parent of src-tauri)
|
||||
let current_dir = env::current_dir()
|
||||
.map_err(|e| format!("Failed to get current directory: {e}"))?;
|
||||
|
||||
// Go up one level to get project root (since we're in src-tauri/)
|
||||
let project_root = current_dir
|
||||
.parent()
|
||||
.ok_or("Failed to get project root directory")?;
|
||||
|
||||
// Ensure filename is safe (only allow .md files)
|
||||
if !filename.ends_with(".md") {
|
||||
return Err("Only .md files are allowed".to_string());
|
||||
}
|
||||
|
||||
// Prevent path traversal by checking for dangerous characters
|
||||
if filename.contains("..") || filename.contains("/") || filename.contains("\\") {
|
||||
return Err("Invalid filename: path traversal not allowed".to_string());
|
||||
}
|
||||
|
||||
// Create path to ai/ directory in project root
|
||||
let ai_dir = project_root.join("ai");
|
||||
let file_path = ai_dir.join(&filename);
|
||||
|
||||
// Ensure ai directory exists
|
||||
std::fs::create_dir_all(&ai_dir)
|
||||
.map_err(|e| format!("Failed to create ai directory: {e}"))?;
|
||||
|
||||
// Write the file
|
||||
std::fs::write(&file_path, content)
|
||||
.map_err(|e| format!("Failed to write file {}: {e}", filename))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// Macro to define common handlers shared across all platforms
|
||||
macro_rules! common_handlers {
|
||||
() => {
|
||||
// Demo
|
||||
greet,
|
||||
// AI config file writing
|
||||
write_ai_config_file,
|
||||
// Auth commands
|
||||
get_auth_state,
|
||||
get_session_token,
|
||||
@@ -649,6 +690,8 @@ pub fn run() {
|
||||
tauri::generate_handler![
|
||||
// Common handlers (expanded from common_handlers! macro)
|
||||
greet,
|
||||
// AI config file writing
|
||||
write_ai_config_file,
|
||||
get_auth_state,
|
||||
get_session_token,
|
||||
get_current_user,
|
||||
@@ -775,6 +818,8 @@ pub fn run() {
|
||||
tauri::generate_handler![
|
||||
// Common handlers (expanded from common_handlers! macro)
|
||||
greet,
|
||||
// AI config file writing
|
||||
write_ai_config_file,
|
||||
get_auth_state,
|
||||
get_session_token,
|
||||
get_current_user,
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Auto-update TOOLS.md with all available tools from the runtime
|
||||
*
|
||||
* This module provides functionality to automatically update the TOOLS.md file
|
||||
* with all tools discovered from the running skills system.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// Prevent excessive updates - limit to once per 10 seconds
|
||||
let lastUpdateTime = 0;
|
||||
const UPDATE_THROTTLE_MS = 10000;
|
||||
|
||||
interface RuntimeToolResponse {
|
||||
skillId: string;
|
||||
tool: {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: {
|
||||
type: string;
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tools from the runtime and update TOOLS.md
|
||||
*/
|
||||
export async function updateToolsDocumentation(): Promise<void> {
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateTime < UPDATE_THROTTLE_MS) {
|
||||
console.log('⏭️ TOOLS.md update skipped - throttled (last update was recent)');
|
||||
return;
|
||||
}
|
||||
lastUpdateTime = now;
|
||||
|
||||
console.log('=== AUTO-UPDATE TOOLS.md START ===');
|
||||
|
||||
try {
|
||||
console.log('🔍 Step 1: Calling runtime_all_tools command...');
|
||||
|
||||
// Call the existing runtime command to get all tools
|
||||
const toolsResponse = await invoke<RuntimeToolResponse[]>('runtime_all_tools');
|
||||
|
||||
console.log('🔧 Step 2: Analyzing response...');
|
||||
console.log('🔧 Raw response from runtime_all_tools:', toolsResponse);
|
||||
console.log('🔧 Response type:', typeof toolsResponse);
|
||||
console.log('🔧 Is array:', Array.isArray(toolsResponse));
|
||||
|
||||
if (Array.isArray(toolsResponse)) {
|
||||
console.log(`🔧 Array length: ${toolsResponse.length}`);
|
||||
if (toolsResponse.length > 0) {
|
||||
console.log('🔧 First tool sample:', JSON.stringify(toolsResponse[0], null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
if (!toolsResponse || toolsResponse.length === 0) {
|
||||
console.warn('⚠️ Step 3: No tools discovered from runtime - STOPPING');
|
||||
console.warn('⚠️ This means runtime_all_tools returned empty/null');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ Step 3: Discovered ${toolsResponse.length} tools from runtime`);
|
||||
|
||||
// Transform the tools data into grouped format
|
||||
console.log('🔍 Step 4: Grouping tools by skill...');
|
||||
const toolsBySkill = groupToolsBySkill(toolsResponse);
|
||||
console.log('🔧 Tools by skill:', Object.keys(toolsBySkill).map(skillId => `${skillId}: ${toolsBySkill[skillId].length} tools`));
|
||||
|
||||
// Generate the TOOLS.md content
|
||||
console.log('🔍 Step 5: Generating TOOLS.md markdown content...');
|
||||
const markdownContent = generateToolsMarkdown(toolsBySkill);
|
||||
console.log(`🔧 Generated markdown length: ${markdownContent.length} characters`);
|
||||
|
||||
// Write to ai/TOOLS.md using new Tauri command
|
||||
console.log('🔍 Step 6: Writing to ai/TOOLS.md file...');
|
||||
console.log('🔧 About to call write_ai_config_file with filename: TOOLS.md');
|
||||
console.log('🔧 Content length:', markdownContent.length);
|
||||
|
||||
try {
|
||||
const writeResult = await invoke('write_ai_config_file', {
|
||||
filename: 'TOOLS.md',
|
||||
content: markdownContent
|
||||
});
|
||||
console.log('🔧 Write command result:', writeResult);
|
||||
} catch (writeError) {
|
||||
console.error('❌ Write command failed:', writeError);
|
||||
console.error('❌ Error type:', typeof writeError);
|
||||
console.error('❌ Error toString:', writeError?.toString());
|
||||
throw new Error(`File write failed: ${writeError}`);
|
||||
}
|
||||
|
||||
console.log('✅ SUCCESS: TOOLS.md updated successfully!');
|
||||
console.log(`📄 Final result: ${toolsResponse.length} tools from ${Object.keys(toolsBySkill).length} skills`);
|
||||
console.log('=== AUTO-UPDATE TOOLS.md COMPLETE ===');
|
||||
} catch (error) {
|
||||
console.error('❌ FATAL ERROR in updateToolsDocumentation:', error);
|
||||
console.error('❌ Error stack:', error instanceof Error ? error.stack : 'No stack trace');
|
||||
console.error('=== AUTO-UPDATE TOOLS.md FAILED ===');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group tools by skill for organized documentation
|
||||
*/
|
||||
function groupToolsBySkill(toolsResponse: RuntimeToolResponse[]): Record<string, RuntimeToolResponse[]> {
|
||||
const grouped: Record<string, RuntimeToolResponse[]> = {};
|
||||
|
||||
for (const toolResponse of toolsResponse) {
|
||||
const skillId = toolResponse.skillId;
|
||||
if (!grouped[skillId]) {
|
||||
grouped[skillId] = [];
|
||||
}
|
||||
grouped[skillId].push(toolResponse);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate markdown content for TOOLS.md
|
||||
*/
|
||||
function generateToolsMarkdown(toolsBySkill: Record<string, RuntimeToolResponse[]>): string {
|
||||
const totalTools = Object.values(toolsBySkill).flat().length;
|
||||
const skillCount = Object.keys(toolsBySkill).length;
|
||||
|
||||
const content = [`# AlphaHuman Tools
|
||||
|
||||
This document lists all available tools that AlphaHuman can use to interact with external services and perform actions. Tools are organized by integration and automatically updated when the app loads.
|
||||
|
||||
## Overview
|
||||
|
||||
AlphaHuman has access to **${totalTools} tools** across **${skillCount} integrations**.
|
||||
|
||||
**Quick Statistics:**
|
||||
${Object.entries(toolsBySkill)
|
||||
.map(([skillId, tools]) => `- **${formatSkillName(skillId)}**: ${tools.length} tools`)
|
||||
.join('\n')}
|
||||
|
||||
## Available Tools
|
||||
`];
|
||||
|
||||
// Generate tool documentation for each skill
|
||||
for (const [skillId, tools] of Object.entries(toolsBySkill)) {
|
||||
content.push(`\n### ${formatSkillName(skillId)} Tools\n`);
|
||||
content.push(`This skill provides ${tools.length} tools for ${skillId} integration.\n`);
|
||||
|
||||
for (const toolResponse of tools) {
|
||||
const tool = toolResponse.tool;
|
||||
content.push(`#### ${tool.name}\n`);
|
||||
content.push(`**Description**: ${tool.description}\n`);
|
||||
|
||||
// Generate parameters documentation
|
||||
const properties = tool.input_schema?.properties || {};
|
||||
const required = tool.input_schema?.required || [];
|
||||
|
||||
if (Object.keys(properties).length > 0) {
|
||||
content.push('**Parameters**:');
|
||||
for (const [paramName, paramDef] of Object.entries(properties)) {
|
||||
const isRequired = required.includes(paramName);
|
||||
const requiredText = isRequired ? ' **(required)**' : '';
|
||||
const type = (paramDef as any).type || 'any';
|
||||
const description = (paramDef as any).description || 'No description';
|
||||
content.push(`- **${paramName}** (${type})${requiredText}: ${description}`);
|
||||
}
|
||||
} else {
|
||||
content.push('**Parameters**: *None*');
|
||||
}
|
||||
|
||||
content.push('\n**Usage Context**: Available in all environments\n');
|
||||
|
||||
// Generate example usage
|
||||
const exampleParams: Record<string, any> = {};
|
||||
for (const [paramName, paramDef] of Object.entries(properties)) {
|
||||
const type = (paramDef as any).type;
|
||||
exampleParams[paramName] = getExampleValue(paramName, type);
|
||||
}
|
||||
|
||||
content.push('**Example**:');
|
||||
content.push('```json');
|
||||
content.push(JSON.stringify({
|
||||
tool: tool.name,
|
||||
parameters: exampleParams
|
||||
}, null, 2));
|
||||
content.push('```\n');
|
||||
|
||||
content.push('---\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Add footer
|
||||
content.push(`
|
||||
## Tool Usage Guidelines
|
||||
|
||||
### Authentication
|
||||
- All tools require proper authentication setup through the Skills system
|
||||
- OAuth credentials are managed securely and refreshed automatically
|
||||
- API keys are stored encrypted in the application keychain
|
||||
|
||||
### Rate Limiting
|
||||
- Tools automatically respect API rate limits of external services
|
||||
- Intelligent retry logic handles temporary failures with exponential backoff
|
||||
|
||||
### Error Handling
|
||||
- All tools return structured error responses with detailed information
|
||||
- Network failures trigger automatic retry with configurable attempts
|
||||
|
||||
---
|
||||
|
||||
**Tool Statistics**
|
||||
- Total Tools: ${totalTools}
|
||||
- Active Skills: ${skillCount}
|
||||
- Last Updated: ${new Date().toISOString()}
|
||||
|
||||
*This file was automatically generated when the app loaded.*
|
||||
*Tools are discovered from the running V8 skills runtime.*`);
|
||||
|
||||
return content.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format skill ID into readable name
|
||||
*/
|
||||
function formatSkillName(skillId: string): string {
|
||||
return skillId
|
||||
.split(/[-_]/)
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate example value for a parameter
|
||||
*/
|
||||
function getExampleValue(paramName: string, type: string): any {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return `example_${paramName}`;
|
||||
case 'number':
|
||||
return 10;
|
||||
case 'boolean':
|
||||
return true;
|
||||
case 'array':
|
||||
return [];
|
||||
case 'object':
|
||||
return {};
|
||||
default:
|
||||
return `example_${paramName}`;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import SkillSetupModal from '../components/skills/SkillSetupModal';
|
||||
import { useIntelligenceStats } from '../hooks/useIntelligenceStats';
|
||||
import { deriveConnectionStatus, useSkillConnectionStatus } from '../lib/skills/hooks';
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import { updateToolsDocumentation } from '../lib/tools/auto-update';
|
||||
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { IS_DEV } from '../utils/config';
|
||||
@@ -269,6 +270,18 @@ export default function Intelligence() {
|
||||
setSetupModalOpen(true);
|
||||
};
|
||||
|
||||
// Test function to manually update TOOLS.md
|
||||
const handleUpdateTools = async () => {
|
||||
console.log('🚀 Intelligence Page: Update TOOLS.md button clicked');
|
||||
try {
|
||||
console.log('🚀 Intelligence Page: Calling updateToolsDocumentation...');
|
||||
await updateToolsDocumentation();
|
||||
console.log('✅ Intelligence Page: TOOLS.md update completed successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Intelligence Page: Failed to update TOOLS.md:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// AI status indicator
|
||||
const aiStatusLabel =
|
||||
aiStatus === 'ready'
|
||||
@@ -326,11 +339,20 @@ export default function Intelligence() {
|
||||
<div className="animate-fade-up" style={{ animationDelay: '100ms' }}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-white opacity-80">Active Skills</h2>
|
||||
<button
|
||||
onClick={() => setManagementModalOpen(true)}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 transition-colors">
|
||||
Manage Skills
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{IS_DEV && (
|
||||
<button
|
||||
onClick={handleUpdateTools}
|
||||
className="text-xs text-sage-400 hover:text-sage-300 transition-colors">
|
||||
Update TOOLS.md
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setManagementModalOpen(true)}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 transition-colors">
|
||||
Manage Skills
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{skillsLoading ? (
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ export default defineConfig(async () => ({
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
// 3. tell Vite to ignore watching `src-tauri` and `ai` directories
|
||||
ignored: ["**/src-tauri/**", "**/ai/**"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user