From 02dd69840d4d87c7308427499a27bc5b2eaf485f Mon Sep 17 00:00:00 2001
From: syui <syui@syui.ai>
Date: Wed, 4 Jun 2025 23:53:05 +0900
Subject: [PATCH] add cargo

---
 .claude/settings.local.json |   9 +
 .gitignore                  |   7 +
 Cargo.toml                  |  28 +++
 README.md                   |  73 ++++++
 claude.md                   | 478 ++++++++++++++++++++++++++++++++++++
 src/commands/build.rs       |  22 ++
 src/commands/clean.rs       |  21 ++
 src/commands/init.rs        | 216 ++++++++++++++++
 src/commands/mod.rs         |   5 +
 src/commands/new.rs         |  48 ++++
 src/commands/serve.rs       |  77 ++++++
 src/config.rs               |  63 +++++
 src/generator.rs            | 178 ++++++++++++++
 src/main.rs                 |  75 ++++++
 src/markdown.rs             | 138 +++++++++++
 src/template.rs             |  35 +++
 16 files changed, 1473 insertions(+)
 create mode 100644 .claude/settings.local.json
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 claude.md
 create mode 100644 src/commands/build.rs
 create mode 100644 src/commands/clean.rs
 create mode 100644 src/commands/init.rs
 create mode 100644 src/commands/mod.rs
 create mode 100644 src/commands/new.rs
 create mode 100644 src/commands/serve.rs
 create mode 100644 src/config.rs
 create mode 100644 src/generator.rs
 create mode 100644 src/main.rs
 create mode 100644 src/markdown.rs
 create mode 100644 src/template.rs

diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..43dced9
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,9 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(cargo init:*)",
+      "Bash(cargo:*)"
+    ],
+    "deny": []
+  }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9ae5be4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/target
+/Cargo.lock
+/public
+*.swp
+*.swo
+*~
+.DS_Store
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..5edcc5e
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "ailog"
+version = "0.1.0"
+edition = "2021"
+authors = ["syui"]
+description = "A static blog generator with AI features"
+license = "MIT"
+
+[dependencies]
+clap = { version = "4.5", features = ["derive"] }
+pulldown-cmark = "0.11"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tokio = { version = "1.40", features = ["full"] }
+anyhow = "1.0"
+toml = "0.8"
+chrono = "0.4"
+tera = "1.20"
+walkdir = "2.5"
+gray_matter = "0.2"
+fs_extra = "1.3"
+colored = "2.1"
+serde_yaml = "0.9"
+syntect = "5.2"
+reqwest = { version = "0.12", features = ["json"] }
+
+[dev-dependencies]
+tempfile = "3.14"
\ No newline at end of file
diff --git a/README.md b/README.md
index e69de29..7305d65 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,73 @@
+# ai.log
+
+A Rust-based static blog generator with AI integration capabilities.
+
+## 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.
+
+## Features
+
+- Static blog generation (inspired by Zola)
+- AI-powered article editing and enhancement
+- Automatic translation (ja → en)
+- AI comment system integrated with atproto
+- OAuth authentication via atproto accounts
+
+## Installation
+
+```bash
+cargo install ailog
+```
+
+## Usage
+
+```bash
+# Initialize a new blog
+ailog init myblog
+
+# Create a new post
+ailog new "My First Post"
+
+# Build the blog
+ailog build
+
+# Serve locally
+ailog serve
+
+# Clean build files
+ailog clean
+```
+
+## Configuration
+
+Configuration files are stored in `~/.config/syui/ai/log/`
+
+## AI Integration (Planned)
+
+- Automatic content suggestions and corrections
+- Multi-language support with AI translation
+- AI-generated comments linked to atproto accounts
+
+## atproto Integration (Planned)
+
+Implements OAuth 2.0 for user authentication:
+- Users can comment using their atproto accounts
+- Comments are stored in atproto collections
+- Full data sovereignty for users
+
+## Build & Deploy
+
+Designed for GitHub Actions and Cloudflare Pages deployment. Push to main branch triggers automatic build and deploy.
+
+## Development Status
+
+Currently implemented:
+- Basic static site generation
+- Markdown parsing and HTML generation
+- Template system
+- Development server
+
+## License
+
+© syui
diff --git a/claude.md b/claude.md
new file mode 100644
index 0000000..9cddeef
--- /dev/null
+++ b/claude.md
@@ -0,0 +1,478 @@
+# エコシステム統合設計書
+
+## 中核思想
+- **存在子理論**: この世界で最も小さいもの(存在子/ai)の探求
+- **唯一性原則**: 現実の個人の唯一性をすべてのシステムで担保
+- **現実の反映**: 現実→ゲーム→現実の循環的影響
+
+## システム構成図
+
+```
+存在子(ai) - 最小単位の意識
+    ↓
+[ai.moji] 文字システム
+    ↓
+[ai.os] + [ai.game device] ← 統合ハードウェア
+    ├── ai.shell (Claude Code的機能)
+    ├── ai.gpt (自律人格・記憶システム)
+    ├── ai.log (AIと連携するブログシステム)
+    ├── ai.ai (個人特化AI・心を読み取るAI)
+    ├── ai.card (カードゲーム・iOS/Web/API)
+    └── ai.bot (分散SNS連携・カード配布)
+    ↓
+[ai.verse] メタバース
+    ├── world system (惑星型3D世界)
+    ├── at system (atproto/分散SNS)
+    ├── yui system (唯一性担保)
+    └── ai system (存在属性)
+```
+
+## 名前規則
+
+名前規則は他のprojectと全て共通しています。exampleを示しますので、このルールに従ってください。
+
+ここでは`ai.os`の場合の名前規則の例を記述します。
+
+name: ai.os
+
+- **[ "package", "code", "command" ]**: aios
+- **[ "dir", "url" ]**: ai/os
+- **[ "domain", "json" ]**: ai.os
+
+```sh
+$ curl -sL https://git.syui.ai/ai/ai/raw/branch/main/ai.json|jq .ai.os
+{ "type": "os" }
+```
+
+```json
+{
+  "ai": {
+    "os":{}
+  }
+}
+```
+
+他のprojectも同じ名前規則を採用します。`ai.gpt`ならpackageは`aigpt`です。
+
+## config(設定ファイル, env, 環境依存)
+
+`config`を置く場所は統一されており、各projectの名前規則の`dir`項目を使用します。例えば、aiosの場合は`~/.config/syui/ai/os/`以下となります。pythonなどを使用する場合、`python -m venv`などでこのpackage config dirに環境を構築して実行するようにしてください。
+
+domain形式を採用して、私は各projectを`git.syui.ai/ai`にhostしていますから、`~/.config/syui/ai`とします。
+
+```sh
+[syui.ai]
+syui/ai
+```
+
+```sh
+# example
+~/.config/syui/ai
+    ├── card
+    ├── gpt
+    ├── os
+    └── shell
+```
+
+## 各システム詳細
+
+### ai.gpt - 自律的送信AI
+**目的**: 関係性に基づく自発的コミュニケーション
+
+**中核概念**:
+- **人格**: 記憶(過去の発話)と関係性パラメータで構成
+- **唯一性**: atproto accountとの1:1紐付け、改変不可能
+- **自律送信**: 関係性が閾値を超えると送信機能が解禁
+
+**技術構成**:
+- `MemoryManager`: 完全ログ→AI要約→コア判定→選択的忘却
+- `RelationshipTracker`: 時間減衰・日次制限付き関係性スコア
+- `TransmissionController`: 閾値判定・送信トリガー
+- `Persona`: AI運勢(1-10ランダム)による人格変動
+
+**実装仕様**:
+```
+- 言語: Python (fastapi_mcp)
+- ストレージ: JSON/SQLite選択式
+- インターフェース: Python CLI (click/typer)
+- スケジューリング: cron-like自律処理
+```
+
+### ai.card - カードゲームシステム
+**目的**: atproto基盤でのユーザーデータ主権カードゲーム
+
+**現在の状況**:
+- ai.botの機能として実装済み
+- atproto accountでmentionすると1日1回カードを取得
+- ai.api (MCP server予定) でユーザー管理
+
+**移行計画**:
+- **iOS移植**: Claudeが担当予定
+- **データ保存**: atproto collection recordに保存(ユーザーがデータを所有)
+- **不正防止**: OAuth 2.1 scope (実装待ち) + MCP serverで対応
+- **画像ファイル**: Cloudflare Pagesが最適
+
+**yui system適用**:
+- カードの効果がアカウント固有
+- 改ざん防止によるゲームバランス維持
+- 将来的にai.verseとの統合で固有スキルと連動
+
+### ai.ai - 心を読み取るAI
+**目的**: 個人特化型AI・深層理解システム
+
+**ai.gptとの関係**:
+- ai.gpt → ai.ai: 自律送信AIから心理分析AIへの連携
+- 関係性パラメータの深層分析
+- ユーザーの思想コア部分の特定支援
+
+### ai.verse - UEメタバース
+**目的**: 現実反映型3D世界
+
+**yui system実装**:
+- キャラクター ↔ プレイヤー 1:1紐付け
+- unique skill: そのプレイヤーのみ使用可能
+- 他プレイヤーは同キャラでも同スキル使用不可
+
+**統合要素**:
+- ai.card: ゲーム内アイテムとしてのカード
+- ai.gpt: NPCとしての自律AI人格
+- atproto: ゲーム内プロフィール連携
+
+## データフロー設計
+
+### 唯一性担保の実装
+```
+現実の個人 → atproto account (DID) → ゲーム内avatar → 固有スキル
+    ↑_______________________________|  (現実の反映)
+```
+
+### AI駆動変換システム
+```
+遊び・創作活動 → ai.gpt分析 → 業務成果変換 → 企業価値創出
+    ↑________________________|  (Play-to-Work)
+```
+
+### カードゲーム・データ主権フロー
+```
+ユーザー → ai.bot mention → カード生成 → atproto collection → ユーザー所有
+    ↑                                ↓
+    ← iOS app表示 ← ai.card API ←
+```
+
+## 技術スタック統合
+
+### Core Infrastructure
+- **OS**: Rust-based ai.os (Arch Linux base)
+- **Container**: Docker image distribution
+- **Identity**: atproto selfhost server + DID管理
+- **AI**: fastapi_mcp server architecture
+- **CLI**: Python unified (click/typer) - Rustから移行
+
+### Game Engine Integration
+- **Engine**: Unreal Engine (Blueprint)
+- **Data**: atproto → UE → atproto sync
+- **Avatar**: 分散SNS profile → 3D character
+- **Streaming**: game screen = broadcast screen
+
+### Mobile/Device
+- **iOS**: ai.card移植 (Claude担当)
+- **Hardware**: ai.game device (future)
+- **Interface**: controller-first design
+
+## 実装優先順位
+
+### Phase 1: AI基盤強化 (現在進行)
+- [ ] ai.gpt memory system完全実装
+  - 記憶の階層化(完全ログ→要約→コア→忘却)
+  - 関係性パラメータの時間減衰システム
+  - AI運勢による人格変動機能
+- [ ] ai.card iOS移植
+  - atproto collection record連携
+  - MCP server化(ai.api刷新)
+- [ ] fastapi_mcp統一基盤構築
+
+### Phase 2: ゲーム統合
+- [ ] ai.verse yui system実装
+  - unique skill機能
+  - atproto連携強化
+- [ ] ai.gpt ↔ ai.ai連携機能
+- [ ] 分散SNS ↔ ゲーム同期
+
+### Phase 3: メタバース浸透
+- [ ] VTuber配信機能統合
+- [ ] Play-to-Work変換システム
+- [ ] ai.game device prototype
+
+## 将来的な連携構想
+
+### システム間連携(現在は独立実装)
+```
+ai.gpt (自律送信) ←→ ai.ai (心理分析)
+ai.card (iOS,Web,API) ←→ ai.verse (UEゲーム世界)
+```
+
+**共通基盤**: fastapi_mcp
+**共通思想**: yui system(現実の反映・唯一性担保)
+
+### データ改ざん防止戦略
+- **短期**: MCP serverによる検証
+- **中期**: OAuth 2.1 scope実装待ち
+- **長期**: ブロックチェーン的整合性チェック
+
+## AIコミュニケーション最適化
+
+### プロジェクト要件定義テンプレート
+```markdown
+# [プロジェクト名] 要件定義
+
+## 哲学的背景
+- 存在子理論との関連:
+- yui system適用範囲:
+- 現実反映の仕組み:
+
+## 技術要件
+- 使用技術(fastapi_mcp統一):
+- atproto連携方法:
+- データ永続化方法:
+
+## ユーザーストーリー
+1. ユーザーが...すると
+2. システムが...を実行し
+3. 結果として...が実現される
+
+## 成功指標
+- 技術的:
+- 哲学的(唯一性担保):
+```
+
+### Claude Code活用戦略
+1. **小さく始める**: ai.gptのMCP機能拡張から
+2. **段階的統合**: 各システムを個別に完成させてから統合
+3. **哲学的一貫性**: 各実装でyui systemとの整合性を確認
+4. **現実反映**: 実装がどう現実とゲームを繋ぐかを常に明記
+
+## 開発上の留意点
+
+### MCP Server設計指針
+- 各AI(gpt, card, ai, bot)は独立したMCPサーバー
+- fastapi_mcp基盤で統一
+- atproto DIDによる認証・認可
+
+### 記憶・データ管理
+- **ai.gpt**: 関係性の不可逆性重視
+- **ai.card**: ユーザーデータ主権重視
+- **ai.verse**: ゲーム世界の整合性重視
+
+### 唯一性担保実装
+- atproto accountとの1:1紐付け必須
+- 改変不可能性をハッシュ・署名で保証
+- 他システムでの再現不可能性を技術的に実現
+
+## 継続的改善
+- 各プロジェクトでこの設計書を参照
+- 新機能追加時はyui systemとの整合性をチェック
+- 他システムへの影響を事前評価
+- Claude Code導入時の段階的移行計画
+
+## ai.gpt深層設計思想
+
+### 人格の不可逆性
+- **関係性の破壊は修復不可能**: 現実の人間関係と同じ重み
+- **記憶の選択的忘却**: 重要でない情報は忘れるが、コア記憶は永続
+- **時間減衰**: すべてのパラメータは時間とともに自然減衰
+
+### AI運勢システム
+- 1-10のランダム値で日々の人格に変化
+- 連続した幸運/不運による突破条件
+- 環境要因としての人格形成
+
+### 記憶の階層構造
+1. **完全ログ**: すべての会話を記録
+2. **AI要約**: 重要な部分を抽出して圧縮
+3. **思想コア判定**: ユーザーの本質的な部分を特定
+4. **選択的忘却**: 重要度の低い情報を段階的に削除
+
+### 実装における重要な決定事項
+- **言語統一**: Python (fastapi_mcp) で統一、CLIはclick/typer
+- **データ形式**: JSON/SQLite選択式
+- **認証**: atproto DIDによる唯一性担保
+- **段階的実装**: まず会話→記憶→関係性→送信機能の順で実装
+
+### 送信機能の段階的実装
+- **Phase 1**: CLIでのprint出力(現在)
+- **Phase 2**: atproto直接投稿
+- **Phase 3**: ai.bot (Rust/seahorse) との連携
+- **将来**: マルチチャネル対応(SNS、Webhook等)
+
+## ai.gpt実装状況(2025/01/06)
+
+### 完成した機能
+- 階層的記憶システム(MemoryManager)
+- 不可逆的関係性システム(RelationshipTracker)
+- AI運勢システム(FortuneSystem)
+- 統合人格システム(Persona)
+- スケジューラー(5種類のタスク)
+- MCP Server(9種類のツール)
+- 設定管理(~/.config/aigpt/)
+- 全CLIコマンド実装
+
+### 次の開発ポイント
+- `ai_gpt/DEVELOPMENT_STATUS.md` を参照
+- 自律送信: transmission.pyでatproto実装
+- ai.bot連携: 新規bot_connector.py作成
+- テスト: tests/ディレクトリ追加
+
+## このprojectはai.log
+
+このprojectは`ai.log`にあたります。
+
+package, codeは`ailog`となります。
+
+```sh
+$ curl -sL https://git.syui.ai/ai/ai/raw/branch/main/ai.json|jq .ai.log
+{
+  "type": "blog",
+  "text": "今はhugoでblogを作ってる。それをclaude codeでrustの静的ブログジェネレーターを作る。AI機能を付け加える。AI機能は具体的に記事の修正、情報の追加、lang:jaを自動翻訳してlang:en(page)を生成。アイが一言コメントするコメント欄の追加を行う。なお、コメント欄はatprotoと連携し、atprotoアカウントのoauthでログインして書き込める"
+}
+```
+
+rustで静的ブログジェネレーターを作ります。参考になるのが`zola`です。
+
+- https://github.com/getzola/zola
+
+また、atprotoとの連携は`ai.card`の`atproto/oauth`の実装が参考になります。
+
+- https://github.com/bluesky-social/atproto/blob/main/packages/api/OAUTH.md
+
+```json
+{
+  "client_id": "https://example.com/client-metadata.json",
+  "client_name": "Example atproto Browser App",
+  "client_uri": "https://example.com",
+  "logo_uri": "https://example.com/logo.png",
+  "tos_uri": "https://example.com/tos",
+  "policy_uri": "https://example.com/policy",
+  "redirect_uris": ["https://example.com/callback"],
+  "scope": "atproto",
+  "grant_types": ["authorization_code", "refresh_token"],
+  "response_types": ["code"],
+  "token_endpoint_auth_method": "none",
+  "application_type": "web",
+  "dpop_bound_access_tokens": true
+}
+```
+
+```js
+// package
+import { Agent } from '@atproto/api'
+import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
+
+async function main() {
+  const oauthClient = await BrowserOAuthClient.load({
+    clientId: '<YOUR_CLIENT_ID>',
+    handleResolver: 'https://bsky.social/',
+  })
+
+  // TO BE CONTINUED
+}
+
+document.addEventListener('DOMContentLoaded', main)
+
+// client 
+const result = await oauthClient.init()
+
+if (result) {
+  if ('state' in result) {
+    console.log('The user was just redirected back from the authorization page')
+  }
+
+  console.log(`The user is currently signed in as ${result.session.did}`)
+}
+
+const session = result?.session
+
+// session
+if (session) {
+  const agent = new Agent(session)
+
+  const fetchProfile = async () => {
+    const profile = await agent.getProfile({ actor: agent.did })
+    return profile.data
+  }
+
+  // Update the user interface
+
+  document.body.textContent = `Authenticated as ${agent.did}`
+
+  const profileBtn = document.createElement('button')
+  document.body.appendChild(profileBtn)
+  profileBtn.textContent = 'Fetch Profile'
+  profileBtn.onclick = async () => {
+    const profile = await fetchProfile()
+    outputPre.textContent = JSON.stringify(profile, null, 2)
+  }
+
+  const logoutBtn = document.createElement('button')
+  document.body.appendChild(logoutBtn)
+  logoutBtn.textContent = 'Logout'
+  logoutBtn.onclick = async () => {
+    await session.signOut()
+    window.location.reload()
+  }
+
+  const outputPre = document.createElement('pre')
+  document.body.appendChild(outputPre)
+}
+```
+
+AIとの連携は`ai.gpt`をみてください。
+
+- https://git.syui.ai/ai/gpt
+
+`claude.md`があるので、`../gpt/claude.md`を読み込んでください。
+
+### build deploy
+
+主に`github-actions`, `cloudflare pages`を使用することを想定しています。
+
+build, deploy, AIとの連携は記事をpushすると、自動で行われます。
+
+```yml
+// .github/workflows/gh-pages.yml
+name: github pages
+
+on:
+  push:
+    branches:
+    - main
+
+jobs:
+  build-deploy:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - name: Setup Hugo
+      uses: peaceiris/actions-hugo@v3
+      with:
+        hugo-version: "0.139.2"
+        extended: true
+
+    - name: Build
+      env: 
+        TZ: "Asia/Tokyo"
+      run: |
+          hugo version
+          TZ=Asia/Tokyo hugo
+          touch ./public/.nojekyll
+
+    - name: Deploy
+      uses: peaceiris/actions-gh-pages@v3
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        publish_dir: ./public
+        publish_branch: gh-pages
+```
+
+# footer
+
+© syui
diff --git a/src/commands/build.rs b/src/commands/build.rs
new file mode 100644
index 0000000..985a2b1
--- /dev/null
+++ b/src/commands/build.rs
@@ -0,0 +1,22 @@
+use anyhow::Result;
+use colored::Colorize;
+use std::path::PathBuf;
+use crate::generator::Generator;
+use crate::config::Config;
+
+pub async fn execute(path: PathBuf) -> Result<()> {
+    println!("{}", "Building blog...".green());
+
+    // Load configuration
+    let config = Config::load(&path)?;
+    
+    // Create generator
+    let generator = Generator::new(path, config)?;
+    
+    // Build the site
+    generator.build().await?;
+    
+    println!("{}", "Build completed successfully!".green().bold());
+    
+    Ok(())
+}
\ No newline at end of file
diff --git a/src/commands/clean.rs b/src/commands/clean.rs
new file mode 100644
index 0000000..35fa464
--- /dev/null
+++ b/src/commands/clean.rs
@@ -0,0 +1,21 @@
+use anyhow::Result;
+use colored::Colorize;
+use std::fs;
+use std::path::Path;
+
+pub async fn execute() -> Result<()> {
+    println!("{}", "Cleaning build artifacts...".yellow());
+
+    let public_dir = Path::new("public");
+    
+    if public_dir.exists() {
+        fs::remove_dir_all(public_dir)?;
+        println!("{} public directory", "Removed".cyan());
+    } else {
+        println!("{}", "No build artifacts to clean");
+    }
+
+    println!("{}", "Clean completed!".green().bold());
+    
+    Ok(())
+}
\ No newline at end of file
diff --git a/src/commands/init.rs b/src/commands/init.rs
new file mode 100644
index 0000000..57fcb03
--- /dev/null
+++ b/src/commands/init.rs
@@ -0,0 +1,216 @@
+use anyhow::Result;
+use colored::Colorize;
+use std::fs;
+use std::path::PathBuf;
+
+pub async fn execute(path: PathBuf) -> Result<()> {
+    println!("{}", "Initializing new blog...".green());
+
+    // Create directory structure
+    let dirs = vec![
+        "content",
+        "content/posts",
+        "templates",
+        "static",
+        "static/css",
+        "static/js",
+        "static/images",
+        "public",
+    ];
+
+    for dir in dirs {
+        let dir_path = path.join(dir);
+        fs::create_dir_all(&dir_path)?;
+        println!("  {} {}", "Created".cyan(), dir_path.display());
+    }
+
+    // Create default config
+    let config_content = r#"[site]
+title = "My Blog"
+description = "A blog powered by ailog"
+base_url = "https://example.com"
+language = "ja"
+
+[build]
+highlight_code = true
+minify = false
+
+[ai]
+enabled = false
+auto_translate = false
+comment_moderation = false
+"#;
+
+    fs::write(path.join("config.toml"), config_content)?;
+    println!("  {} config.toml", "Created".cyan());
+
+    // Create default template
+    let base_template = r#"<!DOCTYPE html>
+<html lang="{{ config.language }}">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}{{ config.title }}{% endblock %}</title>
+    <link rel="stylesheet" href="/css/style.css">
+</head>
+<body>
+    <header>
+        <h1><a href="/">{{ config.title }}</a></h1>
+        <p>{{ config.description }}</p>
+    </header>
+    
+    <main>
+        {% block content %}{% endblock %}
+    </main>
+    
+    <footer>
+        <p>&copy; 2025 {{ config.title }}</p>
+    </footer>
+</body>
+</html>"#;
+
+    fs::write(path.join("templates/base.html"), base_template)?;
+    println!("  {} templates/base.html", "Created".cyan());
+
+    let index_template = r#"{% extends "base.html" %}
+
+{% block content %}
+<h2>Recent Posts</h2>
+<ul class="post-list">
+    {% for post in posts %}
+    <li>
+        <a href="{{ post.url }}">{{ post.title }}</a>
+        <time>{{ post.date }}</time>
+    </li>
+    {% endfor %}
+</ul>
+{% endblock %}"#;
+
+    fs::write(path.join("templates/index.html"), index_template)?;
+    println!("  {} templates/index.html", "Created".cyan());
+
+    let post_template = r#"{% extends "base.html" %}
+
+{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
+
+{% block content %}
+<article>
+    <h1>{{ post.title }}</h1>
+    <time>{{ post.date }}</time>
+    <div class="content">
+        {{ post.content | safe }}
+    </div>
+</article>
+{% endblock %}"#;
+
+    fs::write(path.join("templates/post.html"), post_template)?;
+    println!("  {} templates/post.html", "Created".cyan());
+
+    // Create default CSS
+    let css_content = r#"body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+    line-height: 1.6;
+    color: #333;
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 20px;
+}
+
+header {
+    margin-bottom: 40px;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 20px;
+}
+
+header h1 {
+    margin: 0;
+}
+
+header h1 a {
+    color: #333;
+    text-decoration: none;
+}
+
+.post-list {
+    list-style: none;
+    padding: 0;
+}
+
+.post-list li {
+    margin-bottom: 15px;
+}
+
+.post-list time {
+    color: #666;
+    font-size: 0.9em;
+    margin-left: 10px;
+}
+
+article time {
+    color: #666;
+    display: block;
+    margin-bottom: 20px;
+}
+
+pre {
+    background-color: #f4f4f4;
+    padding: 15px;
+    border-radius: 5px;
+    overflow-x: auto;
+}
+
+code {
+    background-color: #f4f4f4;
+    padding: 2px 5px;
+    border-radius: 3px;
+    font-family: 'Consolas', 'Monaco', monospace;
+}"#;
+
+    fs::write(path.join("static/css/style.css"), css_content)?;
+    println!("  {} static/css/style.css", "Created".cyan());
+
+    // Create sample post
+    let sample_post = r#"---
+title: "Welcome to ailog"
+date: 2025-01-06
+tags: ["welcome", "ailog"]
+---
+
+# Welcome to ailog
+
+This is your first post powered by **ailog** - a static blog generator with AI features.
+
+## Features
+
+- Fast static site generation
+- Markdown support with frontmatter
+- AI-powered features (coming soon)
+- atproto integration for comments
+
+## Getting Started
+
+Create new posts with:
+
+```bash
+ailog new "My New Post"
+```
+
+Build your blog with:
+
+```bash
+ailog build
+```
+
+Happy blogging!"#;
+
+    fs::write(path.join("content/posts/welcome.md"), sample_post)?;
+    println!("  {} content/posts/welcome.md", "Created".cyan());
+
+    println!("\n{}", "Blog initialized successfully!".green().bold());
+    println!("\nNext steps:");
+    println!("  1. cd {}", path.display());
+    println!("  2. ailog build");
+    println!("  3. ailog serve");
+
+    Ok(())
+}
\ No newline at end of file
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
new file mode 100644
index 0000000..7308f56
--- /dev/null
+++ b/src/commands/mod.rs
@@ -0,0 +1,5 @@
+pub mod init;
+pub mod build;
+pub mod new;
+pub mod serve;
+pub mod clean;
\ No newline at end of file
diff --git a/src/commands/new.rs b/src/commands/new.rs
new file mode 100644
index 0000000..d36509d
--- /dev/null
+++ b/src/commands/new.rs
@@ -0,0 +1,48 @@
+use anyhow::Result;
+use chrono::Local;
+use colored::Colorize;
+use std::fs;
+use std::path::PathBuf;
+
+pub async fn execute(title: String, format: String) -> Result<()> {
+    println!("{} {}", "Creating new post:".green(), title);
+
+    let date = Local::now();
+    let filename = format!(
+        "{}-{}.{}",
+        date.format("%Y-%m-%d"),
+        title.to_lowercase().replace(' ', "-"),
+        format
+    );
+
+    let content = format!(
+        r#"---
+title: "{}"
+date: {}
+tags: []
+draft: false
+---
+
+# {}
+
+Write your content here...
+"#,
+        title,
+        date.format("%Y-%m-%d"),
+        title
+    );
+
+    let post_path = PathBuf::from("content/posts").join(&filename);
+    
+    // Ensure directory exists
+    if let Some(parent) = post_path.parent() {
+        fs::create_dir_all(parent)?;
+    }
+
+    fs::write(&post_path, content)?;
+    
+    println!("{} {}", "Created:".cyan(), post_path.display());
+    println!("\nYou can now edit your post at: {}", post_path.display());
+
+    Ok(())
+}
\ No newline at end of file
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
new file mode 100644
index 0000000..e8c69c8
--- /dev/null
+++ b/src/commands/serve.rs
@@ -0,0 +1,77 @@
+use anyhow::Result;
+use colored::Colorize;
+use std::path::PathBuf;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net::{TcpListener, TcpStream};
+
+pub async fn execute(port: u16) -> Result<()> {
+    let addr = format!("127.0.0.1:{}", port);
+    let listener = TcpListener::bind(&addr).await?;
+    
+    println!("{}", "Starting development server...".green());
+    println!("Serving at: {}", format!("http://{}", addr).blue().underline());
+    println!("Press Ctrl+C to stop\n");
+
+    loop {
+        let (stream, _) = listener.accept().await?;
+        tokio::spawn(handle_connection(stream));
+    }
+}
+
+async fn handle_connection(mut stream: TcpStream) -> Result<()> {
+    let mut buffer = [0; 1024];
+    stream.read(&mut buffer).await?;
+
+    let request = String::from_utf8_lossy(&buffer[..]);
+    let path = parse_request_path(&request);
+
+    let (status, content_type, content) = match serve_file(&path).await {
+        Ok((ct, data)) => ("200 OK", ct, data),
+        Err(_) => ("404 NOT FOUND", "text/html", b"<h1>404 - Not Found</h1>".to_vec()),
+    };
+
+    let response = format!(
+        "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n",
+        status,
+        content_type,
+        content.len()
+    );
+
+    stream.write_all(response.as_bytes()).await?;
+    stream.write_all(&content).await?;
+    stream.flush().await?;
+
+    Ok(())
+}
+
+fn parse_request_path(request: &str) -> String {
+    request
+        .lines()
+        .next()
+        .and_then(|line| line.split_whitespace().nth(1))
+        .unwrap_or("/")
+        .to_string()
+}
+
+async fn serve_file(path: &str) -> Result<(&'static str, Vec<u8>)> {
+    let file_path = if path == "/" {
+        PathBuf::from("public/index.html")
+    } else {
+        PathBuf::from("public").join(path.trim_start_matches('/'))
+    };
+
+    let content_type = match file_path.extension().and_then(|ext| ext.to_str()) {
+        Some("html") => "text/html",
+        Some("css") => "text/css",
+        Some("js") => "application/javascript",
+        Some("json") => "application/json",
+        Some("png") => "image/png",
+        Some("jpg") | Some("jpeg") => "image/jpeg",
+        Some("gif") => "image/gif",
+        Some("svg") => "image/svg+xml",
+        _ => "text/plain",
+    };
+
+    let content = tokio::fs::read(file_path).await?;
+    Ok((content_type, content))
+}
\ No newline at end of file
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..32ecf0b
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,63 @@
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::path::Path;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Config {
+    pub site: SiteConfig,
+    pub build: BuildConfig,
+    pub ai: Option<AiConfig>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct SiteConfig {
+    pub title: String,
+    pub description: String,
+    pub base_url: String,
+    pub language: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct BuildConfig {
+    pub highlight_code: bool,
+    pub minify: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct AiConfig {
+    pub enabled: bool,
+    pub auto_translate: bool,
+    pub comment_moderation: bool,
+}
+
+impl Config {
+    pub fn load(path: &Path) -> Result<Self> {
+        let config_path = path.join("config.toml");
+        let content = fs::read_to_string(config_path)?;
+        let config: Config = toml::from_str(&content)?;
+        Ok(config)
+    }
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self {
+            site: SiteConfig {
+                title: "My Blog".to_string(),
+                description: "A blog powered by ailog".to_string(),
+                base_url: "https://example.com".to_string(),
+                language: "ja".to_string(),
+            },
+            build: BuildConfig {
+                highlight_code: true,
+                minify: false,
+            },
+            ai: Some(AiConfig {
+                enabled: false,
+                auto_translate: false,
+                comment_moderation: false,
+            }),
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/generator.rs b/src/generator.rs
new file mode 100644
index 0000000..7d95e85
--- /dev/null
+++ b/src/generator.rs
@@ -0,0 +1,178 @@
+use anyhow::Result;
+use colored::Colorize;
+use std::path::PathBuf;
+use walkdir::WalkDir;
+use std::fs;
+use crate::config::Config;
+use crate::markdown::MarkdownProcessor;
+use crate::template::TemplateEngine;
+
+pub struct Generator {
+    base_path: PathBuf,
+    config: Config,
+    markdown_processor: MarkdownProcessor,
+    template_engine: TemplateEngine,
+}
+
+impl Generator {
+    pub fn new(base_path: PathBuf, config: Config) -> Result<Self> {
+        let markdown_processor = MarkdownProcessor::new(config.build.highlight_code);
+        let template_engine = TemplateEngine::new(base_path.join("templates"))?;
+
+        Ok(Self {
+            base_path,
+            config,
+            markdown_processor,
+            template_engine,
+        })
+    }
+
+    pub async fn build(&self) -> Result<()> {
+        // Clean public directory
+        let public_dir = self.base_path.join("public");
+        if public_dir.exists() {
+            fs::remove_dir_all(&public_dir)?;
+        }
+        fs::create_dir_all(&public_dir)?;
+
+        // Copy static files
+        self.copy_static_files()?;
+
+        // Process posts
+        let posts = self.process_posts().await?;
+
+        // Generate index page
+        self.generate_index(&posts).await?;
+
+        // Generate post pages
+        for post in &posts {
+            self.generate_post_page(post).await?;
+        }
+
+        println!("{} {} posts", "Generated".cyan(), posts.len());
+
+        Ok(())
+    }
+
+    fn copy_static_files(&self) -> Result<()> {
+        let static_dir = self.base_path.join("static");
+        let public_dir = self.base_path.join("public");
+
+        if static_dir.exists() {
+            for entry in WalkDir::new(&static_dir).min_depth(1) {
+                let entry = entry?;
+                let path = entry.path();
+                let relative_path = path.strip_prefix(&static_dir)?;
+                let dest_path = public_dir.join(relative_path);
+
+                if path.is_dir() {
+                    fs::create_dir_all(&dest_path)?;
+                } else {
+                    if let Some(parent) = dest_path.parent() {
+                        fs::create_dir_all(parent)?;
+                    }
+                    fs::copy(path, &dest_path)?;
+                }
+            }
+            println!("{} static files", "Copied".cyan());
+        }
+
+        Ok(())
+    }
+
+    async fn process_posts(&self) -> Result<Vec<Post>> {
+        let mut posts = Vec::new();
+        let posts_dir = self.base_path.join("content/posts");
+
+        if posts_dir.exists() {
+            for entry in WalkDir::new(&posts_dir).min_depth(1) {
+                let entry = entry?;
+                let path = entry.path();
+
+                if path.is_file() && path.extension().map_or(false, |ext| ext == "md") {
+                    match self.process_single_post(path).await {
+                        Ok(post) => posts.push(post),
+                        Err(e) => eprintln!("Error processing {}: {}", path.display(), e),
+                    }
+                }
+            }
+        }
+
+        // Sort posts by date (newest first)
+        posts.sort_by(|a, b| b.date.cmp(&a.date));
+
+        Ok(posts)
+    }
+
+    async fn process_single_post(&self, path: &std::path::Path) -> Result<Post> {
+        let content = fs::read_to_string(path)?;
+        let (frontmatter, content) = self.markdown_processor.parse_frontmatter(&content)?;
+        
+        let html_content = self.markdown_processor.render(&content)?;
+        
+        let slug = path
+            .file_stem()
+            .and_then(|s| s.to_str())
+            .unwrap_or("post")
+            .to_string();
+
+        let post = Post {
+            title: frontmatter.get("title")
+                .and_then(|v| v.as_str())
+                .unwrap_or("Untitled")
+                .to_string(),
+            date: frontmatter.get("date")
+                .and_then(|v| v.as_str())
+                .unwrap_or("")
+                .to_string(),
+            content: html_content,
+            slug: slug.clone(),
+            url: format!("/posts/{}.html", slug),
+            tags: frontmatter.get("tags")
+                .and_then(|v| v.as_array())
+                .map(|arr| arr.iter()
+                    .filter_map(|v| v.as_str())
+                    .map(|s| s.to_string())
+                    .collect())
+                .unwrap_or_default(),
+        };
+
+        Ok(post)
+    }
+
+    async fn generate_index(&self, posts: &[Post]) -> Result<()> {
+        let context = self.template_engine.create_context(&self.config, posts)?;
+        let html = self.template_engine.render("index.html", &context)?;
+        
+        let output_path = self.base_path.join("public/index.html");
+        fs::write(output_path, html)?;
+
+        Ok(())
+    }
+
+    async fn generate_post_page(&self, post: &Post) -> Result<()> {
+        let mut context = tera::Context::new();
+        context.insert("config", &self.config.site);
+        context.insert("post", post);
+
+        let html = self.template_engine.render_with_context("post.html", &context)?;
+        
+        let output_dir = self.base_path.join("public/posts");
+        fs::create_dir_all(&output_dir)?;
+        
+        let output_path = output_dir.join(format!("{}.html", post.slug));
+        fs::write(output_path, html)?;
+
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct Post {
+    pub title: String,
+    pub date: String,
+    pub content: String,
+    pub slug: String,
+    pub url: String,
+    pub tags: Vec<String>,
+}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..32d5c1c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,75 @@
+use anyhow::Result;
+use clap::{Parser, Subcommand};
+use std::path::PathBuf;
+
+mod commands;
+mod generator;
+mod markdown;
+mod template;
+mod config;
+
+#[derive(Parser)]
+#[command(name = "ailog")]
+#[command(about = "A static blog generator with AI features")]
+#[command(version)]
+struct Cli {
+    #[command(subcommand)]
+    command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+    /// Initialize a new blog
+    Init {
+        /// Path to create the blog
+        #[arg(default_value = ".")]
+        path: PathBuf,
+    },
+    /// Build the blog
+    Build {
+        /// Path to the blog directory
+        #[arg(default_value = ".")]
+        path: PathBuf,
+    },
+    /// Create a new post
+    New {
+        /// Title of the post
+        title: String,
+        /// Post format
+        #[arg(short, long, default_value = "md")]
+        format: String,
+    },
+    /// Serve the blog locally
+    Serve {
+        /// Port to serve on
+        #[arg(short, long, default_value = "8080")]
+        port: u16,
+    },
+    /// Clean build artifacts
+    Clean,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let cli = Cli::parse();
+
+    match cli.command {
+        Commands::Init { path } => {
+            commands::init::execute(path).await?;
+        }
+        Commands::Build { path } => {
+            commands::build::execute(path).await?;
+        }
+        Commands::New { title, format } => {
+            commands::new::execute(title, format).await?;
+        }
+        Commands::Serve { port } => {
+            commands::serve::execute(port).await?;
+        }
+        Commands::Clean => {
+            commands::clean::execute().await?;
+        }
+    }
+
+    Ok(())
+}
\ No newline at end of file
diff --git a/src/markdown.rs b/src/markdown.rs
new file mode 100644
index 0000000..7126e34
--- /dev/null
+++ b/src/markdown.rs
@@ -0,0 +1,138 @@
+use anyhow::Result;
+use pulldown_cmark::{html, Options, Parser, CodeBlockKind};
+use syntect::parsing::SyntaxSet;
+use syntect::highlighting::ThemeSet;
+use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
+use gray_matter::Matter;
+use gray_matter::engine::YAML;
+use serde_json::Value;
+
+pub struct MarkdownProcessor {
+    highlight_code: bool,
+    syntax_set: SyntaxSet,
+    theme_set: ThemeSet,
+}
+
+impl MarkdownProcessor {
+    pub fn new(highlight_code: bool) -> Self {
+        Self {
+            highlight_code,
+            syntax_set: SyntaxSet::load_defaults_newlines(),
+            theme_set: ThemeSet::load_defaults(),
+        }
+    }
+
+    pub fn parse_frontmatter(&self, content: &str) -> Result<(serde_json::Map<String, Value>, String)> {
+        let matter = Matter::<YAML>::new();
+        let result = matter.parse(content);
+        
+        let frontmatter = result.data
+            .and_then(|pod| pod.as_hashmap().ok())
+            .map(|map| {
+                let mut json_map = serde_json::Map::new();
+                for (k, v) in map {
+                    // Keys in hashmap are already strings
+                    let value = self.pod_to_json_value(v);
+                    json_map.insert(k, value);
+                }
+                json_map
+            })
+            .unwrap_or_default();
+
+        Ok((frontmatter, result.content))
+    }
+
+    fn pod_to_json_value(&self, pod: gray_matter::Pod) -> Value {
+        match pod {
+            gray_matter::Pod::Null => Value::Null,
+            gray_matter::Pod::Boolean(b) => Value::Bool(b),
+            gray_matter::Pod::Integer(i) => Value::Number(serde_json::Number::from(i)),
+            gray_matter::Pod::Float(f) => serde_json::Number::from_f64(f)
+                .map(Value::Number)
+                .unwrap_or(Value::Null),
+            gray_matter::Pod::String(s) => Value::String(s),
+            gray_matter::Pod::Array(arr) => {
+                Value::Array(arr.into_iter().map(|p| self.pod_to_json_value(p)).collect())
+            }
+            gray_matter::Pod::Hash(map) => {
+                let mut json_map = serde_json::Map::new();
+                for (k, v) in map {
+                    json_map.insert(k, self.pod_to_json_value(v));
+                }
+                Value::Object(json_map)
+            }
+        }
+    }
+
+
+    pub fn render(&self, content: &str) -> Result<String> {
+        let mut options = Options::empty();
+        options.insert(Options::ENABLE_STRIKETHROUGH);
+        options.insert(Options::ENABLE_TABLES);
+        options.insert(Options::ENABLE_FOOTNOTES);
+        options.insert(Options::ENABLE_TASKLISTS);
+
+        if self.highlight_code {
+            self.render_with_syntax_highlighting(content, options)
+        } else {
+            let parser = Parser::new_ext(content, options);
+            let mut html_output = String::new();
+            html::push_html(&mut html_output, parser);
+            Ok(html_output)
+        }
+    }
+
+    fn render_with_syntax_highlighting(&self, content: &str, options: Options) -> Result<String> {
+        let parser = Parser::new_ext(content, options);
+        let mut html_output = String::new();
+        let mut code_block = None;
+        let theme = &self.theme_set.themes["base16-ocean.dark"];
+
+        let mut events = Vec::new();
+        for event in parser {
+            match event {
+                pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(kind)) => {
+                    if let CodeBlockKind::Fenced(lang) = &kind {
+                        code_block = Some((String::new(), lang.to_string()));
+                    }
+                }
+                pulldown_cmark::Event::Text(text) => {
+                    if let Some((ref mut code, _)) = code_block {
+                        code.push_str(&text);
+                    } else {
+                        events.push(pulldown_cmark::Event::Text(text));
+                    }
+                }
+                pulldown_cmark::Event::End(pulldown_cmark::TagEnd::CodeBlock) => {
+                    if let Some((code, lang)) = code_block.take() {
+                        let highlighted = self.highlight_code_block(&code, &lang, theme);
+                        events.push(pulldown_cmark::Event::Html(highlighted.into()));
+                    }
+                }
+                _ => events.push(event),
+            }
+        }
+
+        html::push_html(&mut html_output, events.into_iter());
+        Ok(html_output)
+    }
+
+    fn highlight_code_block(&self, code: &str, lang: &str, theme: &syntect::highlighting::Theme) -> String {
+        let syntax = self.syntax_set
+            .find_syntax_by_token(lang)
+            .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
+
+        let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme);
+        let mut output = String::from("<pre><code>");
+
+        for line in code.lines() {
+            let ranges = highlighter.highlight_line(line, &self.syntax_set).unwrap();
+            let html_line = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No).unwrap();
+            output.push_str(&html_line);
+            output.push('\n');
+        }
+
+        output.push_str("</code></pre>");
+        output
+    }
+}
\ No newline at end of file
diff --git a/src/template.rs b/src/template.rs
new file mode 100644
index 0000000..17bf582
--- /dev/null
+++ b/src/template.rs
@@ -0,0 +1,35 @@
+use anyhow::Result;
+use tera::{Tera, Context};
+use std::path::PathBuf;
+use crate::config::Config;
+use crate::generator::Post;
+
+pub struct TemplateEngine {
+    tera: Tera,
+}
+
+impl TemplateEngine {
+    pub fn new(template_dir: PathBuf) -> Result<Self> {
+        let pattern = format!("{}/**/*.html", template_dir.display());
+        let tera = Tera::new(&pattern)?;
+        
+        Ok(Self { tera })
+    }
+
+    pub fn create_context(&self, config: &Config, posts: &[Post]) -> Result<Context> {
+        let mut context = Context::new();
+        context.insert("config", &config.site);
+        context.insert("posts", posts);
+        Ok(context)
+    }
+
+    pub fn render(&self, template: &str, context: &Context) -> Result<String> {
+        let output = self.tera.render(template, context)?;
+        Ok(output)
+    }
+
+    pub fn render_with_context(&self, template: &str, context: &Context) -> Result<String> {
+        let output = self.tera.render(template, context)?;
+        Ok(output)
+    }
+}
\ No newline at end of file