update gpt
This commit is contained in:
		| @@ -32,6 +32,12 @@ axum = "0.7" | |||||||
| tower = "0.5" | tower = "0.5" | ||||||
| tower-http = { version = "0.5", features = ["cors", "fs"] } | tower-http = { version = "0.5", features = ["cors", "fs"] } | ||||||
| hyper = { version = "1.0", features = ["full"] } | hyper = { version = "1.0", features = ["full"] } | ||||||
|  | # Documentation generation dependencies | ||||||
|  | syn = { version = "2.0", features = ["full", "parsing", "visit"] } | ||||||
|  | quote = "1.0" | ||||||
|  | ignore = "0.4" | ||||||
|  | git2 = "0.18" | ||||||
|  | regex = "1.0" | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| tempfile = "3.14" | tempfile = "3.14" | ||||||
							
								
								
									
										418
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										418
									
								
								README.md
									
									
									
									
									
								
							| @@ -4,15 +4,40 @@ A Rust-based static blog generator with AI integration capabilities. | |||||||
|  |  | ||||||
| ## Overview | ## Overview | ||||||
|  |  | ||||||
| ai.log is part of the ai ecosystem - a static site generator that creates blogs with built-in AI features for content enhancement and atproto integration. | ai.log is part of the ai ecosystem - a static site generator that creates blogs with built-in AI features for content enhancement and atproto integration. The system follows the yui system principles with dual-layer MCP architecture. | ||||||
|  |  | ||||||
|  | ## Architecture | ||||||
|  |  | ||||||
|  | ### Dual MCP Integration | ||||||
|  |  | ||||||
|  | **ai.log MCP Server (API Layer)** | ||||||
|  | - **Role**: Independent blog API | ||||||
|  | - **Port**: 8002 | ||||||
|  | - **Location**: `./src/mcp/` | ||||||
|  | - **Function**: Core blog generation and management | ||||||
|  |  | ||||||
|  | **ai.gpt Integration (Server Layer)** | ||||||
|  | - **Role**: AI integration gateway | ||||||
|  | - **Port**: 8001 (within ai.gpt) | ||||||
|  | - **Location**: `../src/aigpt/mcp_server.py` | ||||||
|  | - **Function**: AI memory system + HTTP proxy to ai.log | ||||||
|  |  | ||||||
|  | ### Data Flow | ||||||
|  | ``` | ||||||
|  | Claude Code → ai.gpt (Server/AI) → ai.log (API/Blog) → Static Site | ||||||
|  |               ↑                      ↑ | ||||||
|  |               Memory System          File Operations | ||||||
|  |               Relationship AI        Markdown Processing | ||||||
|  |               Context Analysis       Template Rendering | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
|  |  | ||||||
| - Static blog generation (inspired by Zola) | - **Static Blog Generation**: Inspired by Zola, built with Rust | ||||||
| - AI-powered article editing and enhancement | - **AI-Powered Content**: Memory-driven article generation via ai.gpt | ||||||
| - Automatic translation (ja → en) | - **🌍 Ollama Translation**: Multi-language markdown translation with structure preservation | ||||||
| - AI comment system integrated with atproto | - **atproto Integration**: OAuth authentication and comment system (planned) | ||||||
| - OAuth authentication via atproto accounts | - **MCP Integration**: Seamless Claude Code workflow | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
| @@ -22,6 +47,8 @@ cargo install ailog | |||||||
|  |  | ||||||
| ## Usage | ## Usage | ||||||
|  |  | ||||||
|  | ### Standalone Mode | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # Initialize a new blog | # Initialize a new blog | ||||||
| ailog init myblog | ailog init myblog | ||||||
| @@ -35,53 +62,380 @@ ailog build | |||||||
| # Serve locally | # Serve locally | ||||||
| ailog serve | ailog serve | ||||||
|  |  | ||||||
|  | # Start MCP server | ||||||
|  | ailog mcp --port 8002 | ||||||
|  |  | ||||||
|  | # Generate documentation | ||||||
|  | ailog doc readme --with-ai | ||||||
|  | ailog doc api --output ./docs | ||||||
|  | ailog doc structure --include-deps | ||||||
|  |  | ||||||
|  | # Translate documents (requires Ollama) | ||||||
|  | ailog doc translate --input README.md --target-lang en | ||||||
|  | ailog doc translate --input docs/api.md --target-lang ja --model qwen2.5:latest | ||||||
|  |  | ||||||
| # Clean build files | # Clean build files | ||||||
| ailog clean | ailog clean | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ### AI Ecosystem Integration | ||||||
|  |  | ||||||
|  | When integrated with ai.gpt, use natural language: | ||||||
|  | - "ブログ記事を書いて" → Triggers `log_ai_content` | ||||||
|  | - "記事一覧を見せて" → Triggers `log_list_posts` | ||||||
|  | - "ブログをビルドして" → Triggers `log_build_blog` | ||||||
|  |  | ||||||
|  | ### Documentation & Translation | ||||||
|  |  | ||||||
|  | Generate comprehensive documentation and translate content: | ||||||
|  | - "READMEを生成して" → Triggers `log_generate_docs` | ||||||
|  | - "APIドキュメントを作成して" → Generates API documentation | ||||||
|  | - "プロジェクト構造を解析して" → Creates structure documentation | ||||||
|  | - "このファイルを英語に翻訳して" → Triggers `log_translate_document` | ||||||
|  | - "マークダウンを日本語に変換して" → Uses Ollama for translation | ||||||
|  |  | ||||||
|  | ## MCP Tools | ||||||
|  |  | ||||||
|  | ### ai.log Server (Port 8002) | ||||||
|  | - `create_blog_post` - Create new blog post | ||||||
|  | - `list_blog_posts` - List existing posts | ||||||
|  | - `build_blog` - Build static site | ||||||
|  | - `get_post_content` - Get post by slug | ||||||
|  | - `translate_document` ⭐ - Ollama-powered markdown translation | ||||||
|  | - `generate_documentation` ⭐ - Code analysis and documentation generation | ||||||
|  |  | ||||||
|  | ### ai.gpt Integration (Port 8001) | ||||||
|  | - `log_create_post` - Proxy to ai.log + error handling | ||||||
|  | - `log_list_posts` - Proxy to ai.log + formatting | ||||||
|  | - `log_build_blog` - Proxy to ai.log + AI features | ||||||
|  | - `log_get_post` - Proxy to ai.log + context | ||||||
|  | - `log_system_status` - Health check for ai.log | ||||||
|  | - `log_ai_content` ⭐ - AI memory → blog content generation | ||||||
|  | - `log_translate_document` 🌍 - Document translation via Ollama | ||||||
|  | - `log_generate_docs` 📚 - Documentation generation | ||||||
|  |  | ||||||
|  | ### Documentation Generation Tools | ||||||
|  | - `doc readme` - Generate README.md from project analysis | ||||||
|  | - `doc api` - Generate API documentation | ||||||
|  | - `doc structure` - Analyze and document project structure | ||||||
|  | - `doc changelog` - Generate changelog from git history | ||||||
|  | - `doc translate` 🌍 - Multi-language document translation | ||||||
|  |  | ||||||
|  | ### Translation Features | ||||||
|  | - **Language Support**: English, Japanese, Chinese, Korean, Spanish | ||||||
|  | - **Markdown Preservation**: Code blocks, links, images, tables maintained | ||||||
|  | - **Auto-Detection**: Automatically detects Japanese content | ||||||
|  | - **Ollama Integration**: Uses local AI models for privacy and cost-efficiency | ||||||
|  | - **Smart Processing**: Section-by-section translation with structure awareness | ||||||
|  |  | ||||||
| ## Configuration | ## Configuration | ||||||
|  |  | ||||||
| Configuration files are stored in `~/.config/syui/ai/log/` | ### ai.log Configuration | ||||||
|  | - Location: `~/.config/syui/ai/log/` | ||||||
|  | - Format: TOML configuration | ||||||
|  |  | ||||||
| ## AI Integration (Planned) | ### ai.gpt Integration | ||||||
|  | - Configuration: `../config.json` | ||||||
|  | - Auto-detection: ai.log tools enabled when `./log/` directory exists | ||||||
|  | - System prompt: Automatically triggers blog tools for related queries | ||||||
|  |  | ||||||
| - Automatic content suggestions and corrections | ## AI Integration Features | ||||||
| - Multi-language support with AI translation |  | ||||||
| - AI-generated comments linked to atproto accounts | ### Memory-Driven Content Generation | ||||||
|  | - **Source**: ai.gpt memory system | ||||||
|  | - **Process**: Contextual memories → AI analysis → Blog content | ||||||
|  | - **Output**: Structured markdown with personal insights | ||||||
|  |  | ||||||
|  | ### Automatic Workflows | ||||||
|  | - Daily blog posts from accumulated memories | ||||||
|  | - Content enhancement and suggestions | ||||||
|  | - Related article recommendations | ||||||
|  | - Multi-language content generation | ||||||
|  |  | ||||||
| ## atproto Integration (Planned) | ## atproto Integration (Planned) | ||||||
|  |  | ||||||
| Implements OAuth 2.0 for user authentication: | ### OAuth 2.0 Authentication | ||||||
| - Users can comment using their atproto accounts | - Client metadata: `public/client-metadata.json` | ||||||
| - Comments are stored in atproto collections | - Comment system integration | ||||||
| - Full data sovereignty for users | - Data sovereignty: Users own their comments | ||||||
|  | - Collection storage in atproto | ||||||
|  |  | ||||||
|  | ### Comment System | ||||||
|  | - atproto account login | ||||||
|  | - Distributed comment storage | ||||||
|  | - Real-time comment synchronization | ||||||
|  |  | ||||||
| ## Build & Deploy | ## Build & Deploy | ||||||
|  |  | ||||||
| Designed for GitHub Actions and Cloudflare Pages deployment. Push to main branch triggers automatic build and deploy. | ### GitHub Actions | ||||||
|  | ```yaml | ||||||
|  | # .github/workflows/gh-pages.yml | ||||||
|  | - name: Build ai.log | ||||||
|  |   run: | | ||||||
|  |     cd log | ||||||
|  |     cargo build --release | ||||||
|  |     ./target/release/ailog build | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Cloudflare Pages | ||||||
|  | - Static output: `./public/` | ||||||
|  | - Automatic deployment on main branch push | ||||||
|  | - AI content generation during build process | ||||||
|  |  | ||||||
| ## Development Status | ## Development Status | ||||||
|  |  | ||||||
| Currently implemented: | ### ✅ Completed Features | ||||||
| - ✅ Project structure and Cargo.toml setup | - Project structure and Cargo.toml setup | ||||||
| - ✅ Basic command-line interface (init, new, build, serve, clean) | - CLI interface (init, new, build, serve, clean, mcp, doc) | ||||||
| - ✅ Configuration system with TOML support | - Configuration system with TOML support | ||||||
| - ✅ Markdown parsing with frontmatter support | - Markdown parsing with frontmatter support | ||||||
| - ✅ Template system with Handlebars | - Template system with Handlebars | ||||||
| - ✅ Static site generation with posts and pages | - Static site generation with posts and pages | ||||||
| - ✅ Development server with hot reload | - Development server with hot reload | ||||||
| - ✅ AI integration foundation (GPT client, translator, comment system) | - **MCP server integration (both layers)** | ||||||
| - ✅ atproto client with OAuth support | - **ai.gpt integration with 6 tools** | ||||||
| - ✅ MCP server integration for AI tools | - **AI memory system connection** | ||||||
| - ✅ Test blog with sample content and styling | - **📚 Documentation generation from code** | ||||||
|  | - **🔍 Rust project analysis and API extraction** | ||||||
|  | - **📝 README, API docs, and structure analysis** | ||||||
|  | - **🌍 Ollama-powered translation system** | ||||||
|  | - **🚀 Complete MCP integration with ai.gpt** | ||||||
|  | - **📄 Markdown-aware translation preserving structure** | ||||||
|  | - Test blog with sample content and styling | ||||||
|  |  | ||||||
| Planned features: | ### 🚧 In Progress | ||||||
| - AI-powered content enhancement and suggestions | - AI-powered content enhancement pipeline | ||||||
| - Automatic translation (ja → en) pipeline | - atproto OAuth implementation | ||||||
| - atproto comment system with OAuth authentication |  | ||||||
|  | ### 📋 Planned Features | ||||||
| - Advanced template customization | - Advanced template customization | ||||||
| - Plugin system for extensibility | - Plugin system for extensibility | ||||||
|  | - Real-time comment system | ||||||
|  | - Multi-blog management | ||||||
|  | - VTuber integration (ai.verse connection) | ||||||
|  |  | ||||||
|  | ## Integration with ai Ecosystem | ||||||
|  |  | ||||||
|  | ### System Dependencies | ||||||
|  | - **ai.gpt**: Memory system, relationship tracking, AI provider | ||||||
|  | - **ai.card**: Future cross-system content sharing | ||||||
|  | - **ai.bot**: atproto posting and mention handling | ||||||
|  | - **ai.verse**: 3D world blog representation (future) | ||||||
|  |  | ||||||
|  | ### yui System Compliance | ||||||
|  | - **Uniqueness**: Each blog post tied to individual identity | ||||||
|  | - **Reality Reflection**: Personal memories → digital content | ||||||
|  | - **Irreversibility**: Published content maintains historical integrity | ||||||
|  |  | ||||||
|  | ## Getting Started | ||||||
|  |  | ||||||
|  | ### 1. Standalone Usage | ||||||
|  | ```bash | ||||||
|  | git clone [repository] | ||||||
|  | cd log | ||||||
|  | cargo run -- init my-blog | ||||||
|  | cargo run -- new "First Post" | ||||||
|  | cargo run -- build | ||||||
|  | cargo run -- serve | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 2. AI Ecosystem Integration | ||||||
|  | ```bash | ||||||
|  | # Start ai.log MCP server | ||||||
|  | cargo run -- mcp --port 8002 | ||||||
|  |  | ||||||
|  | # In another terminal, start ai.gpt | ||||||
|  | cd ../ | ||||||
|  | # ai.gpt startup commands | ||||||
|  |  | ||||||
|  | # Use Claude Code with natural language blog commands | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Documentation Generation Features | ||||||
|  |  | ||||||
|  | ### 📚 Automatic README Generation | ||||||
|  | ```bash | ||||||
|  | # Generate README from project analysis | ||||||
|  | ailog doc readme --source ./src --with-ai | ||||||
|  |  | ||||||
|  | # Output: Enhanced README.md with: | ||||||
|  | # - Project overview and metrics | ||||||
|  | # - Dependency analysis | ||||||
|  | # - Module structure | ||||||
|  | # - AI-generated insights | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 📖 API Documentation | ||||||
|  | ```bash | ||||||
|  | # Generate comprehensive API docs | ||||||
|  | ailog doc api --source ./src --format markdown --output ./docs | ||||||
|  |  | ||||||
|  | # Creates: | ||||||
|  | # - docs/api.md (main API overview) | ||||||
|  | # - docs/module_name.md (per-module documentation) | ||||||
|  | # - Function signatures and documentation | ||||||
|  | # - Struct/enum definitions | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 🏗️ Project Structure Analysis | ||||||
|  | ```bash | ||||||
|  | # Analyze and document project structure | ||||||
|  | ailog doc structure --source . --include-deps | ||||||
|  |  | ||||||
|  | # Generates: | ||||||
|  | # - Directory tree visualization | ||||||
|  | # - File distribution by language | ||||||
|  | # - Dependency graph analysis | ||||||
|  | # - Code metrics and statistics | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 📝 Git Changelog Generation | ||||||
|  | ```bash | ||||||
|  | # Generate changelog from git history | ||||||
|  | ailog doc changelog --from v1.0.0 --explain-changes | ||||||
|  |  | ||||||
|  | # Creates: | ||||||
|  | # - Structured changelog | ||||||
|  | # - Commit categorization | ||||||
|  | # - AI-enhanced change explanations | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 🤖 AI-Enhanced Documentation | ||||||
|  | When `--with-ai` is enabled: | ||||||
|  | - **Content Enhancement**: AI improves readability and adds insights | ||||||
|  | - **Context Awareness**: Leverages ai.gpt memory system | ||||||
|  | - **Smart Categorization**: Automatic organization of content | ||||||
|  | - **Technical Writing**: Professional documentation style | ||||||
|  |  | ||||||
|  | ## 🌍 Translation System | ||||||
|  |  | ||||||
|  | ### Ollama-Powered Translation | ||||||
|  |  | ||||||
|  | ai.log includes a comprehensive translation system powered by Ollama AI models: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Basic translation | ||||||
|  | ailog doc translate --input README.md --target-lang en | ||||||
|  |  | ||||||
|  | # Advanced translation with custom settings | ||||||
|  | ailog doc translate \ | ||||||
|  |   --input docs/technical-guide.ja.md \ | ||||||
|  |   --target-lang en \ | ||||||
|  |   --source-lang ja \ | ||||||
|  |   --output docs/technical-guide.en.md \ | ||||||
|  |   --model qwen2.5:latest \ | ||||||
|  |   --ollama-endpoint http://localhost:11434 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Translation Features | ||||||
|  |  | ||||||
|  | #### 📄 Markdown-Aware Processing | ||||||
|  | - **Code Block Preservation**: All code snippets remain untranslated | ||||||
|  | - **Link Maintenance**: URLs and link structures preserved | ||||||
|  | - **Image Handling**: Alt text can be translated while preserving image paths | ||||||
|  | - **Table Translation**: Table content translated while maintaining structure | ||||||
|  | - **Header Preservation**: Markdown headers translated with level maintenance | ||||||
|  |  | ||||||
|  | #### 🎯 Smart Language Detection | ||||||
|  | - **Auto-Detection**: Automatically detects Japanese content using Unicode ranges | ||||||
|  | - **Manual Override**: Specify source language for precise control | ||||||
|  | - **Mixed Content**: Handles documents with multiple languages | ||||||
|  |  | ||||||
|  | #### 🔧 Flexible Configuration | ||||||
|  | - **Model Selection**: Choose from available Ollama models | ||||||
|  | - **Custom Endpoints**: Use different Ollama instances | ||||||
|  | - **Output Control**: Auto-generate or specify output paths | ||||||
|  | - **Batch Processing**: Process multiple files efficiently | ||||||
|  |  | ||||||
|  | ### Supported Languages | ||||||
|  |  | ||||||
|  | | Language | Code | Direction | Model Optimized | | ||||||
|  | |----------|------|-----------|-----------------| | ||||||
|  | | English  | `en` | ↔️        | ✅ qwen2.5      | | ||||||
|  | | Japanese | `ja` | ↔️        | ✅ qwen2.5      | | ||||||
|  | | Chinese  | `zh` | ↔️        | ✅ qwen2.5      | | ||||||
|  | | Korean   | `ko` | ↔️        | ⚠️ Basic       | | ||||||
|  | | Spanish  | `es` | ↔️        | ⚠️ Basic       | | ||||||
|  |  | ||||||
|  | ### Translation Workflow | ||||||
|  |  | ||||||
|  | 1. **Parse Document**: Analyze markdown structure and identify sections | ||||||
|  | 2. **Preserve Code**: Isolate code blocks and technical content | ||||||
|  | 3. **Translate Content**: Process text sections with Ollama AI | ||||||
|  | 4. **Reconstruct**: Rebuild document maintaining original formatting | ||||||
|  | 5. **Validate**: Ensure structural integrity and completeness | ||||||
|  |  | ||||||
|  | ### Integration with ai.gpt | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | # Via ai.gpt MCP tools | ||||||
|  | await log_translate_document( | ||||||
|  |     input_file="README.ja.md", | ||||||
|  |     target_lang="en", | ||||||
|  |     model="qwen2.5:latest" | ||||||
|  | ) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Requirements | ||||||
|  |  | ||||||
|  | - **Ollama**: Install and run Ollama locally | ||||||
|  | - **Models**: Download supported models (qwen2.5:latest recommended) | ||||||
|  | - **Memory**: Sufficient RAM for model inference | ||||||
|  | - **Network**: For initial model download only | ||||||
|  |  | ||||||
|  | ## Configuration Examples | ||||||
|  |  | ||||||
|  | ### Basic Blog Config | ||||||
|  | ```toml | ||||||
|  | [blog] | ||||||
|  | title = "My AI Blog" | ||||||
|  | description = "Personal thoughts and AI insights" | ||||||
|  | base_url = "https://myblog.example.com" | ||||||
|  |  | ||||||
|  | [ai] | ||||||
|  | provider = "openai" | ||||||
|  | model = "gpt-4" | ||||||
|  | translation = true | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Advanced Integration | ||||||
|  | ```json | ||||||
|  | // ../config.json (ai.gpt) | ||||||
|  | { | ||||||
|  |   "mcp": { | ||||||
|  |     "servers": { | ||||||
|  |       "ai_gpt": { | ||||||
|  |         "endpoints": { | ||||||
|  |           "log_ai_content": "/log_ai_content", | ||||||
|  |           "log_create_post": "/log_create_post" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Troubleshooting | ||||||
|  |  | ||||||
|  | ### MCP Connection Issues | ||||||
|  | - Ensure ai.log server is running: `cargo run -- mcp --port 8002` | ||||||
|  | - Check ai.gpt config includes log endpoints | ||||||
|  | - Verify `./log/` directory exists relative to ai.gpt | ||||||
|  |  | ||||||
|  | ### Build Failures | ||||||
|  | - Check Rust version: `rustc --version` | ||||||
|  | - Update dependencies: `cargo update` | ||||||
|  | - Clear cache: `cargo clean` | ||||||
|  |  | ||||||
|  | ### AI Integration Problems | ||||||
|  | - Verify ai.gpt memory system is initialized | ||||||
|  | - Check AI provider configuration | ||||||
|  | - Ensure sufficient context in memory system | ||||||
|  |  | ||||||
| ## License | ## License | ||||||
|  |  | ||||||
| © syui | © syui | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | **Part of the ai ecosystem**: ai.gpt, ai.card, ai.log, ai.bot, ai.verse, ai.shell | ||||||
|   | |||||||
| @@ -14,24 +14,35 @@ ai.logをai.gptと連携するためのMCPサーバー設定ガイド | |||||||
|  |  | ||||||
| ## ai.gptでの設定 | ## ai.gptでの設定 | ||||||
|  |  | ||||||
| ai.gptの設定ファイル `~/.config/syui/ai/gpt/config.json` に以下を追加: | ai.logツールはai.gptのMCPサーバーに統合済みです。`config.json`に以下の設定が含まれています: | ||||||
|  |  | ||||||
| ```json | ```json | ||||||
| { | { | ||||||
|   "mcp": { |   "mcp": { | ||||||
|     "enabled": true, |     "enabled": "true", | ||||||
|  |     "auto_detect": "true", | ||||||
|     "servers": { |     "servers": { | ||||||
|       "ai_gpt": {"base_url": "http://localhost:8001"}, |       "ai_gpt": { | ||||||
|       "ai_card": {"base_url": "http://localhost:8000"}, |         "base_url": "http://localhost:8001", | ||||||
|       "ai_log": {"base_url": "http://localhost:8002"} |         "endpoints": { | ||||||
|  |           "log_create_post": "/log_create_post", | ||||||
|  |           "log_list_posts": "/log_list_posts", | ||||||
|  |           "log_build_blog": "/log_build_blog", | ||||||
|  |           "log_get_post": "/log_get_post", | ||||||
|  |           "log_system_status": "/log_system_status", | ||||||
|  |           "log_ai_content": "/log_ai_content" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## 利用可能なMCPツール | **重要**: ai.logツールを使用するには、ai.logディレクトリが `./log/` に存在し、ai.logのMCPサーバーがポート8002で稼働している必要があります。 | ||||||
|  |  | ||||||
| ### 1. create_blog_post | ## 利用可能なMCPツール(ai.gpt統合版) | ||||||
|  |  | ||||||
|  | ### 1. log_create_post | ||||||
| 新しいブログ記事を作成します。 | 新しいブログ記事を作成します。 | ||||||
|  |  | ||||||
| **パラメータ**: | **パラメータ**: | ||||||
| @@ -42,34 +53,45 @@ ai.gptの設定ファイル `~/.config/syui/ai/gpt/config.json` に以下を追 | |||||||
|  |  | ||||||
| **使用例**: | **使用例**: | ||||||
| ```python | ```python | ||||||
| # ai.gptからの呼び出し例 | # Claude Code/ai.gptから自動呼び出し | ||||||
| result = await mcp_client.call_tool("create_blog_post", { | # "ブログ記事を書いて"という発言で自動トリガー | ||||||
|     "title": "AI統合の新しい可能性", |  | ||||||
|     "content": "# 概要\n\nai.gptとai.logの連携により...", |  | ||||||
|     "tags": ["AI", "技術", "ブログ"] |  | ||||||
| }) |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### 2. list_blog_posts | ### 2. log_list_posts | ||||||
| 既存のブログ記事一覧を取得します。 | 既存のブログ記事一覧を取得します。 | ||||||
|  |  | ||||||
| **パラメータ**: | **パラメータ**: | ||||||
| - `limit` (オプション): 取得件数上限 (デフォルト: 10) | - `limit` (オプション): 取得件数上限 (デフォルト: 10) | ||||||
| - `offset` (オプション): スキップ件数 (デフォルト: 0) | - `offset` (オプション): スキップ件数 (デフォルト: 0) | ||||||
|  |  | ||||||
| ### 3. build_blog | ### 3. log_build_blog | ||||||
| ブログをビルドして静的ファイルを生成します。 | ブログをビルドして静的ファイルを生成します。 | ||||||
|  |  | ||||||
| **パラメータ**: | **パラメータ**: | ||||||
| - `enable_ai` (オプション): AI機能を有効化 | - `enable_ai` (オプション): AI機能を有効化 (デフォルト: true) | ||||||
| - `translate` (オプション): 自動翻訳を有効化 | - `translate` (オプション): 自動翻訳を有効化 (デフォルト: false) | ||||||
|  |  | ||||||
| ### 4. get_post_content | ### 4. log_get_post | ||||||
| 指定したスラッグの記事内容を取得します。 | 指定したスラッグの記事内容を取得します。 | ||||||
|  |  | ||||||
| **パラメータ**: | **パラメータ**: | ||||||
| - `slug` (必須): 記事のスラッグ | - `slug` (必須): 記事のスラッグ | ||||||
|  |  | ||||||
|  | ### 5. log_system_status | ||||||
|  | ai.logシステムの状態を確認します。 | ||||||
|  |  | ||||||
|  | ### 6. log_ai_content ⭐ NEW | ||||||
|  | AI記憶システムと連携して自動でブログ記事を生成・投稿します。 | ||||||
|  |  | ||||||
|  | **パラメータ**: | ||||||
|  | - `user_id` (必須): ユーザーID | ||||||
|  | - `topic` (オプション): 記事のトピック (デフォルト: "daily thoughts") | ||||||
|  |  | ||||||
|  | **機能**: | ||||||
|  | - ai.gptの記憶システムから関連する思い出を取得 | ||||||
|  | - AI技術で記憶をブログ記事に変換 | ||||||
|  | - 自動でai.logに投稿 | ||||||
|  |  | ||||||
| ## ai.gptからの連携パターン | ## ai.gptからの連携パターン | ||||||
|  |  | ||||||
| ### 記事の自動投稿 | ### 記事の自動投稿 | ||||||
|   | |||||||
							
								
								
									
										313
									
								
								src/analyzer/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								src/analyzer/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | |||||||
|  | pub mod rust_analyzer; | ||||||
|  |  | ||||||
|  | use anyhow::Result; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::path::{Path, PathBuf}; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct ProjectInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub description: Option<String>, | ||||||
|  |     pub version: String, | ||||||
|  |     pub authors: Vec<String>, | ||||||
|  |     pub license: Option<String>, | ||||||
|  |     pub dependencies: HashMap<String, String>, | ||||||
|  |     pub modules: Vec<ModuleInfo>, | ||||||
|  |     pub structure: ProjectStructure, | ||||||
|  |     pub metrics: ProjectMetrics, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct ModuleInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub path: PathBuf, | ||||||
|  |     pub functions: Vec<FunctionInfo>, | ||||||
|  |     pub structs: Vec<StructInfo>, | ||||||
|  |     pub enums: Vec<EnumInfo>, | ||||||
|  |     pub traits: Vec<TraitInfo>, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct FunctionInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub visibility: String, | ||||||
|  |     pub is_async: bool, | ||||||
|  |     pub parameters: Vec<Parameter>, | ||||||
|  |     pub return_type: Option<String>, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  |     pub line_number: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct Parameter { | ||||||
|  |     pub name: String, | ||||||
|  |     pub param_type: String, | ||||||
|  |     pub is_mutable: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct StructInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub visibility: String, | ||||||
|  |     pub fields: Vec<FieldInfo>, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  |     pub line_number: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct FieldInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub field_type: String, | ||||||
|  |     pub visibility: String, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct EnumInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub visibility: String, | ||||||
|  |     pub variants: Vec<VariantInfo>, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  |     pub line_number: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct VariantInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub fields: Vec<FieldInfo>, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct TraitInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub visibility: String, | ||||||
|  |     pub methods: Vec<FunctionInfo>, | ||||||
|  |     pub docs: Option<String>, | ||||||
|  |     pub line_number: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct ProjectStructure { | ||||||
|  |     pub directories: Vec<DirectoryInfo>, | ||||||
|  |     pub files: Vec<FileInfo>, | ||||||
|  |     pub dependency_graph: HashMap<String, Vec<String>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct DirectoryInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub path: PathBuf, | ||||||
|  |     pub file_count: usize, | ||||||
|  |     pub subdirectories: Vec<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct FileInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub path: PathBuf, | ||||||
|  |     pub language: String, | ||||||
|  |     pub lines_of_code: usize, | ||||||
|  |     pub is_test: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct ProjectMetrics { | ||||||
|  |     pub total_lines: usize, | ||||||
|  |     pub total_files: usize, | ||||||
|  |     pub test_files: usize, | ||||||
|  |     pub dependency_count: usize, | ||||||
|  |     pub complexity_score: f32, | ||||||
|  |     pub test_coverage: Option<f32>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct ApiInfo { | ||||||
|  |     pub modules: Vec<ModuleInfo>, | ||||||
|  |     pub public_functions: Vec<FunctionInfo>, | ||||||
|  |     pub public_structs: Vec<StructInfo>, | ||||||
|  |     pub public_enums: Vec<EnumInfo>, | ||||||
|  |     pub public_traits: Vec<TraitInfo>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct CodeAnalyzer { | ||||||
|  |     rust_analyzer: rust_analyzer::RustAnalyzer, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl CodeAnalyzer { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             rust_analyzer: rust_analyzer::RustAnalyzer::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn analyze_project(&self, path: &Path) -> Result<ProjectInfo> { | ||||||
|  |         println!("  🔍 Analyzing project at: {}", path.display()); | ||||||
|  |          | ||||||
|  |         // Check if this is a Rust project | ||||||
|  |         let cargo_toml = path.join("Cargo.toml"); | ||||||
|  |         if cargo_toml.exists() { | ||||||
|  |             return self.rust_analyzer.analyze_project(path); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // For now, only support Rust projects | ||||||
|  |         anyhow::bail!("Only Rust projects are currently supported"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn analyze_api(&self, path: &Path) -> Result<ApiInfo> { | ||||||
|  |         println!("  📚 Analyzing API at: {}", path.display()); | ||||||
|  |          | ||||||
|  |         let project_info = self.analyze_project(path.parent().unwrap_or(path))?; | ||||||
|  |          | ||||||
|  |         // Extract only public items | ||||||
|  |         let mut public_functions = Vec::new(); | ||||||
|  |         let mut public_structs = Vec::new(); | ||||||
|  |         let mut public_enums = Vec::new(); | ||||||
|  |         let mut public_traits = Vec::new(); | ||||||
|  |          | ||||||
|  |         for module in &project_info.modules { | ||||||
|  |             for func in &module.functions { | ||||||
|  |                 if func.visibility == "pub" { | ||||||
|  |                     public_functions.push(func.clone()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             for struct_info in &module.structs { | ||||||
|  |                 if struct_info.visibility == "pub" { | ||||||
|  |                     public_structs.push(struct_info.clone()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             for enum_info in &module.enums { | ||||||
|  |                 if enum_info.visibility == "pub" { | ||||||
|  |                     public_enums.push(enum_info.clone()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             for trait_info in &module.traits { | ||||||
|  |                 if trait_info.visibility == "pub" { | ||||||
|  |                     public_traits.push(trait_info.clone()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(ApiInfo { | ||||||
|  |             modules: project_info.modules, | ||||||
|  |             public_functions, | ||||||
|  |             public_structs, | ||||||
|  |             public_enums, | ||||||
|  |             public_traits, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn analyze_structure(&self, path: &Path, include_deps: bool) -> Result<ProjectStructure> { | ||||||
|  |         println!("  🏗️  Analyzing structure at: {}", path.display()); | ||||||
|  |          | ||||||
|  |         let mut directories = Vec::new(); | ||||||
|  |         let mut files = Vec::new(); | ||||||
|  |         let mut dependency_graph = HashMap::new(); | ||||||
|  |          | ||||||
|  |         self.walk_directory(path, &mut directories, &mut files)?; | ||||||
|  |          | ||||||
|  |         if include_deps { | ||||||
|  |             dependency_graph = self.analyze_dependencies(path)?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(ProjectStructure { | ||||||
|  |             directories, | ||||||
|  |             files, | ||||||
|  |             dependency_graph, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn walk_directory( | ||||||
|  |         &self, | ||||||
|  |         path: &Path, | ||||||
|  |         directories: &mut Vec<DirectoryInfo>, | ||||||
|  |         files: &mut Vec<FileInfo>, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         use walkdir::WalkDir; | ||||||
|  |          | ||||||
|  |         let walker = WalkDir::new(path) | ||||||
|  |             .into_iter() | ||||||
|  |             .filter_entry(|e| { | ||||||
|  |                 let name = e.file_name().to_string_lossy(); | ||||||
|  |                 // Skip hidden files and common build/cache directories | ||||||
|  |                 !name.starts_with('.')  | ||||||
|  |                     && name != "target"  | ||||||
|  |                     && name != "node_modules" | ||||||
|  |                     && name != "dist" | ||||||
|  |             }); | ||||||
|  |          | ||||||
|  |         for entry in walker { | ||||||
|  |             let entry = entry?; | ||||||
|  |             let path = entry.path(); | ||||||
|  |             let relative_path = path.strip_prefix(path.ancestors().last().unwrap())?; | ||||||
|  |              | ||||||
|  |             if entry.file_type().is_dir() { | ||||||
|  |                 let file_count = std::fs::read_dir(path)? | ||||||
|  |                     .filter_map(|e| e.ok()) | ||||||
|  |                     .filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false)) | ||||||
|  |                     .count(); | ||||||
|  |                  | ||||||
|  |                 let subdirectories = std::fs::read_dir(path)? | ||||||
|  |                     .filter_map(|e| e.ok()) | ||||||
|  |                     .filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) | ||||||
|  |                     .map(|e| e.file_name().to_string_lossy().to_string()) | ||||||
|  |                     .collect(); | ||||||
|  |                  | ||||||
|  |                 directories.push(DirectoryInfo { | ||||||
|  |                     name: path.file_name().unwrap().to_string_lossy().to_string(), | ||||||
|  |                     path: relative_path.to_path_buf(), | ||||||
|  |                     file_count, | ||||||
|  |                     subdirectories, | ||||||
|  |                 }); | ||||||
|  |             } else if entry.file_type().is_file() { | ||||||
|  |                 let language = self.detect_language(path); | ||||||
|  |                 let lines_of_code = self.count_lines(path)?; | ||||||
|  |                 let is_test = self.is_test_file(path); | ||||||
|  |                  | ||||||
|  |                 files.push(FileInfo { | ||||||
|  |                     name: path.file_name().unwrap().to_string_lossy().to_string(), | ||||||
|  |                     path: relative_path.to_path_buf(), | ||||||
|  |                     language, | ||||||
|  |                     lines_of_code, | ||||||
|  |                     is_test, | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn detect_language(&self, path: &Path) -> String { | ||||||
|  |         match path.extension().and_then(|s| s.to_str()) { | ||||||
|  |             Some("rs") => "rust".to_string(), | ||||||
|  |             Some("py") => "python".to_string(), | ||||||
|  |             Some("js") => "javascript".to_string(), | ||||||
|  |             Some("ts") => "typescript".to_string(), | ||||||
|  |             Some("md") => "markdown".to_string(), | ||||||
|  |             Some("toml") => "toml".to_string(), | ||||||
|  |             Some("json") => "json".to_string(), | ||||||
|  |             Some("yaml") | Some("yml") => "yaml".to_string(), | ||||||
|  |             _ => "unknown".to_string(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn count_lines(&self, path: &Path) -> Result<usize> { | ||||||
|  |         let content = std::fs::read_to_string(path)?; | ||||||
|  |         Ok(content.lines().count()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn is_test_file(&self, path: &Path) -> bool { | ||||||
|  |         let filename = path.file_name().unwrap().to_string_lossy(); | ||||||
|  |         filename.contains("test")  | ||||||
|  |             || filename.starts_with("test_") | ||||||
|  |             || path.to_string_lossy().contains("/tests/") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn analyze_dependencies(&self, _path: &Path) -> Result<HashMap<String, Vec<String>>> { | ||||||
|  |         // For now, just return empty dependencies | ||||||
|  |         // TODO: Implement actual dependency analysis | ||||||
|  |         Ok(HashMap::new()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										512
									
								
								src/analyzer/rust_analyzer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										512
									
								
								src/analyzer/rust_analyzer.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,512 @@ | |||||||
|  | use anyhow::Result; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::path::Path; | ||||||
|  | use syn::{visit::Visit, ItemEnum, ItemFn, ItemStruct, ItemTrait, Visibility}; | ||||||
|  |  | ||||||
|  | use super::*; | ||||||
|  |  | ||||||
|  | pub struct RustAnalyzer; | ||||||
|  |  | ||||||
|  | impl RustAnalyzer { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn analyze_project(&self, path: &Path) -> Result<ProjectInfo> { | ||||||
|  |         // Parse Cargo.toml | ||||||
|  |         let cargo_toml_path = path.join("Cargo.toml"); | ||||||
|  |         let cargo_content = std::fs::read_to_string(&cargo_toml_path)?; | ||||||
|  |         let cargo_toml: toml::Value = toml::from_str(&cargo_content)?; | ||||||
|  |          | ||||||
|  |         let package = cargo_toml.get("package").unwrap(); | ||||||
|  |         let name = package.get("name").unwrap().as_str().unwrap().to_string(); | ||||||
|  |         let description = package.get("description").map(|v| v.as_str().unwrap().to_string()); | ||||||
|  |         let version = package.get("version").unwrap().as_str().unwrap().to_string(); | ||||||
|  |         let authors = package | ||||||
|  |             .get("authors") | ||||||
|  |             .map(|v| { | ||||||
|  |                 v.as_array() | ||||||
|  |                     .unwrap() | ||||||
|  |                     .iter() | ||||||
|  |                     .map(|a| a.as_str().unwrap().to_string()) | ||||||
|  |                     .collect() | ||||||
|  |             }) | ||||||
|  |             .unwrap_or_default(); | ||||||
|  |         let license = package.get("license").map(|v| v.as_str().unwrap().to_string()); | ||||||
|  |  | ||||||
|  |         // Parse dependencies | ||||||
|  |         let dependencies = self.parse_dependencies(&cargo_toml)?; | ||||||
|  |  | ||||||
|  |         // Analyze source code | ||||||
|  |         let src_path = path.join("src"); | ||||||
|  |         let modules = self.analyze_modules(&src_path)?; | ||||||
|  |  | ||||||
|  |         // Calculate metrics | ||||||
|  |         let metrics = self.calculate_metrics(&modules, &dependencies); | ||||||
|  |  | ||||||
|  |         // Analyze structure | ||||||
|  |         let structure = self.analyze_project_structure(path)?; | ||||||
|  |  | ||||||
|  |         Ok(ProjectInfo { | ||||||
|  |             name, | ||||||
|  |             description, | ||||||
|  |             version, | ||||||
|  |             authors, | ||||||
|  |             license, | ||||||
|  |             dependencies, | ||||||
|  |             modules, | ||||||
|  |             structure, | ||||||
|  |             metrics, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn parse_dependencies(&self, cargo_toml: &toml::Value) -> Result<HashMap<String, String>> { | ||||||
|  |         let mut dependencies = HashMap::new(); | ||||||
|  |  | ||||||
|  |         if let Some(deps) = cargo_toml.get("dependencies") { | ||||||
|  |             if let Some(deps_table) = deps.as_table() { | ||||||
|  |                 for (name, value) in deps_table { | ||||||
|  |                     let version = match value { | ||||||
|  |                         toml::Value::String(v) => v.clone(), | ||||||
|  |                         toml::Value::Table(t) => { | ||||||
|  |                             t.get("version") | ||||||
|  |                                 .and_then(|v| v.as_str()) | ||||||
|  |                                 .unwrap_or("*") | ||||||
|  |                                 .to_string() | ||||||
|  |                         } | ||||||
|  |                         _ => "*".to_string(), | ||||||
|  |                     }; | ||||||
|  |                     dependencies.insert(name.clone(), version); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(dependencies) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn analyze_modules(&self, src_path: &Path) -> Result<Vec<ModuleInfo>> { | ||||||
|  |         let mut modules = Vec::new(); | ||||||
|  |  | ||||||
|  |         if !src_path.exists() { | ||||||
|  |             return Ok(modules); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Walk through all .rs files | ||||||
|  |         for entry in walkdir::WalkDir::new(src_path) { | ||||||
|  |             let entry = entry?; | ||||||
|  |             if entry.file_type().is_file() { | ||||||
|  |                 if let Some(extension) = entry.path().extension() { | ||||||
|  |                     if extension == "rs" { | ||||||
|  |                         if let Ok(module) = self.analyze_rust_file(entry.path()) { | ||||||
|  |                             modules.push(module); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(modules) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn analyze_rust_file(&self, file_path: &Path) -> Result<ModuleInfo> { | ||||||
|  |         let content = std::fs::read_to_string(file_path)?; | ||||||
|  |         let syntax_tree = syn::parse_file(&content)?; | ||||||
|  |  | ||||||
|  |         let mut visitor = RustVisitor::new(); | ||||||
|  |         visitor.visit_file(&syntax_tree); | ||||||
|  |  | ||||||
|  |         let module_name = file_path | ||||||
|  |             .file_stem() | ||||||
|  |             .unwrap() | ||||||
|  |             .to_string_lossy() | ||||||
|  |             .to_string(); | ||||||
|  |  | ||||||
|  |         // Extract module-level documentation | ||||||
|  |         let docs = self.extract_module_docs(&content); | ||||||
|  |  | ||||||
|  |         Ok(ModuleInfo { | ||||||
|  |             name: module_name, | ||||||
|  |             path: file_path.to_path_buf(), | ||||||
|  |             functions: visitor.functions, | ||||||
|  |             structs: visitor.structs, | ||||||
|  |             enums: visitor.enums, | ||||||
|  |             traits: visitor.traits, | ||||||
|  |             docs, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn extract_module_docs(&self, content: &str) -> Option<String> { | ||||||
|  |         let lines: Vec<&str> = content.lines().collect(); | ||||||
|  |         let mut doc_lines = Vec::new(); | ||||||
|  |         let mut in_module_doc = false; | ||||||
|  |  | ||||||
|  |         for line in lines { | ||||||
|  |             let trimmed = line.trim(); | ||||||
|  |             if trimmed.starts_with("//!") { | ||||||
|  |                 in_module_doc = true; | ||||||
|  |                 doc_lines.push(trimmed.trim_start_matches("//!").trim()); | ||||||
|  |             } else if trimmed.starts_with("/*!") { | ||||||
|  |                 in_module_doc = true; | ||||||
|  |                 let content = trimmed.trim_start_matches("/*!").trim_end_matches("*/").trim(); | ||||||
|  |                 doc_lines.push(content); | ||||||
|  |             } else if in_module_doc && !trimmed.is_empty() && !trimmed.starts_with("//") { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if doc_lines.is_empty() { | ||||||
|  |             None | ||||||
|  |         } else { | ||||||
|  |             Some(doc_lines.join("\n")) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn calculate_metrics(&self, modules: &[ModuleInfo], dependencies: &HashMap<String, String>) -> ProjectMetrics { | ||||||
|  |         let total_lines = modules.iter().map(|m| { | ||||||
|  |             std::fs::read_to_string(&m.path) | ||||||
|  |                 .map(|content| content.lines().count()) | ||||||
|  |                 .unwrap_or(0) | ||||||
|  |         }).sum(); | ||||||
|  |  | ||||||
|  |         let total_files = modules.len(); | ||||||
|  |         let test_files = modules.iter().filter(|m| { | ||||||
|  |             m.name.contains("test") || m.path.to_string_lossy().contains("/tests/") | ||||||
|  |         }).count(); | ||||||
|  |  | ||||||
|  |         let dependency_count = dependencies.len(); | ||||||
|  |  | ||||||
|  |         // Simple complexity calculation based on number of functions and structs | ||||||
|  |         let complexity_score = modules.iter().map(|m| { | ||||||
|  |             (m.functions.len() + m.structs.len() + m.enums.len() + m.traits.len()) as f32 | ||||||
|  |         }).sum::<f32>() / modules.len().max(1) as f32; | ||||||
|  |  | ||||||
|  |         ProjectMetrics { | ||||||
|  |             total_lines, | ||||||
|  |             total_files, | ||||||
|  |             test_files, | ||||||
|  |             dependency_count, | ||||||
|  |             complexity_score, | ||||||
|  |             test_coverage: None, // TODO: Implement test coverage calculation | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn analyze_project_structure(&self, path: &Path) -> Result<ProjectStructure> { | ||||||
|  |         let mut directories = Vec::new(); | ||||||
|  |         let mut files = Vec::new(); | ||||||
|  |  | ||||||
|  |         self.walk_directory(path, &mut directories, &mut files)?; | ||||||
|  |  | ||||||
|  |         Ok(ProjectStructure { | ||||||
|  |             directories, | ||||||
|  |             files, | ||||||
|  |             dependency_graph: HashMap::new(), // TODO: Implement dependency graph | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn walk_directory( | ||||||
|  |         &self, | ||||||
|  |         path: &Path, | ||||||
|  |         directories: &mut Vec<DirectoryInfo>, | ||||||
|  |         files: &mut Vec<FileInfo>, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         for entry in walkdir::WalkDir::new(path).max_depth(3) { | ||||||
|  |             let entry = entry?; | ||||||
|  |             let relative_path = entry.path().strip_prefix(path)?; | ||||||
|  |  | ||||||
|  |             if entry.file_type().is_dir() && relative_path != Path::new("") { | ||||||
|  |                 let file_count = std::fs::read_dir(entry.path())? | ||||||
|  |                     .filter_map(|e| e.ok()) | ||||||
|  |                     .filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false)) | ||||||
|  |                     .count(); | ||||||
|  |  | ||||||
|  |                 let subdirectories = std::fs::read_dir(entry.path())? | ||||||
|  |                     .filter_map(|e| e.ok()) | ||||||
|  |                     .filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) | ||||||
|  |                     .map(|e| e.file_name().to_string_lossy().to_string()) | ||||||
|  |                     .collect(); | ||||||
|  |  | ||||||
|  |                 directories.push(DirectoryInfo { | ||||||
|  |                     name: entry.path().file_name().unwrap().to_string_lossy().to_string(), | ||||||
|  |                     path: relative_path.to_path_buf(), | ||||||
|  |                     file_count, | ||||||
|  |                     subdirectories, | ||||||
|  |                 }); | ||||||
|  |             } else if entry.file_type().is_file() { | ||||||
|  |                 let language = match entry.path().extension().and_then(|s| s.to_str()) { | ||||||
|  |                     Some("rs") => "rust".to_string(), | ||||||
|  |                     Some("toml") => "toml".to_string(), | ||||||
|  |                     Some("md") => "markdown".to_string(), | ||||||
|  |                     _ => "unknown".to_string(), | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 let lines_of_code = std::fs::read_to_string(entry.path()) | ||||||
|  |                     .map(|content| content.lines().count()) | ||||||
|  |                     .unwrap_or(0); | ||||||
|  |  | ||||||
|  |                 let is_test = entry.path().to_string_lossy().contains("test"); | ||||||
|  |  | ||||||
|  |                 files.push(FileInfo { | ||||||
|  |                     name: entry.path().file_name().unwrap().to_string_lossy().to_string(), | ||||||
|  |                     path: relative_path.to_path_buf(), | ||||||
|  |                     language, | ||||||
|  |                     lines_of_code, | ||||||
|  |                     is_test, | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct RustVisitor { | ||||||
|  |     functions: Vec<FunctionInfo>, | ||||||
|  |     structs: Vec<StructInfo>, | ||||||
|  |     enums: Vec<EnumInfo>, | ||||||
|  |     traits: Vec<TraitInfo>, | ||||||
|  |     current_line: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl RustVisitor { | ||||||
|  |     fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             functions: Vec::new(), | ||||||
|  |             structs: Vec::new(), | ||||||
|  |             enums: Vec::new(), | ||||||
|  |             traits: Vec::new(), | ||||||
|  |             current_line: 1, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn visibility_to_string(&self, vis: &Visibility) -> String { | ||||||
|  |         match vis { | ||||||
|  |             Visibility::Public(_) => "pub".to_string(), | ||||||
|  |             Visibility::Restricted(_) => "pub(restricted)".to_string(), | ||||||
|  |             Visibility::Inherited => "private".to_string(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn extract_docs(&self, attrs: &[syn::Attribute]) -> Option<String> { | ||||||
|  |         let mut docs = Vec::new(); | ||||||
|  |         for attr in attrs { | ||||||
|  |             if attr.path().is_ident("doc") { | ||||||
|  |                 if let syn::Meta::NameValue(meta) = &attr.meta { | ||||||
|  |                     if let syn::Expr::Lit(expr_lit) = &meta.value { | ||||||
|  |                         if let syn::Lit::Str(lit_str) = &expr_lit.lit { | ||||||
|  |                             docs.push(lit_str.value()); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if docs.is_empty() { | ||||||
|  |             None | ||||||
|  |         } else { | ||||||
|  |             Some(docs.join("\n")) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'ast> Visit<'ast> for RustVisitor { | ||||||
|  |     fn visit_item_fn(&mut self, node: &'ast ItemFn) { | ||||||
|  |         let name = node.sig.ident.to_string(); | ||||||
|  |         let visibility = self.visibility_to_string(&node.vis); | ||||||
|  |         let is_async = node.sig.asyncness.is_some(); | ||||||
|  |          | ||||||
|  |         let parameters = node.sig.inputs.iter().map(|input| { | ||||||
|  |             match input { | ||||||
|  |                 syn::FnArg::Receiver(_) => Parameter { | ||||||
|  |                     name: "self".to_string(), | ||||||
|  |                     param_type: "Self".to_string(), | ||||||
|  |                     is_mutable: false, | ||||||
|  |                 }, | ||||||
|  |                 syn::FnArg::Typed(typed) => { | ||||||
|  |                     let name = match &*typed.pat { | ||||||
|  |                         syn::Pat::Ident(ident) => ident.ident.to_string(), | ||||||
|  |                         _ => "unknown".to_string(), | ||||||
|  |                     }; | ||||||
|  |                     Parameter { | ||||||
|  |                         name, | ||||||
|  |                         param_type: quote::quote!(#typed.ty).to_string(), | ||||||
|  |                         is_mutable: false, // TODO: Detect mutability | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }).collect(); | ||||||
|  |  | ||||||
|  |         let return_type = match &node.sig.output { | ||||||
|  |             syn::ReturnType::Default => None, | ||||||
|  |             syn::ReturnType::Type(_, ty) => Some(quote::quote!(#ty).to_string()), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let docs = self.extract_docs(&node.attrs); | ||||||
|  |  | ||||||
|  |         self.functions.push(FunctionInfo { | ||||||
|  |             name, | ||||||
|  |             visibility, | ||||||
|  |             is_async, | ||||||
|  |             parameters, | ||||||
|  |             return_type, | ||||||
|  |             docs, | ||||||
|  |             line_number: self.current_line, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         syn::visit::visit_item_fn(self, node); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn visit_item_struct(&mut self, node: &'ast ItemStruct) { | ||||||
|  |         let name = node.ident.to_string(); | ||||||
|  |         let visibility = self.visibility_to_string(&node.vis); | ||||||
|  |         let docs = self.extract_docs(&node.attrs); | ||||||
|  |  | ||||||
|  |         let fields = match &node.fields { | ||||||
|  |             syn::Fields::Named(fields) => { | ||||||
|  |                 fields.named.iter().map(|field| { | ||||||
|  |                     FieldInfo { | ||||||
|  |                         name: field.ident.as_ref().unwrap().to_string(), | ||||||
|  |                         field_type: quote::quote!(#field.ty).to_string(), | ||||||
|  |                         visibility: self.visibility_to_string(&field.vis), | ||||||
|  |                         docs: self.extract_docs(&field.attrs), | ||||||
|  |                     } | ||||||
|  |                 }).collect() | ||||||
|  |             } | ||||||
|  |             syn::Fields::Unnamed(fields) => { | ||||||
|  |                 fields.unnamed.iter().enumerate().map(|(i, field)| { | ||||||
|  |                     FieldInfo { | ||||||
|  |                         name: format!("field_{}", i), | ||||||
|  |                         field_type: quote::quote!(#field.ty).to_string(), | ||||||
|  |                         visibility: self.visibility_to_string(&field.vis), | ||||||
|  |                         docs: self.extract_docs(&field.attrs), | ||||||
|  |                     } | ||||||
|  |                 }).collect() | ||||||
|  |             } | ||||||
|  |             syn::Fields::Unit => Vec::new(), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         self.structs.push(StructInfo { | ||||||
|  |             name, | ||||||
|  |             visibility, | ||||||
|  |             fields, | ||||||
|  |             docs, | ||||||
|  |             line_number: self.current_line, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         syn::visit::visit_item_struct(self, node); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn visit_item_enum(&mut self, node: &'ast ItemEnum) { | ||||||
|  |         let name = node.ident.to_string(); | ||||||
|  |         let visibility = self.visibility_to_string(&node.vis); | ||||||
|  |         let docs = self.extract_docs(&node.attrs); | ||||||
|  |  | ||||||
|  |         let variants = node.variants.iter().map(|variant| { | ||||||
|  |             let variant_name = variant.ident.to_string(); | ||||||
|  |             let variant_docs = self.extract_docs(&variant.attrs); | ||||||
|  |  | ||||||
|  |             let fields = match &variant.fields { | ||||||
|  |                 syn::Fields::Named(fields) => { | ||||||
|  |                     fields.named.iter().map(|field| { | ||||||
|  |                         FieldInfo { | ||||||
|  |                             name: field.ident.as_ref().unwrap().to_string(), | ||||||
|  |                             field_type: quote::quote!(#field.ty).to_string(), | ||||||
|  |                             visibility: self.visibility_to_string(&field.vis), | ||||||
|  |                             docs: self.extract_docs(&field.attrs), | ||||||
|  |                         } | ||||||
|  |                     }).collect() | ||||||
|  |                 } | ||||||
|  |                 syn::Fields::Unnamed(fields) => { | ||||||
|  |                     fields.unnamed.iter().enumerate().map(|(i, field)| { | ||||||
|  |                         FieldInfo { | ||||||
|  |                             name: format!("field_{}", i), | ||||||
|  |                             field_type: quote::quote!(#field.ty).to_string(), | ||||||
|  |                             visibility: self.visibility_to_string(&field.vis), | ||||||
|  |                             docs: self.extract_docs(&field.attrs), | ||||||
|  |                         } | ||||||
|  |                     }).collect() | ||||||
|  |                 } | ||||||
|  |                 syn::Fields::Unit => Vec::new(), | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             VariantInfo { | ||||||
|  |                 name: variant_name, | ||||||
|  |                 fields, | ||||||
|  |                 docs: variant_docs, | ||||||
|  |             } | ||||||
|  |         }).collect(); | ||||||
|  |  | ||||||
|  |         self.enums.push(EnumInfo { | ||||||
|  |             name, | ||||||
|  |             visibility, | ||||||
|  |             variants, | ||||||
|  |             docs, | ||||||
|  |             line_number: self.current_line, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         syn::visit::visit_item_enum(self, node); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn visit_item_trait(&mut self, node: &'ast ItemTrait) { | ||||||
|  |         let name = node.ident.to_string(); | ||||||
|  |         let visibility = self.visibility_to_string(&node.vis); | ||||||
|  |         let docs = self.extract_docs(&node.attrs); | ||||||
|  |  | ||||||
|  |         let methods = node.items.iter().filter_map(|item| { | ||||||
|  |             match item { | ||||||
|  |                 syn::TraitItem::Fn(method) => { | ||||||
|  |                     let method_name = method.sig.ident.to_string(); | ||||||
|  |                     let method_visibility = "pub".to_string(); // Trait methods are inherently public | ||||||
|  |                     let is_async = method.sig.asyncness.is_some(); | ||||||
|  |                      | ||||||
|  |                     let parameters = method.sig.inputs.iter().map(|input| { | ||||||
|  |                         match input { | ||||||
|  |                             syn::FnArg::Receiver(_) => Parameter { | ||||||
|  |                                 name: "self".to_string(), | ||||||
|  |                                 param_type: "Self".to_string(), | ||||||
|  |                                 is_mutable: false, | ||||||
|  |                             }, | ||||||
|  |                             syn::FnArg::Typed(typed) => { | ||||||
|  |                                 let name = match &*typed.pat { | ||||||
|  |                                     syn::Pat::Ident(ident) => ident.ident.to_string(), | ||||||
|  |                                     _ => "unknown".to_string(), | ||||||
|  |                                 }; | ||||||
|  |                                 Parameter { | ||||||
|  |                                     name, | ||||||
|  |                                     param_type: quote::quote!(#typed.ty).to_string(), | ||||||
|  |                                     is_mutable: false, | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }).collect(); | ||||||
|  |  | ||||||
|  |                     let return_type = match &method.sig.output { | ||||||
|  |                         syn::ReturnType::Default => None, | ||||||
|  |                         syn::ReturnType::Type(_, ty) => Some(quote::quote!(#ty).to_string()), | ||||||
|  |                     }; | ||||||
|  |  | ||||||
|  |                     let method_docs = self.extract_docs(&method.attrs); | ||||||
|  |  | ||||||
|  |                     Some(FunctionInfo { | ||||||
|  |                         name: method_name, | ||||||
|  |                         visibility: method_visibility, | ||||||
|  |                         is_async, | ||||||
|  |                         parameters, | ||||||
|  |                         return_type, | ||||||
|  |                         docs: method_docs, | ||||||
|  |                         line_number: self.current_line, | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |                 _ => None, | ||||||
|  |             } | ||||||
|  |         }).collect(); | ||||||
|  |  | ||||||
|  |         self.traits.push(TraitInfo { | ||||||
|  |             name, | ||||||
|  |             visibility, | ||||||
|  |             methods, | ||||||
|  |             docs, | ||||||
|  |             line_number: self.current_line, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         syn::visit::visit_item_trait(self, node); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										287
									
								
								src/commands/doc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/commands/doc.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | |||||||
|  | use anyhow::Result; | ||||||
|  | use clap::{Subcommand, Parser}; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use crate::analyzer::CodeAnalyzer; | ||||||
|  | use crate::doc_generator::DocGenerator; | ||||||
|  | use crate::translator::{TranslationConfig, Translator}; | ||||||
|  | use crate::translator::ollama_translator::OllamaTranslator; | ||||||
|  |  | ||||||
|  | #[derive(Parser)] | ||||||
|  | #[command(about = "Generate documentation from code")] | ||||||
|  | pub struct DocCommand { | ||||||
|  |     #[command(subcommand)] | ||||||
|  |     pub action: DocAction, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Subcommand)] | ||||||
|  | pub enum DocAction { | ||||||
|  |     /// Generate README.md from project analysis | ||||||
|  |     Readme { | ||||||
|  |         /// Source directory to analyze | ||||||
|  |         #[arg(long, default_value = ".")] | ||||||
|  |         source: PathBuf, | ||||||
|  |         /// Output file path | ||||||
|  |         #[arg(long, default_value = "README.md")] | ||||||
|  |         output: PathBuf, | ||||||
|  |         /// Include AI-generated insights | ||||||
|  |         #[arg(long)] | ||||||
|  |         with_ai: bool, | ||||||
|  |     }, | ||||||
|  |     /// Generate API documentation | ||||||
|  |     Api { | ||||||
|  |         /// Source directory to analyze | ||||||
|  |         #[arg(long, default_value = "./src")] | ||||||
|  |         source: PathBuf, | ||||||
|  |         /// Output directory | ||||||
|  |         #[arg(long, default_value = "./docs")] | ||||||
|  |         output: PathBuf, | ||||||
|  |         /// Output format (markdown, html, json) | ||||||
|  |         #[arg(long, default_value = "markdown")] | ||||||
|  |         format: String, | ||||||
|  |     }, | ||||||
|  |     /// Analyze and document project structure | ||||||
|  |     Structure { | ||||||
|  |         /// Source directory to analyze | ||||||
|  |         #[arg(long, default_value = ".")] | ||||||
|  |         source: PathBuf, | ||||||
|  |         /// Output file path | ||||||
|  |         #[arg(long, default_value = "docs/structure.md")] | ||||||
|  |         output: PathBuf, | ||||||
|  |         /// Include dependency graph | ||||||
|  |         #[arg(long)] | ||||||
|  |         include_deps: bool, | ||||||
|  |     }, | ||||||
|  |     /// Generate changelog from git commits | ||||||
|  |     Changelog { | ||||||
|  |         /// Start from this commit/tag | ||||||
|  |         #[arg(long)] | ||||||
|  |         from: Option<String>, | ||||||
|  |         /// End at this commit/tag | ||||||
|  |         #[arg(long)] | ||||||
|  |         to: Option<String>, | ||||||
|  |         /// Output file path | ||||||
|  |         #[arg(long, default_value = "CHANGELOG.md")] | ||||||
|  |         output: PathBuf, | ||||||
|  |         /// Include AI explanations for changes | ||||||
|  |         #[arg(long)] | ||||||
|  |         explain_changes: bool, | ||||||
|  |     }, | ||||||
|  |     /// Translate documentation using Ollama | ||||||
|  |     Translate { | ||||||
|  |         /// Input file path | ||||||
|  |         #[arg(long)] | ||||||
|  |         input: PathBuf, | ||||||
|  |         /// Target language (en, ja, zh, ko, es) | ||||||
|  |         #[arg(long)] | ||||||
|  |         target_lang: String, | ||||||
|  |         /// Source language (auto-detect if not specified) | ||||||
|  |         #[arg(long)] | ||||||
|  |         source_lang: Option<String>, | ||||||
|  |         /// Output file path (auto-generated if not specified) | ||||||
|  |         #[arg(long)] | ||||||
|  |         output: Option<PathBuf>, | ||||||
|  |         /// Ollama model to use | ||||||
|  |         #[arg(long, default_value = "qwen2.5:latest")] | ||||||
|  |         model: String, | ||||||
|  |         /// Ollama endpoint | ||||||
|  |         #[arg(long, default_value = "http://localhost:11434")] | ||||||
|  |         ollama_endpoint: String, | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl DocCommand { | ||||||
|  |     pub async fn execute(self, base_path: PathBuf) -> Result<()> { | ||||||
|  |         match self.action { | ||||||
|  |             DocAction::Readme { ref source, ref output, with_ai } => { | ||||||
|  |                 self.generate_readme(base_path, source.clone(), output.clone(), with_ai).await | ||||||
|  |             } | ||||||
|  |             DocAction::Api { ref source, ref output, ref format } => { | ||||||
|  |                 self.generate_api_docs(base_path, source.clone(), output.clone(), format.clone()).await | ||||||
|  |             } | ||||||
|  |             DocAction::Structure { ref source, ref output, include_deps } => { | ||||||
|  |                 self.analyze_structure(base_path, source.clone(), output.clone(), include_deps).await | ||||||
|  |             } | ||||||
|  |             DocAction::Changelog { ref from, ref to, ref output, explain_changes } => { | ||||||
|  |                 self.generate_changelog(base_path, from.clone(), to.clone(), output.clone(), explain_changes).await | ||||||
|  |             } | ||||||
|  |             DocAction::Translate { ref input, ref target_lang, ref source_lang, ref output, ref model, ref ollama_endpoint } => { | ||||||
|  |                 self.translate_document(input.clone(), target_lang.clone(), source_lang.clone(), output.clone(), model.clone(), ollama_endpoint.clone()).await | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn generate_readme( | ||||||
|  |         &self, | ||||||
|  |         base_path: PathBuf, | ||||||
|  |         source: PathBuf, | ||||||
|  |         output: PathBuf, | ||||||
|  |         with_ai: bool, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         println!("🔍 Analyzing project for README generation..."); | ||||||
|  |          | ||||||
|  |         let analyzer = CodeAnalyzer::new(); | ||||||
|  |         let generator = DocGenerator::new(base_path.clone(), with_ai); | ||||||
|  |          | ||||||
|  |         let project_info = analyzer.analyze_project(&source)?; | ||||||
|  |         let readme_content = generator.generate_readme(&project_info).await?; | ||||||
|  |          | ||||||
|  |         std::fs::write(&output, readme_content)?; | ||||||
|  |          | ||||||
|  |         println!("✅ README generated: {}", output.display()); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn generate_api_docs( | ||||||
|  |         &self, | ||||||
|  |         base_path: PathBuf, | ||||||
|  |         source: PathBuf, | ||||||
|  |         output: PathBuf, | ||||||
|  |         format: String, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         println!("📚 Generating API documentation..."); | ||||||
|  |          | ||||||
|  |         let analyzer = CodeAnalyzer::new(); | ||||||
|  |         let generator = DocGenerator::new(base_path.clone(), true); | ||||||
|  |          | ||||||
|  |         let api_info = analyzer.analyze_api(&source)?; | ||||||
|  |          | ||||||
|  |         match format.as_str() { | ||||||
|  |             "markdown" => { | ||||||
|  |                 let docs = generator.generate_api_markdown(&api_info).await?; | ||||||
|  |                 std::fs::create_dir_all(&output)?; | ||||||
|  |                  | ||||||
|  |                 for (filename, content) in docs { | ||||||
|  |                     let file_path = output.join(filename); | ||||||
|  |                     std::fs::write(&file_path, content)?; | ||||||
|  |                     println!("  📄 Generated: {}", file_path.display()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             "html" => { | ||||||
|  |                 println!("HTML format not yet implemented"); | ||||||
|  |             } | ||||||
|  |             "json" => { | ||||||
|  |                 let json_content = serde_json::to_string_pretty(&api_info)?; | ||||||
|  |                 let file_path = output.join("api.json"); | ||||||
|  |                 std::fs::create_dir_all(&output)?; | ||||||
|  |                 std::fs::write(&file_path, json_content)?; | ||||||
|  |                 println!("  📄 Generated: {}", file_path.display()); | ||||||
|  |             } | ||||||
|  |             _ => { | ||||||
|  |                 anyhow::bail!("Unsupported format: {}", format); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         println!("✅ API documentation generated in: {}", output.display()); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn analyze_structure( | ||||||
|  |         &self, | ||||||
|  |         base_path: PathBuf, | ||||||
|  |         source: PathBuf, | ||||||
|  |         output: PathBuf, | ||||||
|  |         include_deps: bool, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         println!("🏗️  Analyzing project structure..."); | ||||||
|  |          | ||||||
|  |         let analyzer = CodeAnalyzer::new(); | ||||||
|  |         let generator = DocGenerator::new(base_path.clone(), false); | ||||||
|  |          | ||||||
|  |         let structure = analyzer.analyze_structure(&source, include_deps)?; | ||||||
|  |         let structure_doc = generator.generate_structure_doc(&structure).await?; | ||||||
|  |          | ||||||
|  |         // Ensure output directory exists | ||||||
|  |         if let Some(parent) = output.parent() { | ||||||
|  |             std::fs::create_dir_all(parent)?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         std::fs::write(&output, structure_doc)?; | ||||||
|  |          | ||||||
|  |         println!("✅ Structure documentation generated: {}", output.display()); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn generate_changelog( | ||||||
|  |         &self, | ||||||
|  |         base_path: PathBuf, | ||||||
|  |         from: Option<String>, | ||||||
|  |         to: Option<String>, | ||||||
|  |         output: PathBuf, | ||||||
|  |         explain_changes: bool, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         println!("📝 Generating changelog from git history..."); | ||||||
|  |          | ||||||
|  |         let generator = DocGenerator::new(base_path.clone(), explain_changes); | ||||||
|  |         let changelog = generator.generate_changelog(from, to).await?; | ||||||
|  |          | ||||||
|  |         std::fs::write(&output, changelog)?; | ||||||
|  |          | ||||||
|  |         println!("✅ Changelog generated: {}", output.display()); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn translate_document( | ||||||
|  |         &self, | ||||||
|  |         input: PathBuf, | ||||||
|  |         target_lang: String, | ||||||
|  |         source_lang: Option<String>, | ||||||
|  |         output: Option<PathBuf>, | ||||||
|  |         model: String, | ||||||
|  |         ollama_endpoint: String, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         println!("🌍 Translating document with Ollama..."); | ||||||
|  |          | ||||||
|  |         // Read input file | ||||||
|  |         let content = std::fs::read_to_string(&input)?; | ||||||
|  |         println!("📖 Read {} characters from {}", content.len(), input.display()); | ||||||
|  |          | ||||||
|  |         // Setup translation config | ||||||
|  |         let config = TranslationConfig { | ||||||
|  |             source_lang: source_lang.unwrap_or_else(|| { | ||||||
|  |                 // Simple language detection based on content | ||||||
|  |                 if content.chars().any(|c| { | ||||||
|  |                     (c >= '\u{3040}' && c <= '\u{309F}') || // Hiragana | ||||||
|  |                     (c >= '\u{30A0}' && c <= '\u{30FF}') || // Katakana | ||||||
|  |                     (c >= '\u{4E00}' && c <= '\u{9FAF}')    // CJK Unified Ideographs | ||||||
|  |                 }) { | ||||||
|  |                     "ja".to_string() | ||||||
|  |                 } else { | ||||||
|  |                     "en".to_string() | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |             target_lang, | ||||||
|  |             ollama_endpoint, | ||||||
|  |             model, | ||||||
|  |             preserve_code: true, | ||||||
|  |             preserve_links: true, | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         println!("🔧 Translation config: {} → {}", config.source_lang, config.target_lang); | ||||||
|  |         println!("🤖 Using model: {} at {}", config.model, config.ollama_endpoint); | ||||||
|  |          | ||||||
|  |         // Create translator | ||||||
|  |         let translator = OllamaTranslator::new(); | ||||||
|  |          | ||||||
|  |         // Perform translation | ||||||
|  |         let translated = translator.translate_markdown(&content, &config).await?; | ||||||
|  |          | ||||||
|  |         // Determine output path | ||||||
|  |         let output_path = match output { | ||||||
|  |             Some(path) => path, | ||||||
|  |             None => { | ||||||
|  |                 let input_stem = input.file_stem().unwrap().to_string_lossy(); | ||||||
|  |                 let input_ext = input.extension().unwrap_or_default().to_string_lossy(); | ||||||
|  |                 let output_name = format!("{}.{}.{}", input_stem, config.target_lang, input_ext); | ||||||
|  |                 input.parent().unwrap_or_else(|| std::path::Path::new(".")).join(output_name) | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // Write translated content | ||||||
|  |         std::fs::write(&output_path, translated)?; | ||||||
|  |          | ||||||
|  |         println!("✅ Translation completed: {}", output_path.display()); | ||||||
|  |         println!("📝 Language: {} → {}", config.source_lang, config.target_lang); | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -3,3 +3,4 @@ pub mod build; | |||||||
| pub mod new; | pub mod new; | ||||||
| pub mod serve; | pub mod serve; | ||||||
| pub mod clean; | pub mod clean; | ||||||
|  | pub mod doc; | ||||||
							
								
								
									
										235
									
								
								src/doc_generator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								src/doc_generator.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | |||||||
|  | use anyhow::Result; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use crate::analyzer::{ProjectInfo, ApiInfo, ProjectStructure}; | ||||||
|  | use crate::ai::gpt_client::GptClient; | ||||||
|  |  | ||||||
|  | pub struct DocGenerator { | ||||||
|  |     base_path: PathBuf, | ||||||
|  |     ai_enabled: bool, | ||||||
|  |     templates: DocTemplates, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct DocTemplates { | ||||||
|  |     readme_template: String, | ||||||
|  |     api_template: String, | ||||||
|  |     structure_template: String, | ||||||
|  |     changelog_template: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl DocGenerator { | ||||||
|  |     pub fn new(base_path: PathBuf, ai_enabled: bool) -> Self { | ||||||
|  |         let templates = DocTemplates::default(); | ||||||
|  |         Self { | ||||||
|  |             base_path, | ||||||
|  |             ai_enabled, | ||||||
|  |             templates, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn generate_readme(&self, project_info: &ProjectInfo) -> Result<String> { | ||||||
|  |         let mut content = self.templates.readme_template.clone(); | ||||||
|  |          | ||||||
|  |         // Simple template substitution | ||||||
|  |         content = content.replace("{{name}}", &project_info.name); | ||||||
|  |         content = content.replace("{{description}}",  | ||||||
|  |             &project_info.description.as_ref().unwrap_or(&"A Rust project".to_string())); | ||||||
|  |         content = content.replace("{{module_count}}", &project_info.modules.len().to_string()); | ||||||
|  |         content = content.replace("{{total_lines}}", &project_info.metrics.total_lines.to_string()); | ||||||
|  |          | ||||||
|  |         let deps = project_info.dependencies.iter() | ||||||
|  |             .map(|(name, version)| format!("- {}: {}", name, version)) | ||||||
|  |             .collect::<Vec<_>>() | ||||||
|  |             .join("\n"); | ||||||
|  |         content = content.replace("{{dependencies}}", &deps); | ||||||
|  |         content = content.replace("{{license}}",  | ||||||
|  |             &project_info.license.as_ref().unwrap_or(&"MIT".to_string())); | ||||||
|  |          | ||||||
|  |         if self.ai_enabled { | ||||||
|  |             content = self.enhance_with_ai(&content, "readme").await?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(content) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn generate_api_markdown(&self, api_info: &ApiInfo) -> Result<Vec<(String, String)>> { | ||||||
|  |         let mut files = Vec::new(); | ||||||
|  |          | ||||||
|  |         // Generate main API documentation | ||||||
|  |         let main_content = self.templates.api_template.replace("{{content}}", "Generated API Documentation"); | ||||||
|  |         files.push(("api.md".to_string(), main_content)); | ||||||
|  |          | ||||||
|  |         // Generate individual module docs | ||||||
|  |         for module in &api_info.modules { | ||||||
|  |             if !module.functions.is_empty() || !module.structs.is_empty() { | ||||||
|  |                 let module_content = self.generate_module_doc(module).await?; | ||||||
|  |                 files.push((format!("{}.md", module.name), module_content)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(files) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn generate_structure_doc(&self, structure: &ProjectStructure) -> Result<String> { | ||||||
|  |         let content = self.templates.structure_template.replace("{{content}}",  | ||||||
|  |             &format!("Found {} directories and {} files",  | ||||||
|  |                 structure.directories.len(),  | ||||||
|  |                 structure.files.len())); | ||||||
|  |         Ok(content) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn generate_changelog(&self, from: Option<String>, to: Option<String>) -> Result<String> { | ||||||
|  |         let commits = self.get_git_commits(from, to)?; | ||||||
|  |          | ||||||
|  |         let mut content = self.templates.changelog_template.replace("{{content}}",  | ||||||
|  |             &format!("Found {} commits", commits.len())); | ||||||
|  |          | ||||||
|  |         if self.ai_enabled { | ||||||
|  |             content = self.enhance_changelog_with_ai(&content, &commits).await?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(content) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     async fn enhance_with_ai(&self, content: &str, doc_type: &str) -> Result<String> { | ||||||
|  |         if !self.ai_enabled { | ||||||
|  |             return Ok(content.to_string()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let gpt_client = GptClient::new( | ||||||
|  |             std::env::var("OPENAI_API_KEY").unwrap_or_default(), | ||||||
|  |             None, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         let prompt = format!( | ||||||
|  |             "Enhance this {} documentation with additional insights and improve readability:\n\n{}", | ||||||
|  |             doc_type, content | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         match gpt_client.chat("You are a technical writer helping to improve documentation.", &prompt).await { | ||||||
|  |             Ok(enhanced) => Ok(enhanced), | ||||||
|  |             Err(_) => Ok(content.to_string()), // Fallback to original content | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn generate_module_doc(&self, module: &crate::analyzer::ModuleInfo) -> Result<String> { | ||||||
|  |         let mut content = format!("# Module: {}\n\n", module.name); | ||||||
|  |          | ||||||
|  |         if let Some(docs) = &module.docs { | ||||||
|  |             content.push_str(&format!("{}\n\n", docs)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add functions | ||||||
|  |         if !module.functions.is_empty() { | ||||||
|  |             content.push_str("## Functions\n\n"); | ||||||
|  |             for func in &module.functions { | ||||||
|  |                 content.push_str(&self.format_function_doc(func)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add structs | ||||||
|  |         if !module.structs.is_empty() { | ||||||
|  |             content.push_str("## Structs\n\n"); | ||||||
|  |             for struct_info in &module.structs { | ||||||
|  |                 content.push_str(&self.format_struct_doc(struct_info)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(content) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn format_function_doc(&self, func: &crate::analyzer::FunctionInfo) -> String { | ||||||
|  |         let mut doc = format!("### `{}`\n\n", func.name); | ||||||
|  |          | ||||||
|  |         if let Some(docs) = &func.docs { | ||||||
|  |             doc.push_str(&format!("{}\n\n", docs)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         doc.push_str(&format!("**Visibility:** `{}`\n", func.visibility)); | ||||||
|  |          | ||||||
|  |         if func.is_async { | ||||||
|  |             doc.push_str("**Async:** Yes\n"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if !func.parameters.is_empty() { | ||||||
|  |             doc.push_str("\n**Parameters:**\n"); | ||||||
|  |             for param in &func.parameters { | ||||||
|  |                 doc.push_str(&format!("- `{}`: `{}`\n", param.name, param.param_type)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Some(return_type) = &func.return_type { | ||||||
|  |             doc.push_str(&format!("\n**Returns:** `{}`\n", return_type)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         doc.push_str("\n---\n\n"); | ||||||
|  |         doc | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn format_struct_doc(&self, struct_info: &crate::analyzer::StructInfo) -> String { | ||||||
|  |         let mut doc = format!("### `{}`\n\n", struct_info.name); | ||||||
|  |          | ||||||
|  |         if let Some(docs) = &struct_info.docs { | ||||||
|  |             doc.push_str(&format!("{}\n\n", docs)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         doc.push_str(&format!("**Visibility:** `{}`\n\n", struct_info.visibility)); | ||||||
|  |  | ||||||
|  |         if !struct_info.fields.is_empty() { | ||||||
|  |             doc.push_str("**Fields:**\n"); | ||||||
|  |             for field in &struct_info.fields { | ||||||
|  |                 doc.push_str(&format!("- `{}`: `{}` ({})\n", field.name, field.field_type, field.visibility)); | ||||||
|  |                 if let Some(field_docs) = &field.docs { | ||||||
|  |                     doc.push_str(&format!("  - {}\n", field_docs)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         doc.push_str("\n---\n\n"); | ||||||
|  |         doc | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn enhance_changelog_with_ai(&self, content: &str, _commits: &[GitCommit]) -> Result<String> { | ||||||
|  |         // TODO: Implement AI-enhanced changelog generation | ||||||
|  |         Ok(content.to_string()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_git_commits(&self, _from: Option<String>, _to: Option<String>) -> Result<Vec<GitCommit>> { | ||||||
|  |         // TODO: Implement git history parsing | ||||||
|  |         Ok(vec![]) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct GitCommit { | ||||||
|  |     pub hash: String, | ||||||
|  |     pub message: String, | ||||||
|  |     pub author: String, | ||||||
|  |     pub date: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl DocTemplates { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             readme_template: r#"# {{name}} | ||||||
|  |  | ||||||
|  | {{description}} | ||||||
|  |  | ||||||
|  | ## Overview | ||||||
|  |  | ||||||
|  | This project contains {{module_count}} modules with {{total_lines}} lines of code. | ||||||
|  |  | ||||||
|  | ## Dependencies | ||||||
|  |  | ||||||
|  | {{dependencies}} | ||||||
|  |  | ||||||
|  | ## License | ||||||
|  |  | ||||||
|  | {{license}} | ||||||
|  | "#.to_string(), | ||||||
|  |             api_template: "# API Documentation\n\n{{content}}".to_string(), | ||||||
|  |             structure_template: "# Project Structure\n\n{{content}}".to_string(), | ||||||
|  |             changelog_template: "# Changelog\n\n{{content}}".to_string(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,10 +2,13 @@ use anyhow::Result; | |||||||
| use clap::{Parser, Subcommand}; | use clap::{Parser, Subcommand}; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
|  |  | ||||||
|  | mod analyzer; | ||||||
| mod commands; | mod commands; | ||||||
|  | mod doc_generator; | ||||||
| mod generator; | mod generator; | ||||||
| mod markdown; | mod markdown; | ||||||
| mod template; | mod template; | ||||||
|  | mod translator; | ||||||
| mod config; | mod config; | ||||||
| mod ai; | mod ai; | ||||||
| mod atproto; | mod atproto; | ||||||
| @@ -59,6 +62,8 @@ enum Commands { | |||||||
|         #[arg(default_value = ".")] |         #[arg(default_value = ".")] | ||||||
|         path: PathBuf, |         path: PathBuf, | ||||||
|     }, |     }, | ||||||
|  |     /// Generate documentation from code | ||||||
|  |     Doc(commands::doc::DocCommand), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| @@ -86,6 +91,9 @@ async fn main() -> Result<()> { | |||||||
|             let server = McpServer::new(path); |             let server = McpServer::new(path); | ||||||
|             server.serve(port).await?; |             server.serve(port).await?; | ||||||
|         } |         } | ||||||
|  |         Commands::Doc(doc_cmd) => { | ||||||
|  |             doc_cmd.execute(std::env::current_dir()?).await?; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
|   | |||||||
| @@ -113,6 +113,12 @@ async fn call_tool( | |||||||
|                 .ok_or(StatusCode::BAD_REQUEST)?; |                 .ok_or(StatusCode::BAD_REQUEST)?; | ||||||
|             state.blog_tools.get_post_content(slug).await |             state.blog_tools.get_post_content(slug).await | ||||||
|         } |         } | ||||||
|  |         "translate_document" => { | ||||||
|  |             state.blog_tools.translate_document(arguments).await | ||||||
|  |         } | ||||||
|  |         "generate_documentation" => { | ||||||
|  |             state.blog_tools.generate_documentation(arguments).await | ||||||
|  |         } | ||||||
|         _ => { |         _ => { | ||||||
|             return Ok(Json(McpResponse { |             return Ok(Json(McpResponse { | ||||||
|                 jsonrpc: "2.0".to_string(), |                 jsonrpc: "2.0".to_string(), | ||||||
|   | |||||||
							
								
								
									
										205
									
								
								src/mcp/tools.rs
									
									
									
									
									
								
							
							
						
						
									
										205
									
								
								src/mcp/tools.rs
									
									
									
									
									
								
							| @@ -217,6 +217,141 @@ impl BlogTools { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn translate_document(&self, args: Value) -> Result<ToolResult> { | ||||||
|  |         use crate::commands::doc::DocCommand; | ||||||
|  |         use crate::commands::doc::DocAction; | ||||||
|  |  | ||||||
|  |         let input_file = args.get("input_file") | ||||||
|  |             .and_then(|v| v.as_str()) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("input_file is required"))?; | ||||||
|  |          | ||||||
|  |         let target_lang = args.get("target_lang") | ||||||
|  |             .and_then(|v| v.as_str()) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("target_lang is required"))?; | ||||||
|  |          | ||||||
|  |         let source_lang = args.get("source_lang").and_then(|v| v.as_str()).map(|s| s.to_string()); | ||||||
|  |         let output_file = args.get("output_file").and_then(|v| v.as_str()).map(|s| PathBuf::from(s)); | ||||||
|  |         let model = args.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5:latest"); | ||||||
|  |         let ollama_endpoint = args.get("ollama_endpoint").and_then(|v| v.as_str()).unwrap_or("http://localhost:11434"); | ||||||
|  |  | ||||||
|  |         let doc_cmd = DocCommand { | ||||||
|  |             action: DocAction::Translate { | ||||||
|  |                 input: PathBuf::from(input_file), | ||||||
|  |                 target_lang: target_lang.to_string(), | ||||||
|  |                 source_lang: source_lang.clone(), | ||||||
|  |                 output: output_file, | ||||||
|  |                 model: model.to_string(), | ||||||
|  |                 ollama_endpoint: ollama_endpoint.to_string(), | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         match doc_cmd.execute(self.base_path.clone()).await { | ||||||
|  |             Ok(_) => { | ||||||
|  |                 let output_path = if let Some(output) = args.get("output_file").and_then(|v| v.as_str()) { | ||||||
|  |                     output.to_string() | ||||||
|  |                 } else { | ||||||
|  |                     let input_path = PathBuf::from(input_file); | ||||||
|  |                     let stem = input_path.file_stem().unwrap().to_string_lossy(); | ||||||
|  |                     let ext = input_path.extension().unwrap_or_default().to_string_lossy(); | ||||||
|  |                     format!("{}.{}.{}", stem, target_lang, ext) | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 Ok(ToolResult { | ||||||
|  |                     content: vec![Content { | ||||||
|  |                         content_type: "text".to_string(), | ||||||
|  |                         text: format!("Document translated successfully from {} to {}. Output: {}",  | ||||||
|  |                                     source_lang.unwrap_or_else(|| "auto-detected".to_string()),  | ||||||
|  |                                     target_lang, output_path), | ||||||
|  |                     }], | ||||||
|  |                     is_error: None, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             Err(e) => Ok(ToolResult { | ||||||
|  |                 content: vec![Content { | ||||||
|  |                     content_type: "text".to_string(), | ||||||
|  |                     text: format!("Translation failed: {}", e), | ||||||
|  |                 }], | ||||||
|  |                 is_error: Some(true), | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn generate_documentation(&self, args: Value) -> Result<ToolResult> { | ||||||
|  |         use crate::commands::doc::DocCommand; | ||||||
|  |         use crate::commands::doc::DocAction; | ||||||
|  |  | ||||||
|  |         let doc_type = args.get("doc_type") | ||||||
|  |             .and_then(|v| v.as_str()) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("doc_type is required"))?; | ||||||
|  |  | ||||||
|  |         let source_path = args.get("source_path").and_then(|v| v.as_str()).unwrap_or("."); | ||||||
|  |         let output_path = args.get("output_path").and_then(|v| v.as_str()); | ||||||
|  |         let with_ai = args.get("with_ai").and_then(|v| v.as_bool()).unwrap_or(true); | ||||||
|  |         let include_deps = args.get("include_deps").and_then(|v| v.as_bool()).unwrap_or(false); | ||||||
|  |         let format_type = args.get("format_type").and_then(|v| v.as_str()).unwrap_or("markdown"); | ||||||
|  |  | ||||||
|  |         let action = match doc_type { | ||||||
|  |             "readme" => DocAction::Readme { | ||||||
|  |                 source: PathBuf::from(source_path), | ||||||
|  |                 output: PathBuf::from(output_path.unwrap_or("README.md")), | ||||||
|  |                 with_ai, | ||||||
|  |             }, | ||||||
|  |             "api" => DocAction::Api { | ||||||
|  |                 source: PathBuf::from(source_path), | ||||||
|  |                 output: PathBuf::from(output_path.unwrap_or("./docs")), | ||||||
|  |                 format: format_type.to_string(), | ||||||
|  |             }, | ||||||
|  |             "structure" => DocAction::Structure { | ||||||
|  |                 source: PathBuf::from(source_path), | ||||||
|  |                 output: PathBuf::from(output_path.unwrap_or("docs/structure.md")), | ||||||
|  |                 include_deps, | ||||||
|  |             }, | ||||||
|  |             "changelog" => DocAction::Changelog { | ||||||
|  |                 from: None, | ||||||
|  |                 to: None, | ||||||
|  |                 output: PathBuf::from(output_path.unwrap_or("CHANGELOG.md")), | ||||||
|  |                 explain_changes: with_ai, | ||||||
|  |             }, | ||||||
|  |             _ => return Ok(ToolResult { | ||||||
|  |                 content: vec![Content { | ||||||
|  |                     content_type: "text".to_string(), | ||||||
|  |                     text: format!("Unsupported doc_type: {}. Supported types: readme, api, structure, changelog", doc_type), | ||||||
|  |                 }], | ||||||
|  |                 is_error: Some(true), | ||||||
|  |             }) | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let doc_cmd = DocCommand { action }; | ||||||
|  |  | ||||||
|  |         match doc_cmd.execute(self.base_path.clone()).await { | ||||||
|  |             Ok(_) => { | ||||||
|  |                 let output_path = match doc_type { | ||||||
|  |                     "readme" => output_path.unwrap_or("README.md"), | ||||||
|  |                     "api" => output_path.unwrap_or("./docs"), | ||||||
|  |                     "structure" => output_path.unwrap_or("docs/structure.md"), | ||||||
|  |                     "changelog" => output_path.unwrap_or("CHANGELOG.md"), | ||||||
|  |                     _ => "unknown" | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 Ok(ToolResult { | ||||||
|  |                     content: vec![Content { | ||||||
|  |                         content_type: "text".to_string(), | ||||||
|  |                         text: format!("{} documentation generated successfully. Output: {}",  | ||||||
|  |                                     doc_type.to_uppercase(), output_path), | ||||||
|  |                     }], | ||||||
|  |                     is_error: None, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             Err(e) => Ok(ToolResult { | ||||||
|  |                 content: vec![Content { | ||||||
|  |                     content_type: "text".to_string(), | ||||||
|  |                     text: format!("Documentation generation failed: {}", e), | ||||||
|  |                 }], | ||||||
|  |                 is_error: Some(true), | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub fn get_tools() -> Vec<Tool> { |     pub fn get_tools() -> Vec<Tool> { | ||||||
|         vec![ |         vec![ | ||||||
|             Tool { |             Tool { | ||||||
| @@ -294,6 +429,76 @@ impl BlogTools { | |||||||
|                     "required": ["slug"] |                     "required": ["slug"] | ||||||
|                 }), |                 }), | ||||||
|             }, |             }, | ||||||
|  |             Tool { | ||||||
|  |                 name: "translate_document".to_string(), | ||||||
|  |                 description: "Translate markdown documents using Ollama AI while preserving structure".to_string(), | ||||||
|  |                 input_schema: json!({ | ||||||
|  |                     "type": "object", | ||||||
|  |                     "properties": { | ||||||
|  |                         "input_file": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Path to the input markdown file" | ||||||
|  |                         }, | ||||||
|  |                         "target_lang": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Target language code (en, ja, zh, ko, es)" | ||||||
|  |                         }, | ||||||
|  |                         "source_lang": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Source language code (auto-detect if not specified)" | ||||||
|  |                         }, | ||||||
|  |                         "output_file": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Output file path (auto-generated if not specified)" | ||||||
|  |                         }, | ||||||
|  |                         "model": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Ollama model to use (default: qwen2.5:latest)" | ||||||
|  |                         }, | ||||||
|  |                         "ollama_endpoint": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Ollama API endpoint (default: http://localhost:11434)" | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     "required": ["input_file", "target_lang"] | ||||||
|  |                 }), | ||||||
|  |             }, | ||||||
|  |             Tool { | ||||||
|  |                 name: "generate_documentation".to_string(), | ||||||
|  |                 description: "Generate various types of documentation from code analysis".to_string(), | ||||||
|  |                 input_schema: json!({ | ||||||
|  |                     "type": "object", | ||||||
|  |                     "properties": { | ||||||
|  |                         "doc_type": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "enum": ["readme", "api", "structure", "changelog"], | ||||||
|  |                             "description": "Type of documentation to generate" | ||||||
|  |                         }, | ||||||
|  |                         "source_path": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Source directory to analyze (default: current directory)" | ||||||
|  |                         }, | ||||||
|  |                         "output_path": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "description": "Output file or directory path" | ||||||
|  |                         }, | ||||||
|  |                         "with_ai": { | ||||||
|  |                             "type": "boolean", | ||||||
|  |                             "description": "Include AI-generated insights (default: true)" | ||||||
|  |                         }, | ||||||
|  |                         "include_deps": { | ||||||
|  |                             "type": "boolean", | ||||||
|  |                             "description": "Include dependency analysis (default: false)" | ||||||
|  |                         }, | ||||||
|  |                         "format_type": { | ||||||
|  |                             "type": "string", | ||||||
|  |                             "enum": ["markdown", "html", "json"], | ||||||
|  |                             "description": "Output format (default: markdown)" | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     "required": ["doc_type"] | ||||||
|  |                 }), | ||||||
|  |             }, | ||||||
|         ] |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										253
									
								
								src/translator/markdown_parser.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								src/translator/markdown_parser.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,253 @@ | |||||||
|  | use anyhow::Result; | ||||||
|  | use regex::Regex; | ||||||
|  | use super::MarkdownSection; | ||||||
|  |  | ||||||
|  | pub struct MarkdownParser { | ||||||
|  |     code_block_regex: Regex, | ||||||
|  |     header_regex: Regex, | ||||||
|  |     link_regex: Regex, | ||||||
|  |     image_regex: Regex, | ||||||
|  |     table_regex: Regex, | ||||||
|  |     list_regex: Regex, | ||||||
|  |     quote_regex: Regex, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MarkdownParser { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             code_block_regex: Regex::new(r"```([a-zA-Z0-9]*)\n([\s\S]*?)\n```").unwrap(), | ||||||
|  |             header_regex: Regex::new(r"^(#{1,6})\s+(.+)$").unwrap(), | ||||||
|  |             link_regex: Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap(), | ||||||
|  |             image_regex: Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap(), | ||||||
|  |             table_regex: Regex::new(r"^\|.*\|$").unwrap(), | ||||||
|  |             list_regex: Regex::new(r"^[\s]*[-*+]\s+(.+)$").unwrap(), | ||||||
|  |             quote_regex: Regex::new(r"^>\s+(.+)$").unwrap(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub fn parse_markdown(&self, content: &str) -> Result<Vec<MarkdownSection>> { | ||||||
|  |         let mut sections = Vec::new(); | ||||||
|  |         let mut current_text = String::new(); | ||||||
|  |         let lines: Vec<&str> = content.lines().collect(); | ||||||
|  |         let mut i = 0; | ||||||
|  |          | ||||||
|  |         while i < lines.len() { | ||||||
|  |             let line = lines[i]; | ||||||
|  |              | ||||||
|  |             // Check for code blocks | ||||||
|  |             if line.starts_with("```") { | ||||||
|  |                 // Save accumulated text | ||||||
|  |                 if !current_text.trim().is_empty() { | ||||||
|  |                     sections.extend(self.parse_text_sections(¤t_text)?); | ||||||
|  |                     current_text.clear(); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // Parse code block | ||||||
|  |                 let (code_section, lines_consumed) = self.parse_code_block(&lines[i..])?; | ||||||
|  |                 sections.push(code_section); | ||||||
|  |                 i += lines_consumed; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Check for headers | ||||||
|  |             if let Some(caps) = self.header_regex.captures(line) { | ||||||
|  |                 // Save accumulated text | ||||||
|  |                 if !current_text.trim().is_empty() { | ||||||
|  |                     sections.extend(self.parse_text_sections(¤t_text)?); | ||||||
|  |                     current_text.clear(); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 let level = caps.get(1).unwrap().as_str().len() as u8; | ||||||
|  |                 let header_text = caps.get(2).unwrap().as_str().to_string(); | ||||||
|  |                 sections.push(MarkdownSection::Header(header_text, level)); | ||||||
|  |                 i += 1; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Check for tables | ||||||
|  |             if self.table_regex.is_match(line) { | ||||||
|  |                 // Save accumulated text | ||||||
|  |                 if !current_text.trim().is_empty() { | ||||||
|  |                     sections.extend(self.parse_text_sections(¤t_text)?); | ||||||
|  |                     current_text.clear(); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 let (table_section, lines_consumed) = self.parse_table(&lines[i..])?; | ||||||
|  |                 sections.push(table_section); | ||||||
|  |                 i += lines_consumed; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Check for quotes | ||||||
|  |             if let Some(caps) = self.quote_regex.captures(line) { | ||||||
|  |                 // Save accumulated text | ||||||
|  |                 if !current_text.trim().is_empty() { | ||||||
|  |                     sections.extend(self.parse_text_sections(¤t_text)?); | ||||||
|  |                     current_text.clear(); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 let quote_text = caps.get(1).unwrap().as_str().to_string(); | ||||||
|  |                 sections.push(MarkdownSection::Quote(quote_text)); | ||||||
|  |                 i += 1; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Check for lists | ||||||
|  |             if let Some(caps) = self.list_regex.captures(line) { | ||||||
|  |                 // Save accumulated text | ||||||
|  |                 if !current_text.trim().is_empty() { | ||||||
|  |                     sections.extend(self.parse_text_sections(¤t_text)?); | ||||||
|  |                     current_text.clear(); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 let list_text = caps.get(1).unwrap().as_str().to_string(); | ||||||
|  |                 sections.push(MarkdownSection::List(list_text)); | ||||||
|  |                 i += 1; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Accumulate regular text | ||||||
|  |             current_text.push_str(line); | ||||||
|  |             current_text.push('\n'); | ||||||
|  |             i += 1; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Process remaining text | ||||||
|  |         if !current_text.trim().is_empty() { | ||||||
|  |             sections.extend(self.parse_text_sections(¤t_text)?); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(sections) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn parse_code_block(&self, lines: &[&str]) -> Result<(MarkdownSection, usize)> { | ||||||
|  |         if lines.is_empty() || !lines[0].starts_with("```") { | ||||||
|  |             anyhow::bail!("Not a code block"); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let first_line = lines[0]; | ||||||
|  |         let language = if first_line.len() > 3 { | ||||||
|  |             Some(first_line[3..].trim().to_string()) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let mut content = String::new(); | ||||||
|  |         let mut end_index = 1; | ||||||
|  |          | ||||||
|  |         for (i, &line) in lines[1..].iter().enumerate() { | ||||||
|  |             if line.starts_with("```") { | ||||||
|  |                 end_index = i + 2; // +1 for slice offset, +1 for closing line | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             if i > 0 { | ||||||
|  |                 content.push('\n'); | ||||||
|  |             } | ||||||
|  |             content.push_str(line); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok((MarkdownSection::Code(content, language), end_index)) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn parse_table(&self, lines: &[&str]) -> Result<(MarkdownSection, usize)> { | ||||||
|  |         let mut table_content = String::new(); | ||||||
|  |         let mut line_count = 0; | ||||||
|  |          | ||||||
|  |         for &line in lines { | ||||||
|  |             if self.table_regex.is_match(line) { | ||||||
|  |                 if line_count > 0 { | ||||||
|  |                     table_content.push('\n'); | ||||||
|  |                 } | ||||||
|  |                 table_content.push_str(line); | ||||||
|  |                 line_count += 1; | ||||||
|  |             } else { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok((MarkdownSection::Table(table_content), line_count)) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn parse_text_sections(&self, text: &str) -> Result<Vec<MarkdownSection>> { | ||||||
|  |         let mut sections = Vec::new(); | ||||||
|  |         let mut remaining = text; | ||||||
|  |          | ||||||
|  |         // Look for images first (they should be preserved) | ||||||
|  |         while let Some(caps) = self.image_regex.captures(remaining) { | ||||||
|  |             let full_match = caps.get(0).unwrap(); | ||||||
|  |             let before = &remaining[..full_match.start()]; | ||||||
|  |             let alt = caps.get(1).unwrap().as_str().to_string(); | ||||||
|  |             let url = caps.get(2).unwrap().as_str().to_string(); | ||||||
|  |              | ||||||
|  |             if !before.trim().is_empty() { | ||||||
|  |                 sections.push(MarkdownSection::Text(before.to_string())); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             sections.push(MarkdownSection::Image(alt, url)); | ||||||
|  |             remaining = &remaining[full_match.end()..]; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Look for links | ||||||
|  |         let mut current_text = remaining.to_string(); | ||||||
|  |         while let Some(caps) = self.link_regex.captures(¤t_text) { | ||||||
|  |             let full_match = caps.get(0).unwrap(); | ||||||
|  |             let before = ¤t_text[..full_match.start()]; | ||||||
|  |             let link_text = caps.get(1).unwrap().as_str().to_string(); | ||||||
|  |             let url = caps.get(2).unwrap().as_str().to_string(); | ||||||
|  |              | ||||||
|  |             if !before.trim().is_empty() { | ||||||
|  |                 sections.push(MarkdownSection::Text(before.to_string())); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             sections.push(MarkdownSection::Link(link_text, url)); | ||||||
|  |             current_text = current_text[full_match.end()..].to_string(); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Add remaining text | ||||||
|  |         if !current_text.trim().is_empty() { | ||||||
|  |             sections.push(MarkdownSection::Text(current_text)); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(sections) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub fn rebuild_markdown(&self, sections: Vec<MarkdownSection>) -> String { | ||||||
|  |         let mut result = String::new(); | ||||||
|  |          | ||||||
|  |         for section in sections { | ||||||
|  |             match section { | ||||||
|  |                 MarkdownSection::Text(text) => { | ||||||
|  |                     result.push_str(&text); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Code(content, Some(lang)) => { | ||||||
|  |                     result.push_str(&format!("```{}\n{}\n```\n", lang, content)); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Code(content, None) => { | ||||||
|  |                     result.push_str(&format!("```\n{}\n```\n", content)); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Header(text, level) => { | ||||||
|  |                     let hashes = "#".repeat(level as usize); | ||||||
|  |                     result.push_str(&format!("{} {}\n", hashes, text)); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Link(text, url) => { | ||||||
|  |                     result.push_str(&format!("[{}]({})", text, url)); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Image(alt, url) => { | ||||||
|  |                     result.push_str(&format!("", alt, url)); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Table(content) => { | ||||||
|  |                     result.push_str(&content); | ||||||
|  |                     result.push('\n'); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::List(text) => { | ||||||
|  |                     result.push_str(&format!("- {}\n", text)); | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Quote(text) => { | ||||||
|  |                     result.push_str(&format!("> {}\n", text)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         result | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										123
									
								
								src/translator/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/translator/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | pub mod ollama_translator; | ||||||
|  | pub mod markdown_parser; | ||||||
|  |  | ||||||
|  | use anyhow::Result; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
|  | pub struct TranslationConfig { | ||||||
|  |     pub source_lang: String, | ||||||
|  |     pub target_lang: String, | ||||||
|  |     pub ollama_endpoint: String, | ||||||
|  |     pub model: String, | ||||||
|  |     pub preserve_code: bool, | ||||||
|  |     pub preserve_links: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Default for TranslationConfig { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             source_lang: "ja".to_string(), | ||||||
|  |             target_lang: "en".to_string(), | ||||||
|  |             ollama_endpoint: "http://localhost:11434".to_string(), | ||||||
|  |             model: "qwen2.5:latest".to_string(), | ||||||
|  |             preserve_code: true, | ||||||
|  |             preserve_links: true, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum MarkdownSection { | ||||||
|  |     Text(String), | ||||||
|  |     Code(String, Option<String>), // content, language | ||||||
|  |     Header(String, u8), // content, level (1-6) | ||||||
|  |     Link(String, String), // text, url | ||||||
|  |     Image(String, String), // alt, url | ||||||
|  |     Table(String), | ||||||
|  |     List(String), | ||||||
|  |     Quote(String), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub trait Translator { | ||||||
|  |     async fn translate(&self, content: &str, config: &TranslationConfig) -> Result<String>; | ||||||
|  |     async fn translate_markdown(&self, content: &str, config: &TranslationConfig) -> Result<String>; | ||||||
|  |     async fn translate_sections(&self, sections: Vec<MarkdownSection>, config: &TranslationConfig) -> Result<Vec<MarkdownSection>>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct TranslationResult { | ||||||
|  |     pub original: String, | ||||||
|  |     pub translated: String, | ||||||
|  |     pub source_lang: String, | ||||||
|  |     pub target_lang: String, | ||||||
|  |     pub model: String, | ||||||
|  |     pub metrics: TranslationMetrics, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Default)] | ||||||
|  | pub struct TranslationMetrics { | ||||||
|  |     pub character_count: usize, | ||||||
|  |     pub word_count: usize, | ||||||
|  |     pub translation_time_ms: u64, | ||||||
|  |     pub sections_translated: usize, | ||||||
|  |     pub sections_preserved: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct LanguageMapping { | ||||||
|  |     pub mappings: HashMap<String, LanguageInfo>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub struct LanguageInfo { | ||||||
|  |     pub name: String, | ||||||
|  |     pub code: String, | ||||||
|  |     pub ollama_prompt: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl LanguageMapping { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         let mut mappings = HashMap::new(); | ||||||
|  |          | ||||||
|  |         // 主要言語の設定 | ||||||
|  |         mappings.insert("ja".to_string(), LanguageInfo { | ||||||
|  |             name: "Japanese".to_string(), | ||||||
|  |             code: "ja".to_string(), | ||||||
|  |             ollama_prompt: "You are a professional Japanese translator specializing in technical documentation.".to_string(), | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         mappings.insert("en".to_string(), LanguageInfo { | ||||||
|  |             name: "English".to_string(), | ||||||
|  |             code: "en".to_string(), | ||||||
|  |             ollama_prompt: "You are a professional English translator specializing in technical documentation.".to_string(), | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         mappings.insert("zh".to_string(), LanguageInfo { | ||||||
|  |             name: "Chinese".to_string(), | ||||||
|  |             code: "zh".to_string(), | ||||||
|  |             ollama_prompt: "You are a professional Chinese translator specializing in technical documentation.".to_string(), | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         mappings.insert("ko".to_string(), LanguageInfo { | ||||||
|  |             name: "Korean".to_string(), | ||||||
|  |             code: "ko".to_string(), | ||||||
|  |             ollama_prompt: "You are a professional Korean translator specializing in technical documentation.".to_string(), | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         mappings.insert("es".to_string(), LanguageInfo { | ||||||
|  |             name: "Spanish".to_string(), | ||||||
|  |             code: "es".to_string(), | ||||||
|  |             ollama_prompt: "You are a professional Spanish translator specializing in technical documentation.".to_string(), | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         Self { mappings } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub fn get_language_info(&self, code: &str) -> Option<&LanguageInfo> { | ||||||
|  |         self.mappings.get(code) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub fn get_supported_languages(&self) -> Vec<String> { | ||||||
|  |         self.mappings.keys().cloned().collect() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										214
									
								
								src/translator/ollama_translator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/translator/ollama_translator.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | |||||||
|  | use anyhow::Result; | ||||||
|  | use reqwest::Client; | ||||||
|  | use serde_json::json; | ||||||
|  | use std::time::Instant; | ||||||
|  | use super::*; | ||||||
|  | use crate::translator::markdown_parser::MarkdownParser; | ||||||
|  |  | ||||||
|  | pub struct OllamaTranslator { | ||||||
|  |     client: Client, | ||||||
|  |     language_mapping: LanguageMapping, | ||||||
|  |     parser: MarkdownParser, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl OllamaTranslator { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             client: Client::new(), | ||||||
|  |             language_mapping: LanguageMapping::new(), | ||||||
|  |             parser: MarkdownParser::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     async fn call_ollama(&self, prompt: &str, config: &TranslationConfig) -> Result<String> { | ||||||
|  |         let request_body = json!({ | ||||||
|  |             "model": config.model, | ||||||
|  |             "prompt": prompt, | ||||||
|  |             "stream": false, | ||||||
|  |             "options": { | ||||||
|  |                 "temperature": 0.3, | ||||||
|  |                 "top_p": 0.9, | ||||||
|  |                 "top_k": 40 | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         let url = format!("{}/api/generate", config.ollama_endpoint); | ||||||
|  |          | ||||||
|  |         let response = self.client | ||||||
|  |             .post(&url) | ||||||
|  |             .json(&request_body) | ||||||
|  |             .send() | ||||||
|  |             .await?; | ||||||
|  |          | ||||||
|  |         if !response.status().is_success() { | ||||||
|  |             anyhow::bail!("Ollama API request failed: {}", response.status()); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let response_text = response.text().await?; | ||||||
|  |         let response_json: serde_json::Value = serde_json::from_str(&response_text)?; | ||||||
|  |          | ||||||
|  |         let translated = response_json | ||||||
|  |             .get("response") | ||||||
|  |             .and_then(|v| v.as_str()) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("Invalid response from Ollama"))?; | ||||||
|  |          | ||||||
|  |         Ok(translated.to_string()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn build_translation_prompt(&self, text: &str, config: &TranslationConfig) -> Result<String> { | ||||||
|  |         let source_info = self.language_mapping.get_language_info(&config.source_lang) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("Unsupported source language: {}", config.source_lang))?; | ||||||
|  |          | ||||||
|  |         let target_info = self.language_mapping.get_language_info(&config.target_lang) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("Unsupported target language: {}", config.target_lang))?; | ||||||
|  |          | ||||||
|  |         let prompt = format!( | ||||||
|  |             r#"{system_prompt} | ||||||
|  |  | ||||||
|  | Translate the following text from {source_lang} to {target_lang}. | ||||||
|  |  | ||||||
|  | IMPORTANT RULES: | ||||||
|  | 1. Preserve all Markdown formatting (headers, links, code blocks, etc.) | ||||||
|  | 2. Do NOT translate content within code blocks (```) | ||||||
|  | 3. Do NOT translate URLs or file paths | ||||||
|  | 4. Preserve technical terms when appropriate | ||||||
|  | 5. Maintain the original structure and formatting | ||||||
|  | 6. Only output the translated text, no explanations | ||||||
|  |  | ||||||
|  | Original text ({source_code}): | ||||||
|  | {text} | ||||||
|  |  | ||||||
|  | Translated text ({target_code}):"#, | ||||||
|  |             system_prompt = target_info.ollama_prompt, | ||||||
|  |             source_lang = source_info.name, | ||||||
|  |             target_lang = target_info.name, | ||||||
|  |             source_code = source_info.code, | ||||||
|  |             target_code = target_info.code, | ||||||
|  |             text = text | ||||||
|  |         ); | ||||||
|  |          | ||||||
|  |         Ok(prompt) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn build_section_translation_prompt(&self, section: &MarkdownSection, config: &TranslationConfig) -> Result<String> { | ||||||
|  |         let target_info = self.language_mapping.get_language_info(&config.target_lang) | ||||||
|  |             .ok_or_else(|| anyhow::anyhow!("Unsupported target language: {}", config.target_lang))?; | ||||||
|  |          | ||||||
|  |         let (content, section_type) = match section { | ||||||
|  |             MarkdownSection::Text(text) => (text.clone(), "text"), | ||||||
|  |             MarkdownSection::Header(text, _) => (text.clone(), "header"), | ||||||
|  |             MarkdownSection::Quote(text) => (text.clone(), "quote"), | ||||||
|  |             MarkdownSection::List(text) => (text.clone(), "list"), | ||||||
|  |             _ => return Ok(String::new()), // Skip translation for code, links, etc. | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let prompt = format!( | ||||||
|  |             r#"{system_prompt} | ||||||
|  |  | ||||||
|  | Translate this {section_type} from {source_lang} to {target_lang}. | ||||||
|  |  | ||||||
|  | RULES: | ||||||
|  | - Only translate the text content | ||||||
|  | - Preserve formatting symbols (*, #, >, etc.) | ||||||
|  | - Keep technical terms when appropriate | ||||||
|  | - Output only the translated text | ||||||
|  |  | ||||||
|  | Text to translate: | ||||||
|  | {content} | ||||||
|  |  | ||||||
|  | Translation:"#, | ||||||
|  |             system_prompt = target_info.ollama_prompt, | ||||||
|  |             section_type = section_type, | ||||||
|  |             source_lang = config.source_lang, | ||||||
|  |             target_lang = config.target_lang, | ||||||
|  |             content = content | ||||||
|  |         ); | ||||||
|  |          | ||||||
|  |         Ok(prompt) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Translator for OllamaTranslator { | ||||||
|  |     async fn translate(&self, content: &str, config: &TranslationConfig) -> Result<String> { | ||||||
|  |         let prompt = self.build_translation_prompt(content, config)?; | ||||||
|  |         self.call_ollama(&prompt, config).await | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     async fn translate_markdown(&self, content: &str, config: &TranslationConfig) -> Result<String> { | ||||||
|  |         println!("🔄 Parsing markdown content..."); | ||||||
|  |         let sections = self.parser.parse_markdown(content)?; | ||||||
|  |          | ||||||
|  |         println!("📝 Found {} sections to process", sections.len()); | ||||||
|  |         let translated_sections = self.translate_sections(sections, config).await?; | ||||||
|  |          | ||||||
|  |         println!("✅ Rebuilding markdown from translated sections..."); | ||||||
|  |         let result = self.parser.rebuild_markdown(translated_sections); | ||||||
|  |          | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     async fn translate_sections(&self, sections: Vec<MarkdownSection>, config: &TranslationConfig) -> Result<Vec<MarkdownSection>> { | ||||||
|  |         let mut translated_sections = Vec::new(); | ||||||
|  |         let start_time = Instant::now(); | ||||||
|  |          | ||||||
|  |         for (index, section) in sections.into_iter().enumerate() { | ||||||
|  |             println!("  🔤 Processing section {}", index + 1); | ||||||
|  |              | ||||||
|  |             let translated_section = match §ion { | ||||||
|  |                 MarkdownSection::Code(content, lang) => { | ||||||
|  |                     if config.preserve_code { | ||||||
|  |                         println!("    ⏭️  Preserving code block"); | ||||||
|  |                         section // Preserve code blocks | ||||||
|  |                     } else { | ||||||
|  |                         section // Still preserve for now | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Link(text, url) => { | ||||||
|  |                     if config.preserve_links { | ||||||
|  |                         println!("    ⏭️  Preserving link"); | ||||||
|  |                         section // Preserve links | ||||||
|  |                     } else { | ||||||
|  |                         // Translate link text only | ||||||
|  |                         let prompt = self.build_section_translation_prompt(&MarkdownSection::Text(text.clone()), config)?; | ||||||
|  |                         let translated_text = self.call_ollama(&prompt, config).await?; | ||||||
|  |                         MarkdownSection::Link(translated_text.trim().to_string(), url.clone()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Image(alt, url) => { | ||||||
|  |                     println!("    🖼️  Preserving image"); | ||||||
|  |                     section // Preserve images | ||||||
|  |                 } | ||||||
|  |                 MarkdownSection::Table(content) => { | ||||||
|  |                     println!("    📊 Translating table content"); | ||||||
|  |                     let prompt = self.build_section_translation_prompt(&MarkdownSection::Text(content.clone()), config)?; | ||||||
|  |                     let translated_content = self.call_ollama(&prompt, config).await?; | ||||||
|  |                     MarkdownSection::Table(translated_content.trim().to_string()) | ||||||
|  |                 } | ||||||
|  |                 _ => { | ||||||
|  |                     // Translate text sections | ||||||
|  |                     println!("    🔤 Translating text"); | ||||||
|  |                     let prompt = self.build_section_translation_prompt(§ion, config)?; | ||||||
|  |                     let translated_text = self.call_ollama(&prompt, config).await?; | ||||||
|  |                      | ||||||
|  |                     match section { | ||||||
|  |                         MarkdownSection::Text(_) => MarkdownSection::Text(translated_text.trim().to_string()), | ||||||
|  |                         MarkdownSection::Header(_, level) => MarkdownSection::Header(translated_text.trim().to_string(), level), | ||||||
|  |                         MarkdownSection::Quote(_) => MarkdownSection::Quote(translated_text.trim().to_string()), | ||||||
|  |                         MarkdownSection::List(_) => MarkdownSection::List(translated_text.trim().to_string()), | ||||||
|  |                         _ => section, | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |              | ||||||
|  |             translated_sections.push(translated_section); | ||||||
|  |              | ||||||
|  |             // Add small delay to avoid overwhelming Ollama | ||||||
|  |             tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let elapsed = start_time.elapsed(); | ||||||
|  |         println!("⏱️  Translation completed in {:.2}s", elapsed.as_secs_f64()); | ||||||
|  |          | ||||||
|  |         Ok(translated_sections) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										103
									
								
								templates/api.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								templates/api.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | # API Documentation | ||||||
|  |  | ||||||
|  | ## Public Functions | ||||||
|  |  | ||||||
|  | {{#each api.public_functions}} | ||||||
|  | ### `{{this.name}}` | ||||||
|  |  | ||||||
|  | {{#if this.docs}} | ||||||
|  | {{this.docs}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | **Visibility:** `{{this.visibility}}` | ||||||
|  | {{#if this.is_async}}**Async:** Yes{{/if}} | ||||||
|  |  | ||||||
|  | {{#if this.parameters}} | ||||||
|  | **Parameters:** | ||||||
|  | {{#each this.parameters}} | ||||||
|  | - `{{this.name}}`: `{{this.param_type}}`{{#if this.is_mutable}} (mutable){{/if}} | ||||||
|  | {{/each}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | {{#if this.return_type}} | ||||||
|  | **Returns:** `{{this.return_type}}` | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Public Structs | ||||||
|  |  | ||||||
|  | {{#each api.public_structs}} | ||||||
|  | ### `{{this.name}}` | ||||||
|  |  | ||||||
|  | {{#if this.docs}} | ||||||
|  | {{this.docs}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | **Visibility:** `{{this.visibility}}` | ||||||
|  |  | ||||||
|  | {{#if this.fields}} | ||||||
|  | **Fields:** | ||||||
|  | {{#each this.fields}} | ||||||
|  | - `{{this.name}}`: `{{this.field_type}}` ({{this.visibility}}) | ||||||
|  | {{#if this.docs}}  - {{this.docs}}{{/if}} | ||||||
|  | {{/each}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Public Enums | ||||||
|  |  | ||||||
|  | {{#each api.public_enums}} | ||||||
|  | ### `{{this.name}}` | ||||||
|  |  | ||||||
|  | {{#if this.docs}} | ||||||
|  | {{this.docs}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | **Visibility:** `{{this.visibility}}` | ||||||
|  |  | ||||||
|  | {{#if this.variants}} | ||||||
|  | **Variants:** | ||||||
|  | {{#each this.variants}} | ||||||
|  | - `{{this.name}}` | ||||||
|  | {{#if this.docs}}  - {{this.docs}}{{/if}} | ||||||
|  | {{#if this.fields}} | ||||||
|  |   **Fields:** | ||||||
|  | {{#each this.fields}} | ||||||
|  |   - `{{this.name}}`: `{{this.field_type}}` | ||||||
|  | {{/each}} | ||||||
|  | {{/if}} | ||||||
|  | {{/each}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Public Traits | ||||||
|  |  | ||||||
|  | {{#each api.public_traits}} | ||||||
|  | ### `{{this.name}}` | ||||||
|  |  | ||||||
|  | {{#if this.docs}} | ||||||
|  | {{this.docs}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | **Visibility:** `{{this.visibility}}` | ||||||
|  |  | ||||||
|  | {{#if this.methods}} | ||||||
|  | **Methods:** | ||||||
|  | {{#each this.methods}} | ||||||
|  | - `{{this.name}}({{#each this.parameters}}{{this.name}}: {{this.param_type}}{{#unless @last}}, {{/unless}}{{/each}}){{#if this.return_type}} -> {{this.return_type}}{{/if}}` | ||||||
|  | {{#if this.docs}}  - {{this.docs}}{{/if}} | ||||||
|  | {{/each}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
							
								
								
									
										19
									
								
								templates/changelog.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								templates/changelog.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | # Changelog | ||||||
|  |  | ||||||
|  | ## Recent Changes | ||||||
|  |  | ||||||
|  | {{#each commits}} | ||||||
|  | ### {{this.date}} | ||||||
|  |  | ||||||
|  | **{{this.hash}}** by {{this.author}} | ||||||
|  |  | ||||||
|  | {{this.message}} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Summary | ||||||
|  |  | ||||||
|  | - **Total Commits:** {{commits.length}} | ||||||
|  | - **Contributors:** {{#unique commits "author"}}{{this.author}}{{#unless @last}}, {{/unless}}{{/unique}} | ||||||
							
								
								
									
										76
									
								
								templates/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								templates/readme.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | # {{project.name}} | ||||||
|  |  | ||||||
|  | {{#if project.description}} | ||||||
|  | {{project.description}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | ## Overview | ||||||
|  |  | ||||||
|  | This project contains {{project.modules.length}} modules with a total of {{project.metrics.total_lines}} lines of code. | ||||||
|  |  | ||||||
|  | ## Installation | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | cargo install {{project.name}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | {{project.name}} --help | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Dependencies | ||||||
|  |  | ||||||
|  | {{#each project.dependencies}} | ||||||
|  | - `{{@key}}`: {{this}} | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Project Structure | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | {{#each project.structure.directories}} | ||||||
|  | {{this.name}}/ | ||||||
|  | {{/each}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## API Documentation | ||||||
|  |  | ||||||
|  | {{#each project.modules}} | ||||||
|  | ### {{this.name}} | ||||||
|  |  | ||||||
|  | {{#if this.docs}} | ||||||
|  | {{this.docs}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | {{#if this.functions}} | ||||||
|  | **Functions:** {{this.functions.length}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | {{#if this.structs}} | ||||||
|  | **Structs:** {{this.structs.length}} | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Metrics | ||||||
|  |  | ||||||
|  | - **Lines of Code:** {{project.metrics.total_lines}} | ||||||
|  | - **Total Files:** {{project.metrics.total_files}} | ||||||
|  | - **Test Files:** {{project.metrics.test_files}} | ||||||
|  | - **Dependencies:** {{project.metrics.dependency_count}} | ||||||
|  | - **Complexity Score:** {{project.metrics.complexity_score}} | ||||||
|  |  | ||||||
|  | ## License | ||||||
|  |  | ||||||
|  | {{#if project.license}} | ||||||
|  | {{project.license}} | ||||||
|  | {{else}} | ||||||
|  | MIT | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | ## Authors | ||||||
|  |  | ||||||
|  | {{#each project.authors}} | ||||||
|  | - {{this}} | ||||||
|  | {{/each}} | ||||||
							
								
								
									
										39
									
								
								templates/structure.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								templates/structure.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | # Project Structure | ||||||
|  |  | ||||||
|  | ## Directory Overview | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | {{#each structure.directories}} | ||||||
|  | {{this.name}}/ | ||||||
|  | {{#each this.subdirectories}} | ||||||
|  | ├── {{this}}/ | ||||||
|  | {{/each}} | ||||||
|  | {{#if this.file_count}} | ||||||
|  | └── ({{this.file_count}} files) | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | {{/each}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## File Distribution | ||||||
|  |  | ||||||
|  | {{#each structure.files}} | ||||||
|  | - **{{this.name}}** ({{this.language}}) - {{this.lines_of_code}} lines{{#if this.is_test}} [TEST]{{/if}} | ||||||
|  | {{/each}} | ||||||
|  |  | ||||||
|  | ## Statistics | ||||||
|  |  | ||||||
|  | - **Total Directories:** {{structure.directories.length}} | ||||||
|  | - **Total Files:** {{structure.files.length}} | ||||||
|  | - **Languages Used:** | ||||||
|  | {{#group structure.files by="language"}} | ||||||
|  |   - {{@key}}: {{this.length}} files | ||||||
|  | {{/group}} | ||||||
|  |  | ||||||
|  | {{#if structure.dependency_graph}} | ||||||
|  | ## Dependencies | ||||||
|  |  | ||||||
|  | {{#each structure.dependency_graph}} | ||||||
|  | - **{{@key}}** depends on: {{#each this}}{{this}}{{#unless @last}}, {{/unless}}{{/each}} | ||||||
|  | {{/each}} | ||||||
|  | {{/if}} | ||||||
		Reference in New Issue
	
	Block a user