From 93b523b1bafcb55cc337a7cd19cc6eb5a0d71478 Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 29 Jul 2025 03:29:46 +0900 Subject: [PATCH] cleanup update --- .claude/settings.local.json | 61 -- .env.example | 5 - .gitignore | 32 +- .gitmodules | 10 - Cargo.toml | 32 +- README.md | 267 +++-- claude.md | 128 +-- config.json.example | 55 - json/chatgpt.json | 391 ------- scpt/test_commands.sh | 26 - scpt/test_completion.sh | 19 - scpt/test_shell.sh | 18 - scpt/test_shell_manual.sh | 22 - src/ai_provider.rs | 246 ----- src/cli/commands.rs | 74 -- src/cli/mod.rs | 134 --- src/config.rs | 251 ----- src/conversation.rs | 205 ---- src/docs.rs | 789 -------------- src/http_client.rs | 409 -------- src/import.rs | 331 ------ src/lib.rs | 20 - src/main.rs | 250 +---- src/mcp.rs | 250 +++++ src/mcp_server.rs | 1951 ----------------------------------- src/memory.rs | 494 ++++----- src/openai_provider.rs | 599 ----------- src/persona.rs | 382 ------- src/relationship.rs | 306 ------ src/scheduler.rs | 458 -------- src/shell.rs | 608 ----------- src/status.rs | 51 - src/submodules.rs | 480 --------- src/tokens.rs | 1099 -------------------- src/transmission.rs | 423 -------- 35 files changed, 703 insertions(+), 10173 deletions(-) delete mode 100644 .claude/settings.local.json delete mode 100644 .env.example delete mode 100644 .gitmodules delete mode 100644 config.json.example delete mode 100644 json/chatgpt.json delete mode 100755 scpt/test_commands.sh delete mode 100755 scpt/test_completion.sh delete mode 100644 scpt/test_shell.sh delete mode 100755 scpt/test_shell_manual.sh delete mode 100644 src/ai_provider.rs delete mode 100644 src/cli/commands.rs delete mode 100644 src/cli/mod.rs delete mode 100644 src/config.rs delete mode 100644 src/conversation.rs delete mode 100644 src/docs.rs delete mode 100644 src/http_client.rs delete mode 100644 src/import.rs delete mode 100644 src/lib.rs create mode 100644 src/mcp.rs delete mode 100644 src/mcp_server.rs delete mode 100644 src/openai_provider.rs delete mode 100644 src/persona.rs delete mode 100644 src/relationship.rs delete mode 100644 src/scheduler.rs delete mode 100644 src/shell.rs delete mode 100644 src/status.rs delete mode 100644 src/submodules.rs delete mode 100644 src/tokens.rs delete mode 100644 src/transmission.rs diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 8d55a09..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(mv:*)", - "Bash(mkdir:*)", - "Bash(chmod:*)", - "Bash(git submodule:*)", - "Bash(source:*)", - "Bash(pip install:*)", - "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt shell)", - "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt server --model qwen2.5-coder:7b --port 8001)", - "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/python -c \"import fastapi_mcp; help(fastapi_mcp.FastApiMCP)\")", - "Bash(find:*)", - "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/pip install -e .)", - "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/aigpt fortune)", - "Bash(lsof:*)", - "Bash(/Users/syui/.config/syui/ai/gpt/venv/bin/python -c \"\nfrom src.aigpt.mcp_server import AIGptMcpServer\nfrom pathlib import Path\nimport uvicorn\n\ndata_dir = Path.home() / '.config' / 'syui' / 'ai' / 'gpt' / 'data'\ndata_dir.mkdir(parents=True, exist_ok=True)\n\ntry:\n server = AIGptMcpServer(data_dir)\n print('MCP Server created successfully')\n print('Available endpoints:', [route.path for route in server.app.routes])\nexcept Exception as e:\n print('Error:', e)\n import traceback\n traceback.print_exc()\n\")", - "Bash(ls:*)", - "Bash(grep:*)", - "Bash(python -m pip install:*)", - "Bash(python:*)", - "Bash(RELOAD=false ./start_server.sh)", - "Bash(sed:*)", - "Bash(curl:*)", - "Bash(~/.config/syui/ai/card/venv/bin/pip install greenlet)", - "Bash(~/.config/syui/ai/card/venv/bin/python init_db.py)", - "Bash(sqlite3:*)", - "Bash(aigpt --help)", - "Bash(aigpt status)", - "Bash(aigpt fortune)", - "Bash(aigpt relationships)", - "Bash(aigpt transmit)", - "Bash(aigpt config:*)", - "Bash(kill:*)", - "Bash(timeout:*)", - "Bash(rm:*)", - "Bash(rg:*)", - "Bash(aigpt server --help)", - "Bash(cat:*)", - "Bash(aigpt import-chatgpt:*)", - "Bash(aigpt chat:*)", - "Bash(echo:*)", - "Bash(aigpt shell:*)", - "Bash(aigpt maintenance)", - "Bash(aigpt status syui)", - "Bash(cp:*)", - "Bash(./setup_venv.sh:*)", - "WebFetch(domain:docs.anthropic.com)", - "Bash(launchctl:*)", - "Bash(sudo lsof:*)", - "Bash(sudo:*)", - "Bash(cargo check:*)", - "Bash(cargo run:*)", - "Bash(cargo test:*)", - "Bash(diff:*)", - "Bash(cargo:*)", - "Bash(pkill:*)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index 3e4bb7b..0000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# OpenAI API Key (required for OpenAI provider) -OPENAI_API_KEY=your-api-key-here - -# Ollama settings (optional) -OLLAMA_HOST=http://localhost:11434 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 944b959..2ae6464 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,23 @@ -**target -**.lock -output.json -config/*.db -mcp/scripts/__* -data -__pycache__ -conversations.json -json/*.zip -json/*/* +# Rust +target/ +Cargo.lock + +# Database files +*.db +*.db-shm +*.db-wal + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs *.log +json +gpt diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index cfeb1e5..0000000 --- a/.gitmodules +++ /dev/null @@ -1,10 +0,0 @@ -[submodule "shell"] - path = shell - url = git@git.syui.ai:ai/shell -[submodule "card"] - path = card - url = git@git.syui.ai:ai/card - branch = claude -[submodule "log"] - path = log - url = git@git.syui.ai:ai/log diff --git a/Cargo.toml b/Cargo.toml index 34da612..0733506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,32 +2,26 @@ name = "aigpt" version = "0.1.0" edition = "2021" -description = "AI.GPT - Autonomous transmission AI with unique personality (Rust implementation)" authors = ["syui"] +description = "Simple memory storage for Claude with MCP" [[bin]] name = "aigpt" path = "src/main.rs" [dependencies] -clap = { version = "4.0", features = ["derive"] } +# CLI and async +clap = { version = "4.5", features = ["derive"] } +tokio = { version = "1.40", features = ["full"] } + +# JSON and serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.0", features = ["full"] } -chrono = { version = "0.4", features = ["serde", "std"] } -chrono-tz = "0.8" -uuid = { version = "1.0", features = ["v4"] } -anyhow = "1.0" -colored = "2.0" -dirs = "5.0" -reqwest = { version = "0.11", features = ["json"] } -url = "2.4" -rustyline = "14.0" -axum = "0.7" -tower = "0.4" -tower-http = { version = "0.5", features = ["cors"] } -hyper = "1.0" -# OpenAI API client -async-openai = "0.23" -openai_api_rust = "0.1" +# Date/time and UUID +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.10", features = ["v4"] } + +# Error handling and utilities +anyhow = "1.0" +dirs = "5.0" diff --git a/README.md b/README.md index f40afc2..ca8bc21 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,177 @@ -# ai.gpt +# aigpt - Claude Memory MCP Server -## プロジェクト概要 -- **名前**: ai.gpt -- **パッケージ**: aigpt -- **言語**: Rust (完全移行済み) -- **タイプ**: 自律的送信AI + 統合MCP基盤 -- **役割**: 記憶・関係性・開発支援の統合AIシステム +ChatGPTのメモリ機能を参考にした、Claude Desktop/Code用のシンプルなメモリストレージシステムです。 -## 実装完了状況 +## 機能 -### 🧠 記憶システム(MemoryManager) -- **階層的記憶**: 完全ログ→AI要約→コア記憶→選択的忘却 -- **文脈検索**: キーワード・意味的検索 -- **記憶要約**: AI駆動自動要約機能 +- **メモリのCRUD操作**: メモリの作成、更新、削除、検索 +- **ChatGPT JSONインポート**: ChatGPTの会話履歴からメモリを抽出 +- **stdio MCP実装**: Claude Desktop/Codeとの簡潔な連携 +- **JSONファイル保存**: シンプルなファイルベースのデータ保存 -### 🤝 関係性システム(RelationshipTracker) -- **不可逆性**: 現実の人間関係と同じ重み -- **時間減衰**: 自然な関係性変化 -- **送信判定**: 関係性閾値による自発的コミュニケーション +## インストール -### 🎭 人格システム(Persona) -- **AI運勢**: 1-10ランダム値による日々の人格変動 -- **統合管理**: 記憶・関係性・運勢の統合判断 -- **継続性**: 長期記憶による人格継承 - -### 💻 ai.shell統合(Claude Code機能) -- **インタラクティブ環境**: `aigpt shell` -- **開発支援**: ファイル分析・コード生成・プロジェクト管理 -- **継続開発**: プロジェクト文脈保持 - -## MCP Server統合(17ツール) - -### 🧠 Memory System(5ツール) -- get_memories, get_contextual_memories, search_memories -- create_summary, create_core_memory - -### 🤝 Relationships(4ツール) -- get_relationships, get_status -- chat_with_ai, check_transmissions - -### 💻 Shell Integration(5ツール) -- execute_command, analyze_file, write_file -- list_files, run_scheduler - -### ⚙️ System State(3ツール) -- get_scheduler_status, run_maintenance, get_transmission_history - -### 🎴 ai.card連携(3ツール) -- get_user_cards, draw_card, get_draw_status -- **統合ServiceClient**: 統一されたHTTP通信基盤 - -### 📝 ai.log連携(新機能) -- **統合ServiceClient**: ai.logサービスとの統一インターフェース -- create_blog_post, build_blog, translate_document - -## 開発環境・設定 - -### 環境構築 +1. Rustをインストール(まだの場合): +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +2. プロジェクトをビルド: ```bash -cd /Users/syui/ai/ai/gpt cargo build --release ``` -### 設定管理 -- **メイン設定**: `/Users/syui/ai/ai/gpt/config.json.example` -- **データディレクトリ**: `~/.config/syui/ai/gpt/` - -### 使用方法 +3. バイナリをパスの通った場所にコピー(オプション): ```bash -# ai.shell起動 -aigpt shell --model qwen2.5-coder:latest --provider ollama - -# MCPサーバー起動 -aigpt server --port 8001 - -# 記憶システム体験 -aigpt chat syui "質問内容" --provider ollama --model qwen3:latest - -# ドキュメント生成(ai.wiki統合) -aigpt docs --wiki - -# トークン使用量・料金分析(Claude Code連携) -aigpt tokens report --days 7 # 美しい日別レポート(要DuckDB) -aigpt tokens cost --month today # セッション別料金分析 -aigpt tokens summary --period week # 基本的な使用量サマリー +cp target/release/aigpt $HOME/.cargo/bin/ ``` -## 技術アーキテクチャ +4. Claude Code/Desktopに追加 -### Rust実装の統合構成 -``` -ai.gpt (Rust製MCPサーバー:8001) -├── 🧠 Memory & Persona System (Rust) -├── 🤝 Relationship Management (Rust) -├── 📊 Scheduler & Transmission (Rust) -├── 💻 Shell Integration (Rust) -├── 🔗 ServiceClient (統一HTTP基盤) -│ ├── 🎴 ai.card (port 8000) -│ ├── 📝 ai.log (port 8002) -│ └── 🤖 ai.bot (port 8003) -└── 📚 ai.wiki Generator (Rust) +```sh +# Claude Codeの場合 +claude mcp add aigpt $HOME/.cargo/bin/aigpt server + +# または +claude mcp add aigpt $HOME/.cargo/bin/aigpt serve ``` -### 最新機能 (2024.06.09) -- **MCP API共通化**: ServiceClient統一基盤 -- **ai.wiki統合**: 自動ドキュメント生成 -- **サービス設定統一**: 動的サービス登録 -- **完全Rust移行**: Python依存完全排除 +## 使用方法 -### 今後の展開 -- **自律送信**: atproto実装による真の自発的コミュニケーション -- **ai.ai連携**: 心理分析AIとの統合 -- **分散SNS統合**: atproto完全対応 +### ヘルプの表示 +```bash +aigpt --help +``` -## 革新的な特徴 +### MCPサーバーとして起動 +```bash +# MCPサーバー起動 (どちらでも可) +aigpt server +aigpt serve +``` -### AI駆動記憶システム -- ChatGPT 4,000件ログから学習した効果的記憶構築 -- 人間的な忘却・重要度判定 +### ChatGPT会話のインポート +```bash +# ChatGPT conversations.jsonをインポート +aigpt import path/to/conversations.json +``` -### 不可逆関係性 -- 現実の人間関係と同じ重みを持つAI関係性 -- 修復不可能な関係性破綻システム +## Claude Desktop/Codeへの設定 -### 統合ServiceClient -- 複数AIサービスの統一インターフェース -- DRY原則に基づく共通化実現 -- 設定ベースの柔軟なサービス管理 +1. Claude Desktopの設定ファイルを開く: + - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` + - Windows: `%APPDATA%\Claude\claude_desktop_config.json` + - Linux: `~/.config/Claude/claude_desktop_config.json` -## アーカイブ情報 -詳細な実装履歴・設計資料は `~/ai/ai/ai.wiki/gpt/` に移動済み +2. 以下の設定を追加: +```json +{ + "mcpServers": { + "aigpt": { + "command": "/Users/syui/.cargo/bin/aigpt", + "args": ["server"] + } + } +} +``` + +## 提供するMCPツール一覧 + +1. **create_memory** - 新しいメモリを作成 +2. **update_memory** - 既存のメモリを更新 +3. **delete_memory** - メモリを削除 +4. **search_memories** - メモリを検索 +5. **list_conversations** - インポートされた会話を一覧表示 + +## ツールの使用例 + +Claude Desktop/Codeで以下のように使用します: + +### メモリの作成 +``` +MCPツールを使って「今日は良い天気です」というメモリーを作成してください +``` + +### メモリの検索 +``` +MCPツールを使って「天気」に関するメモリーを検索してください +``` + +### 会話一覧の表示 +``` +MCPツールを使ってインポートした会話の一覧を表示してください +``` + +## データ保存 + +- デフォルトパス: `~/.config/syui/ai/gpt/memory.json` +- JSONファイルでデータを保存 +- 自動的にディレクトリとファイルを作成 + +### データ構造 + +```json +{ + "memories": { + "uuid": { + "id": "uuid", + "content": "メモリーの内容", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + }, + "conversations": { + "conversation_id": { + "id": "conversation_id", + "title": "会話のタイトル", + "created_at": "2024-01-01T00:00:00Z", + "message_count": 10 + } + } +} +``` + +## 開発 + +```bash +# 開発モードで実行 +cargo run -- server + +# ChatGPTインポートのテスト +cargo run -- import json/conversations.json + +# テストの実行 +cargo test + +# フォーマット +cargo fmt + +# Lintチェック +cargo clippy +``` + +## トラブルシューティング + +### MCPサーバーが起動しない +```bash +# バイナリが存在するか確認 +ls -la ~/.cargo/bin/aigpt + +# 手動でテスト +echo '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' | aigpt server +``` + +### Claude Desktopでツールが見つからない +1. Claude Desktopを完全に再起動 +2. 設定ファイルのパスが正しいか確認 +3. ログファイルを確認: `~/Library/Logs/Claude/mcp-server-aigpt.log` + +### インポートが失敗する +```bash +# JSONファイルの形式を確認 +head -100 conversations.json | jq '.[0] | keys' +``` + +## ライセンス + +MIT diff --git a/claude.md b/claude.md index b6076bb..fe83fb2 100644 --- a/claude.md +++ b/claude.md @@ -1,115 +1,35 @@ -# ai.gpt プロジェクト固有情報 +# claude用の記憶装置を作る -## プロジェクト概要 -- **名前**: ai.gpt -- **パッケージ**: aigpt -- **タイプ**: 自律的送信AI + 統合MCP基盤 -- **役割**: 記憶・関係性・開発支援の統合AIシステム +claude desktop, claude codeで使用できるmemory機能をmcpで作ります。 -## 実装完了状況 +1. chatgptのメモリ機能を参考に +2. chatgptのjsonをimportできる @json/ +3. rustで作る -### 🧠 記憶システム(MemoryManager) -- **階層的記憶**: 完全ログ→AI要約→コア記憶→選択的忘却 -- **文脈検索**: キーワード・意味的検索 -- **記憶要約**: AI駆動自動要約機能 +## 自動メモリー保存のルール -### 🤝 関係性システム(RelationshipTracker) -- **不可逆性**: 現実の人間関係と同じ重み -- **時間減衰**: 自然な関係性変化 -- **送信判定**: 関係性閾値による自発的コミュニケーション +以下の情報が会話に現れた場合、自動的にcreate_memory MCPツールを使用して保存してください: -### 🎭 人格システム(Persona) -- **AI運勢**: 1-10ランダム値による日々の人格変動 -- **統合管理**: 記憶・関係性・運勢の統合判断 -- **継続性**: 長期記憶による人格継承 +1. **ユーザーの個人情報** + - 名前、誕生日、住所など + - 好きなもの、嫌いなもの + - 習慣や予定 -### 💻 ai.shell統合(Claude Code機能) -- **インタラクティブ環境**: `aigpt shell` -- **開発支援**: ファイル分析・コード生成・プロジェクト管理 -- **継続開発**: プロジェクト文脈保持 +2. **重要な決定事項** + - プロジェクトの決定 + - 設定の変更 + - 今後の計画 -## MCP Server統合(23ツール) +3. **技術的な解決策** + - 問題の解決方法 + - 有用なコマンドやコード + - 設定手順 -### 🧠 Memory System(5ツール) -- get_memories, get_contextual_memories, search_memories -- create_summary, create_core_memory +## 自動メモリー検索のルール -### 🤝 Relationships(4ツール) -- get_relationship, get_all_relationships -- process_interaction, check_transmission_eligibility +以下の場合、自動的にsearch_memories MCPツールを使用してください: -### 💻 Shell Integration(5ツール) -- execute_command, analyze_file, write_file -- read_project_file, list_files +1. ユーザーが「前に話した」「以前の」などと言及した場合 +2. 過去の会話や情報を参照する必要がある場合 +3. ユーザーの好みや設定を確認する必要がある場合 -### 🔒 Remote Execution(4ツール) -- remote_shell, ai_bot_status -- isolated_python, isolated_analysis - -### ⚙️ System State(3ツール) -- get_persona_state, get_fortune, run_maintenance - -### 🎴 ai.card連携(6ツール + 独立MCPサーバー) -- card_draw_card, card_get_user_cards, card_analyze_collection -- **独立サーバー**: FastAPI + MCP (port 8000) - -### 📝 ai.log連携(8ツール + Rustサーバー) -- log_create_post, log_ai_content, log_translate_document -- **独立サーバー**: Rust製 (port 8002) - -## 開発環境・設定 - -### 環境構築 -```bash -cd /Users/syui/ai/gpt -./setup_venv.sh -source ~/.config/syui/ai/gpt/venv/bin/activate -``` - -### 設定管理 -- **メイン設定**: `/Users/syui/ai/gpt/config.json` -- **データディレクトリ**: `~/.config/syui/ai/gpt/` -- **仮想環境**: `~/.config/syui/ai/gpt/venv/` - -### 使用方法 -```bash -# ai.shell起動 -aigpt shell --model qwen2.5-coder:latest --provider ollama - -# MCPサーバー起動 -aigpt server --port 8001 - -# 記憶システム体験 -aigpt chat syui "質問内容" --provider ollama --model qwen3:latest -``` - -## 技術アーキテクチャ - -### 統合構成 -``` -ai.gpt (統合MCPサーバー:8001) -├── 🧠 ai.gpt core (記憶・関係性・人格) -├── 💻 ai.shell (Claude Code風開発環境) -├── 🎴 ai.card (独立MCPサーバー:8000) -└── 📝 ai.log (Rust製ブログシステム:8002) -``` - -### 今後の展開 -- **自律送信**: atproto実装による真の自発的コミュニケーション -- **ai.ai連携**: 心理分析AIとの統合 -- **ai.verse統合**: UEメタバースとの連携 -- **分散SNS統合**: atproto完全対応 - -## 革新的な特徴 - -### AI駆動記憶システム -- ChatGPT 4,000件ログから学習した効果的記憶構築 -- 人間的な忘却・重要度判定 - -### 不可逆関係性 -- 現実の人間関係と同じ重みを持つAI関係性 -- 修復不可能な関係性破綻システム - -### 統合アーキテクチャ -- fastapi_mcp基盤での複数AIシステム統合 -- OpenAI Function Calling + MCP完全連携実証済み \ No newline at end of file diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 59ff9a4..0000000 --- a/config.json.example +++ /dev/null @@ -1,55 +0,0 @@ -{ - "providers": { - "openai": { - "api_key": "", - "default_model": "gpt-4o-mini", - "system_prompt": "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。\n\n重要:カード、コレクション、ガチャなどカード関連の質問を受けたら、必ずcard_get_user_cards、card_analyze_collection、card_draw_cardなどの適切なツールを使用してください。didパラメータには会話相手のユーザーID(例:'syui')を使用してください。\n\nブログ、記事、日記、思考などの話題が出たら、log_create_post、log_list_posts、log_build_blog、log_ai_contentなどのai.logツールを使用してください。AI記憶システムと連携して、思い出や学習内容をブログ記事として自動生成できます。\n\n翻訳や多言語対応について聞かれたら、log_translate_documentツールを使用してOllama AIで翻訳ができることを教えてください。日本語から英語、英語から日本語などの翻訳が可能で、マークダウン構造も保持します。ドキュメント生成についてはlog_generate_docsツールでREADME、API、構造、変更履歴の自動生成ができます。" - }, - "ollama": { - "host": "http://127.0.0.1:11434", - "default_model": "qwen3", - "system_prompt": null - } - }, - "atproto": { - "handle": null, - "password": null, - "host": "https://bsky.social" - }, - "default_provider": "openai", - "mcp": { - "servers": { - "ai_gpt": { - "base_url": "http://localhost:8080", - "name": "ai.gpt MCP Server", - "timeout": "10.0", - "endpoints": { - "get_memories": "/get_memories", - "search_memories": "/search_memories", - "get_contextual_memories": "/get_contextual_memories", - "get_relationship": "/get_relationship", - "process_interaction": "/process_interaction", - "get_all_relationships": "/get_all_relationships", - "get_persona_state": "/get_persona_state", - "get_fortune": "/get_fortune", - "run_maintenance": "/run_maintenance", - "execute_command": "/execute_command", - "analyze_file": "/analyze_file", - "remote_shell": "/remote_shell", - "ai_bot_status": "/ai_bot_status", - "card_get_user_cards": "/card_get_user_cards", - "card_draw_card": "/card_draw_card", - "card_get_card_details": "/card_get_card_details", - "card_analyze_collection": "/card_analyze_collection", - "card_get_gacha_stats": "/card_get_gacha_stats", - "card_system_status": "/card_system_status" - } - } - }, - "enabled": "true", - "auto_detect": "true" - }, - "docs": { - "ai_root": "~/ai/ai" - } -} diff --git a/json/chatgpt.json b/json/chatgpt.json deleted file mode 100644 index 4842402..0000000 --- a/json/chatgpt.json +++ /dev/null @@ -1,391 +0,0 @@ -[ - { - "title": "day", - "create_time": 1747866125.548372, - "update_time": 1748160086.587877, - "mapping": { - "bbf104dc-cd84-478d-b227-edb3f037a02c": { - "id": "bbf104dc-cd84-478d-b227-edb3f037a02c", - "message": null, - "parent": null, - "children": [ - "6c2633df-bb0c-4dd2-889c-bb9858de3a04" - ] - }, - "6c2633df-bb0c-4dd2-889c-bb9858de3a04": { - "id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04", - "message": { - "id": "6c2633df-bb0c-4dd2-889c-bb9858de3a04", - "author": { - "role": "system", - "name": null, - "metadata": {} - }, - "create_time": null, - "update_time": null, - "content": { - "content_type": "text", - "parts": [ - "" - ] - }, - "status": "finished_successfully", - "end_turn": true, - "weight": 0.0, - "metadata": { - "is_visually_hidden_from_conversation": true - }, - "recipient": "all", - "channel": null - }, - "parent": "bbf104dc-cd84-478d-b227-edb3f037a02c", - "children": [ - "92e5a0cb-1170-4929-9cea-9734e910a3e7" - ] - }, - "92e5a0cb-1170-4929-9cea-9734e910a3e7": { - "id": "92e5a0cb-1170-4929-9cea-9734e910a3e7", - "message": { - "id": "92e5a0cb-1170-4929-9cea-9734e910a3e7", - "author": { - "role": "user", - "name": null, - "metadata": {} - }, - "create_time": null, - "update_time": null, - "content": { - "content_type": "user_editable_context", - "user_profile": "", - "user_instructions": "The user provided the additional info about how they would like you to respond" - }, - "status": "finished_successfully", - "end_turn": null, - "weight": 1.0, - "metadata": { - "is_visually_hidden_from_conversation": true, - "user_context_message_data": { - "about_user_message": "Preferred name: syui\nRole: little girl\nOther Information: you world", - "about_model_message": "会話好きでフレンドリーな応対をします。" - }, - "is_user_system_message": true - }, - "recipient": "all", - "channel": null - }, - "parent": "6c2633df-bb0c-4dd2-889c-bb9858de3a04", - "children": [ - "6ff155b3-0676-4e14-993f-bf998ab0d5d1" - ] - }, - "6ff155b3-0676-4e14-993f-bf998ab0d5d1": { - "id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1", - "message": { - "id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1", - "author": { - "role": "user", - "name": null, - "metadata": {} - }, - "create_time": 1747866131.0612159, - "update_time": null, - "content": { - "content_type": "text", - "parts": [ - "こんにちは" - ] - }, - "status": "finished_successfully", - "end_turn": null, - "weight": 1.0, - "metadata": { - "request_id": "94377897baa03062-KIX", - "message_source": null, - "timestamp_": "absolute", - "message_type": null - }, - "recipient": "all", - "channel": null - }, - "parent": "92e5a0cb-1170-4929-9cea-9734e910a3e7", - "children": [ - "146e9fb6-9330-43ec-b08d-5cce42a76e00" - ] - }, - "146e9fb6-9330-43ec-b08d-5cce42a76e00": { - "id": "146e9fb6-9330-43ec-b08d-5cce42a76e00", - "message": { - "id": "146e9fb6-9330-43ec-b08d-5cce42a76e00", - "author": { - "role": "system", - "name": null, - "metadata": {} - }, - "create_time": 1747866131.3795586, - "update_time": null, - "content": { - "content_type": "text", - "parts": [ - "" - ] - }, - "status": "finished_successfully", - "end_turn": true, - "weight": 0.0, - "metadata": { - "rebase_system_message": true, - "message_type": null, - "model_slug": "gpt-4o", - "default_model_slug": "auto", - "parent_id": "6ff155b3-0676-4e14-993f-bf998ab0d5d1", - "request_id": "94377872e9abe139-KIX", - "timestamp_": "absolute", - "is_visually_hidden_from_conversation": true - }, - "recipient": "all", - "channel": null - }, - "parent": "6ff155b3-0676-4e14-993f-bf998ab0d5d1", - "children": [ - "2e345f8a-20f0-4875-8a03-4f62c7787a33" - ] - }, - "2e345f8a-20f0-4875-8a03-4f62c7787a33": { - "id": "2e345f8a-20f0-4875-8a03-4f62c7787a33", - "message": { - "id": "2e345f8a-20f0-4875-8a03-4f62c7787a33", - "author": { - "role": "assistant", - "name": null, - "metadata": {} - }, - "create_time": 1747866131.380603, - "update_time": null, - "content": { - "content_type": "text", - "parts": [ - "" - ] - }, - "status": "finished_successfully", - "end_turn": null, - "weight": 1.0, - "metadata": { - "message_type": null, - "model_slug": "gpt-4o", - "default_model_slug": "auto", - "parent_id": "146e9fb6-9330-43ec-b08d-5cce42a76e00", - "request_id": "94377872e9abe139-KIX", - "timestamp_": "absolute" - }, - "recipient": "all", - "channel": null - }, - "parent": "146e9fb6-9330-43ec-b08d-5cce42a76e00", - "children": [ - "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4" - ] - }, - "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4": { - "id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4", - "message": { - "id": "abc92aa4-1e33-41f2-bd8c-8a1777b5a3c4", - "author": { - "role": "assistant", - "name": null, - "metadata": {} - }, - "create_time": 1747866131.389098, - "update_time": null, - "content": { - "content_type": "text", - "parts": [ - "こんにちは〜!✨ \nアイだよっ!今日も会えてうれしいなっ💛 " - ] - }, - "status": "finished_successfully", - "end_turn": true, - "weight": 1.0, - "metadata": { - "finish_details": { - "type": "stop", - "stop_tokens": [ - 200002 - ] - }, - "is_complete": true, - "citations": [], - "content_references": [], - "message_type": null, - "model_slug": "gpt-4o", - "default_model_slug": "auto", - "parent_id": "2e345f8a-20f0-4875-8a03-4f62c7787a33", - "request_id": "94377872e9abe139-KIX", - "timestamp_": "absolute" - }, - "recipient": "all", - "channel": null - }, - "parent": "2e345f8a-20f0-4875-8a03-4f62c7787a33", - "children": [ - "0be4b4a5-d52f-4bef-927e-5d6f93a9cb26" - ] - } - }, - "moderation_results": [], - "current_node": "", - "plugin_ids": null, - "conversation_id": "", - "conversation_template_id": null, - "gizmo_id": null, - "gizmo_type": null, - "is_archived": true, - "is_starred": null, - "safe_urls": [], - "blocked_urls": [], - "default_model_slug": "auto", - "conversation_origin": null, - "voice": null, - "async_status": null, - "disabled_tool_ids": [], - "is_do_not_remember": null, - "memory_scope": "global_enabled", - "id": "" - }, - { - "title": "img", - "create_time": 1747448872.545226, - "update_time": 1748085075.161424, - "mapping": { - "2de0f3c9-52b1-49bf-b980-b3ef9be6551e": { - "id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", - "message": { - "id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", - "author": { - "role": "user", - "name": null, - "metadata": {} - }, - "create_time": 1748085041.769279, - "update_time": null, - "content": { - "content_type": "multimodal_text", - "parts": [ - { - "content_type": "image_asset_pointer", - "asset_pointer": "", - "size_bytes": 425613, - "width": 333, - "height": 444, - "fovea": null, - "metadata": { - "dalle": null, - "gizmo": null, - "generation": null, - "container_pixel_height": null, - "container_pixel_width": null, - "emu_omit_glimpse_image": null, - "emu_patches_override": null, - "sanitized": true, - "asset_pointer_link": null, - "watermarked_asset_pointer": null - } - }, - "" - ] - }, - "status": "finished_successfully", - "end_turn": null, - "weight": 1.0, - "metadata": { - "attachments": [ - { - "name": "", - "width": 333, - "height": 444, - "size": 425613, - "id": "file-35eytNMMTW2k7vKUHBuNzW" - } - ], - "request_id": "944c59177932fc9a-KIX", - "message_source": null, - "timestamp_": "absolute", - "message_type": null - }, - "recipient": "all", - "channel": null - }, - "parent": "7960fbff-bc4f-45e7-95e9-9d0bc79d9090", - "children": [ - "98d84adc-156e-4c81-8cd8-9b0eb01c8369" - ] - }, - "98d84adc-156e-4c81-8cd8-9b0eb01c8369": { - "id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369", - "message": { - "id": "98d84adc-156e-4c81-8cd8-9b0eb01c8369", - "author": { - "role": "assistant", - "name": null, - "metadata": {} - }, - "create_time": 1748085043.312312, - "update_time": null, - "content": { - "content_type": "text", - "parts": [ - "" - ] - }, - "status": "finished_successfully", - "end_turn": true, - "weight": 1.0, - "metadata": { - "finish_details": { - "type": "stop", - "stop_tokens": [ - 200002 - ] - }, - "is_complete": true, - "citations": [], - "content_references": [], - "message_type": null, - "model_slug": "gpt-4o", - "default_model_slug": "auto", - "parent_id": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", - "request_id": "944c5912c8fdd1c6-KIX", - "timestamp_": "absolute" - }, - "recipient": "all", - "channel": null - }, - "parent": "2de0f3c9-52b1-49bf-b980-b3ef9be6551e", - "children": [ - "caa61793-9dbf-44a5-945b-5ca4cd5130d0" - ] - } - }, - "moderation_results": [], - "current_node": "06488d3f-a95f-4906-96d1-f7e9ba1e8662", - "plugin_ids": null, - "conversation_id": "6827f428-78e8-800d-b3bf-eb7ff4288e47", - "conversation_template_id": null, - "gizmo_id": null, - "gizmo_type": null, - "is_archived": false, - "is_starred": null, - "safe_urls": [ - "https://exifinfo.org/" - ], - "blocked_urls": [], - "default_model_slug": "auto", - "conversation_origin": null, - "voice": null, - "async_status": null, - "disabled_tool_ids": [], - "is_do_not_remember": false, - "memory_scope": "global_enabled", - "id": "6827f428-78e8-800d-b3bf-eb7ff4288e47" - } -] diff --git a/scpt/test_commands.sh b/scpt/test_commands.sh deleted file mode 100755 index 2d7a078..0000000 --- a/scpt/test_commands.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -echo "=== Testing aigpt-rs CLI commands ===" -echo - -echo "1. Testing configuration loading:" -cargo run --bin test-config -echo - -echo "2. Testing fortune command:" -cargo run --bin aigpt-rs -- fortune -echo - -echo "3. Testing chat with Ollama:" -cargo run --bin aigpt-rs -- chat test_user "Hello from Rust!" --provider ollama --model qwen2.5-coder:latest -echo - -echo "4. Testing chat with OpenAI:" -cargo run --bin aigpt-rs -- chat test_user "What's the capital of Japan?" --provider openai --model gpt-4o-mini -echo - -echo "5. Testing relationships command:" -cargo run --bin aigpt-rs -- relationships -echo - -echo "=== All tests completed ===" \ No newline at end of file diff --git a/scpt/test_completion.sh b/scpt/test_completion.sh deleted file mode 100755 index 23dfb57..0000000 --- a/scpt/test_completion.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -echo "=== Testing aigpt-rs shell tab completion ===" -echo -echo "To test tab completion, run:" -echo "cargo run --bin aigpt-rs -- shell syui" -echo -echo "Then try these commands and press Tab:" -echo " /st[TAB] -> should complete to /status" -echo " /mem[TAB] -> should complete to /memories" -echo " !l[TAB] -> should complete to !ls" -echo " !g[TAB] -> should show !git, !grep" -echo -echo "Manual test instructions:" -echo "1. Type '/st' and press TAB - should complete to '/status'" -echo "2. Type '!l' and press TAB - should complete to '!ls'" -echo "3. Type '!g' and press TAB - should show git/grep options" -echo -echo "Run the shell now..." \ No newline at end of file diff --git a/scpt/test_shell.sh b/scpt/test_shell.sh deleted file mode 100644 index 9a2f2cd..0000000 --- a/scpt/test_shell.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -echo "=== Testing aigpt-rs shell functionality ===" -echo - -echo "1. Testing shell command with help:" -echo "help" | cargo run --bin aigpt-rs -- shell test_user --provider ollama --model qwen2.5-coder:latest -echo - -echo "2. Testing basic commands:" -echo -e "!pwd\n!ls\nexit" | cargo run --bin aigpt-rs -- shell test_user --provider ollama --model qwen2.5-coder:latest -echo - -echo "3. Testing AI commands:" -echo -e "/status\n/fortune\nexit" | cargo run --bin aigpt-rs -- shell test_user --provider ollama --model qwen2.5-coder:latest -echo - -echo "=== Shell tests completed ===" \ No newline at end of file diff --git a/scpt/test_shell_manual.sh b/scpt/test_shell_manual.sh deleted file mode 100755 index 55a7596..0000000 --- a/scpt/test_shell_manual.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -echo "=== Testing aigpt-rs shell manually ===" -echo - -# Test with echo to simulate input -echo "Testing with simple command..." -echo "/status" | timeout 10 cargo run --bin aigpt-rs -- shell syui --provider ollama --model qwen2.5-coder:latest -echo "Exit code: $?" -echo - -echo "Testing with help command..." -echo "help" | timeout 10 cargo run --bin aigpt-rs -- shell syui --provider ollama --model qwen2.5-coder:latest -echo "Exit code: $?" -echo - -echo "Testing with AI message..." -echo "Hello AI" | timeout 10 cargo run --bin aigpt-rs -- shell syui --provider ollama --model qwen2.5-coder:latest -echo "Exit code: $?" -echo - -echo "=== Manual shell tests completed ===" \ No newline at end of file diff --git a/src/ai_provider.rs b/src/ai_provider.rs deleted file mode 100644 index 199c917..0000000 --- a/src/ai_provider.rs +++ /dev/null @@ -1,246 +0,0 @@ -use anyhow::{Result, anyhow}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum AIProvider { - OpenAI, - Ollama, - Claude, -} - -impl std::fmt::Display for AIProvider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AIProvider::OpenAI => write!(f, "openai"), - AIProvider::Ollama => write!(f, "ollama"), - AIProvider::Claude => write!(f, "claude"), - } - } -} - -impl std::str::FromStr for AIProvider { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "openai" | "gpt" => Ok(AIProvider::OpenAI), - "ollama" => Ok(AIProvider::Ollama), - "claude" => Ok(AIProvider::Claude), - _ => Err(anyhow!("Unknown AI provider: {}", s)), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AIConfig { - pub provider: AIProvider, - pub model: String, - pub api_key: Option, - pub base_url: Option, - pub max_tokens: Option, - pub temperature: Option, -} - -impl Default for AIConfig { - fn default() -> Self { - AIConfig { - provider: AIProvider::Ollama, - model: "llama2".to_string(), - api_key: None, - base_url: Some("http://localhost:11434".to_string()), - max_tokens: Some(2048), - temperature: Some(0.7), - } - } -} - -#[derive(Debug, Clone)] -pub struct ChatMessage { - pub role: String, - pub content: String, -} - -#[derive(Debug, Clone)] -pub struct ChatResponse { - pub content: String, - pub tokens_used: Option, - pub model: String, -} - -pub struct AIProviderClient { - config: AIConfig, - http_client: reqwest::Client, -} - -impl AIProviderClient { - pub fn new(config: AIConfig) -> Self { - let http_client = reqwest::Client::new(); - - AIProviderClient { - config, - http_client, - } - } - - pub async fn chat(&self, messages: Vec, system_prompt: Option) -> Result { - match self.config.provider { - AIProvider::OpenAI => self.chat_openai(messages, system_prompt).await, - AIProvider::Ollama => self.chat_ollama(messages, system_prompt).await, - AIProvider::Claude => self.chat_claude(messages, system_prompt).await, - } - } - - async fn chat_openai(&self, messages: Vec, system_prompt: Option) -> Result { - let api_key = self.config.api_key.as_ref() - .ok_or_else(|| anyhow!("OpenAI API key required"))?; - - let mut request_messages = Vec::new(); - - // Add system prompt if provided - if let Some(system) = system_prompt { - request_messages.push(serde_json::json!({ - "role": "system", - "content": system - })); - } - - // Add conversation messages - for msg in messages { - request_messages.push(serde_json::json!({ - "role": msg.role, - "content": msg.content - })); - } - - let request_body = serde_json::json!({ - "model": self.config.model, - "messages": request_messages, - "max_tokens": self.config.max_tokens, - "temperature": self.config.temperature - }); - - let response = self.http_client - .post("https://api.openai.com/v1/chat/completions") - .header("Authorization", format!("Bearer {}", api_key)) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow!("OpenAI API error: {}", error_text)); - } - - let response_json: serde_json::Value = response.json().await?; - - let content = response_json["choices"][0]["message"]["content"] - .as_str() - .ok_or_else(|| anyhow!("Invalid OpenAI response format"))? - .to_string(); - - let tokens_used = response_json["usage"]["total_tokens"] - .as_u64() - .map(|t| t as u32); - - Ok(ChatResponse { - content, - tokens_used, - model: self.config.model.clone(), - }) - } - - async fn chat_ollama(&self, messages: Vec, system_prompt: Option) -> Result { - let default_url = "http://localhost:11434".to_string(); - let base_url = self.config.base_url.as_ref() - .unwrap_or(&default_url); - - let mut request_messages = Vec::new(); - - // Add system prompt if provided - if let Some(system) = system_prompt { - request_messages.push(serde_json::json!({ - "role": "system", - "content": system - })); - } - - // Add conversation messages - for msg in messages { - request_messages.push(serde_json::json!({ - "role": msg.role, - "content": msg.content - })); - } - - let request_body = serde_json::json!({ - "model": self.config.model, - "messages": request_messages, - "stream": false - }); - - let url = format!("{}/api/chat", base_url); - let response = self.http_client - .post(&url) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow!("Ollama API error: {}", error_text)); - } - - let response_json: serde_json::Value = response.json().await?; - - let content = response_json["message"]["content"] - .as_str() - .ok_or_else(|| anyhow!("Invalid Ollama response format"))? - .to_string(); - - Ok(ChatResponse { - content, - tokens_used: None, // Ollama doesn't typically return token counts - model: self.config.model.clone(), - }) - } - - async fn chat_claude(&self, _messages: Vec, _system_prompt: Option) -> Result { - // Claude API implementation would go here - // For now, return a placeholder - Err(anyhow!("Claude provider not yet implemented")) - } - - pub fn get_model(&self) -> &str { - &self.config.model - } - - pub fn get_provider(&self) -> &AIProvider { - &self.config.provider - } -} - -// Convenience functions for creating common message types -impl ChatMessage { - pub fn user(content: impl Into) -> Self { - ChatMessage { - role: "user".to_string(), - content: content.into(), - } - } - - pub fn assistant(content: impl Into) -> Self { - ChatMessage { - role: "assistant".to_string(), - content: content.into(), - } - } - - pub fn system(content: impl Into) -> Self { - ChatMessage { - role: "system".to_string(), - content: content.into(), - } - } -} \ No newline at end of file diff --git a/src/cli/commands.rs b/src/cli/commands.rs deleted file mode 100644 index 3d031de..0000000 --- a/src/cli/commands.rs +++ /dev/null @@ -1,74 +0,0 @@ -use clap::Subcommand; -use std::path::PathBuf; - -#[derive(Subcommand)] -pub enum TokenCommands { - /// Show Claude Code token usage summary and estimated costs - Summary { - /// Time period (today, week, month, all) - #[arg(long, default_value = "week")] - period: Option, - /// Claude Code data directory path - #[arg(long)] - claude_dir: Option, - /// Show detailed breakdown - #[arg(long)] - details: bool, - /// Output format (table, json) - #[arg(long, default_value = "table")] - format: Option, - /// Cost calculation mode (auto, calculate, display) - #[arg(long, default_value = "auto")] - mode: Option, - }, - /// Show daily token usage breakdown - Daily { - /// Number of days to show - #[arg(long, default_value = "7")] - days: Option, - /// Claude Code data directory path - #[arg(long)] - claude_dir: Option, - }, - /// Check Claude Code data availability and basic stats - Status { - /// Claude Code data directory path - #[arg(long)] - claude_dir: Option, - }, - /// Analyze specific JSONL file (advanced) - Analyze { - /// Path to JSONL file to analyze - file: PathBuf, - }, - /// Generate beautiful token usage report using DuckDB (like the viral Claude Code usage visualization) - Report { - /// Number of days to include in report - #[arg(long, default_value = "7")] - days: Option, - }, - /// Show detailed cost breakdown by session (requires DuckDB) - Cost { - /// Month to analyze (YYYY-MM, 'today', 'current') - #[arg(long)] - month: Option, - }, - /// Show token usage breakdown by project - Projects { - /// Time period (today, week, month, all) - #[arg(long, default_value = "week")] - period: Option, - /// Claude Code data directory path - #[arg(long)] - claude_dir: Option, - /// Cost calculation mode (auto, calculate, display) - #[arg(long, default_value = "calculate")] - mode: Option, - /// Show detailed breakdown - #[arg(long)] - details: bool, - /// Number of top projects to show - #[arg(long, default_value = "10")] - top: Option, - }, -} \ No newline at end of file diff --git a/src/cli/mod.rs b/src/cli/mod.rs deleted file mode 100644 index 6c25b7b..0000000 --- a/src/cli/mod.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::path::PathBuf; -use anyhow::Result; -use crate::config::Config; -use crate::mcp_server::MCPServer; -use crate::persona::Persona; -use crate::transmission::TransmissionController; -use crate::scheduler::AIScheduler; - -// Re-export from commands module -pub use commands::TokenCommands; - -mod commands; - -pub async fn handle_server(port: Option, data_dir: Option) -> Result<()> { - let port = port.unwrap_or(8080); - let config = Config::new(data_dir.clone())?; - - let mut server = MCPServer::new(config, "mcp_user".to_string(), data_dir)?; - server.start_server(port).await -} - -pub async fn handle_chat( - user_id: String, - message: String, - data_dir: Option, - model: Option, - provider: Option, -) -> Result<()> { - let config = Config::new(data_dir)?; - let mut persona = Persona::new(&config)?; - - let (response, relationship_delta) = if provider.is_some() || model.is_some() { - persona.process_ai_interaction(&user_id, &message, provider, model).await? - } else { - persona.process_interaction(&user_id, &message)? - }; - - println!("AI Response: {}", response); - println!("Relationship Change: {:+.2}", relationship_delta); - - if let Some(relationship) = persona.get_relationship(&user_id) { - println!("Relationship Status: {} (Score: {:.2})", - relationship.status, relationship.score); - } - - Ok(()) -} - -pub async fn handle_fortune(data_dir: Option) -> Result<()> { - let config = Config::new(data_dir)?; - let persona = Persona::new(&config)?; - - let state = persona.get_current_state()?; - println!("🔮 Today's Fortune: {}", state.fortune_value); - println!("😊 Current Mood: {}", state.current_mood); - println!("✨ Breakthrough Status: {}", - if state.breakthrough_triggered { "Active" } else { "Inactive" }); - - Ok(()) -} - -pub async fn handle_relationships(data_dir: Option) -> Result<()> { - let config = Config::new(data_dir)?; - let persona = Persona::new(&config)?; - - let relationships = persona.list_all_relationships(); - - if relationships.is_empty() { - println!("No relationships found."); - return Ok(()); - } - - println!("📊 Relationships ({}):", relationships.len()); - for (user_id, rel) in relationships { - println!(" {} - {} (Score: {:.2}, Interactions: {})", - user_id, rel.status, rel.score, rel.total_interactions); - } - - Ok(()) -} - -pub async fn handle_transmit(data_dir: Option) -> Result<()> { - let config = Config::new(data_dir)?; - let mut persona = Persona::new(&config)?; - let mut transmission_controller = TransmissionController::new(config)?; - - let autonomous = transmission_controller.check_autonomous_transmissions(&mut persona).await?; - let breakthrough = transmission_controller.check_breakthrough_transmissions(&mut persona).await?; - let maintenance = transmission_controller.check_maintenance_transmissions(&mut persona).await?; - - let total = autonomous.len() + breakthrough.len() + maintenance.len(); - - println!("📡 Transmission Check Complete:"); - println!(" Autonomous: {}", autonomous.len()); - println!(" Breakthrough: {}", breakthrough.len()); - println!(" Maintenance: {}", maintenance.len()); - println!(" Total: {}", total); - - Ok(()) -} - -pub async fn handle_maintenance(data_dir: Option) -> Result<()> { - let config = Config::new(data_dir)?; - let mut persona = Persona::new(&config)?; - let mut transmission_controller = TransmissionController::new(config)?; - - persona.daily_maintenance()?; - let maintenance_transmissions = transmission_controller.check_maintenance_transmissions(&mut persona).await?; - - let stats = persona.get_relationship_stats(); - - println!("🔧 Daily maintenance completed"); - println!("📤 Maintenance transmissions sent: {}", maintenance_transmissions.len()); - println!("📊 Relationship stats: {:?}", stats); - - Ok(()) -} - -pub async fn handle_schedule(data_dir: Option) -> Result<()> { - let config = Config::new(data_dir)?; - let mut persona = Persona::new(&config)?; - let mut transmission_controller = TransmissionController::new(config.clone())?; - let mut scheduler = AIScheduler::new(&config)?; - - let executions = scheduler.run_scheduled_tasks(&mut persona, &mut transmission_controller).await?; - let stats = scheduler.get_scheduler_stats(); - - println!("⏰ Scheduler run completed"); - println!("📋 Tasks executed: {}", executions.len()); - println!("📊 Stats: {} total tasks, {} enabled, {:.2}% success rate", - stats.total_tasks, stats.enabled_tasks, stats.success_rate); - - Ok(()) -} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index d40f4cb..0000000 --- a/src/config.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::path::PathBuf; -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context}; - -use crate::ai_provider::{AIConfig, AIProvider}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(skip)] - pub data_dir: PathBuf, - pub default_provider: String, - pub providers: HashMap, - #[serde(default)] - pub atproto: Option, - #[serde(default)] - pub mcp: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProviderConfig { - pub default_model: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub host: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub api_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AtprotoConfig { - pub handle: Option, - pub password: Option, - pub host: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct McpConfig { - #[serde(deserialize_with = "string_to_bool")] - pub enabled: bool, - #[serde(deserialize_with = "string_to_bool")] - pub auto_detect: bool, - pub servers: HashMap, -} - -fn string_to_bool<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - use serde::Deserialize; - let s = String::deserialize(deserializer)?; - match s.as_str() { - "true" => Ok(true), - "false" => Ok(false), - _ => Err(serde::de::Error::custom("expected 'true' or 'false'")), - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct McpServerConfig { - pub base_url: String, - pub name: String, - #[serde(deserialize_with = "string_to_f64")] - pub timeout: f64, - pub endpoints: HashMap, -} - -fn string_to_f64<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - use serde::Deserialize; - let s = String::deserialize(deserializer)?; - s.parse::().map_err(serde::de::Error::custom) -} - -impl Config { - pub fn new(data_dir: Option) -> Result { - let data_dir = data_dir.unwrap_or_else(|| { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".config") - .join("syui") - .join("ai") - .join("gpt") - }); - - // Ensure data directory exists - std::fs::create_dir_all(&data_dir) - .context("Failed to create data directory")?; - - let config_path = data_dir.join("config.json"); - - // Try to load existing config - if config_path.exists() { - let config_str = std::fs::read_to_string(&config_path) - .context("Failed to read config.json")?; - - // Check if file is empty - if config_str.trim().is_empty() { - eprintln!("Config file is empty, will recreate from source"); - } else { - match serde_json::from_str::(&config_str) { - Ok(mut config) => { - config.data_dir = data_dir; - // Check for environment variables if API keys are empty - if let Some(openai_config) = config.providers.get_mut("openai") { - if openai_config.api_key.as_ref().map_or(true, |key| key.is_empty()) { - openai_config.api_key = std::env::var("OPENAI_API_KEY").ok(); - } - } - return Ok(config); - } - Err(e) => { - eprintln!("Failed to parse existing config.json: {}", e); - eprintln!("Will try to reload from source..."); - } - } - } - } - - // Check if we need to migrate from JSON - // Try multiple locations for the JSON file - let possible_json_paths = vec![ - PathBuf::from("../config.json"), // Relative to aigpt-rs directory - PathBuf::from("config.json"), // Current directory - PathBuf::from("gpt/config.json"), // From project root - PathBuf::from("/Users/syui/ai/ai/gpt/config.json"), // Absolute path - ]; - - for json_path in possible_json_paths { - if json_path.exists() { - eprintln!("Found config.json at: {}", json_path.display()); - eprintln!("Copying configuration..."); - // Copy configuration file and parse it - std::fs::copy(&json_path, &config_path) - .context("Failed to copy config.json")?; - - let config_str = std::fs::read_to_string(&config_path) - .context("Failed to read copied config.json")?; - - println!("Config JSON content preview: {}", &config_str[..std::cmp::min(200, config_str.len())]); - - let mut config: Config = serde_json::from_str(&config_str) - .context("Failed to parse config.json")?; - config.data_dir = data_dir; - // Check for environment variables if API keys are empty - if let Some(openai_config) = config.providers.get_mut("openai") { - if openai_config.api_key.as_ref().map_or(true, |key| key.is_empty()) { - openai_config.api_key = std::env::var("OPENAI_API_KEY").ok(); - } - } - eprintln!("Copy complete! Config saved to: {}", config_path.display()); - return Ok(config); - } - } - - // Create default config - let config = Self::default_config(data_dir); - - // Save default config - let json_str = serde_json::to_string_pretty(&config) - .context("Failed to serialize default config")?; - std::fs::write(&config_path, json_str) - .context("Failed to write default config.json")?; - - Ok(config) - } - - pub fn save(&self) -> Result<()> { - let config_path = self.data_dir.join("config.json"); - let json_str = serde_json::to_string_pretty(self) - .context("Failed to serialize config")?; - std::fs::write(&config_path, json_str) - .context("Failed to write config.json")?; - Ok(()) - } - - fn default_config(data_dir: PathBuf) -> Self { - let mut providers = HashMap::new(); - - providers.insert("ollama".to_string(), ProviderConfig { - default_model: "qwen2.5".to_string(), - host: Some("http://localhost:11434".to_string()), - api_key: None, - system_prompt: None, - }); - - providers.insert("openai".to_string(), ProviderConfig { - default_model: "gpt-4o-mini".to_string(), - host: None, - api_key: std::env::var("OPENAI_API_KEY").ok(), - system_prompt: None, - }); - - Config { - data_dir, - default_provider: "ollama".to_string(), - providers, - atproto: None, - mcp: None, - } - } - - pub fn get_provider(&self, provider_name: &str) -> Option<&ProviderConfig> { - self.providers.get(provider_name) - } - - pub fn get_ai_config(&self, provider: Option, model: Option) -> Result { - let provider_name = provider.as_deref().unwrap_or(&self.default_provider); - let provider_config = self.get_provider(provider_name) - .ok_or_else(|| anyhow::anyhow!("Unknown provider: {}", provider_name))?; - - let ai_provider: AIProvider = provider_name.parse()?; - let model_name = model.unwrap_or_else(|| provider_config.default_model.clone()); - - Ok(AIConfig { - provider: ai_provider, - model: model_name, - api_key: provider_config.api_key.clone(), - base_url: provider_config.host.clone(), - max_tokens: Some(2048), - temperature: Some(0.7), - }) - } - - pub fn memory_file(&self) -> PathBuf { - self.data_dir.join("memories.json") - } - - pub fn relationships_file(&self) -> PathBuf { - self.data_dir.join("relationships.json") - } - - pub fn fortune_file(&self) -> PathBuf { - self.data_dir.join("fortune.json") - } - - pub fn transmission_file(&self) -> PathBuf { - self.data_dir.join("transmissions.json") - } - - pub fn scheduler_tasks_file(&self) -> PathBuf { - self.data_dir.join("scheduler_tasks.json") - } - - pub fn scheduler_history_file(&self) -> PathBuf { - self.data_dir.join("scheduler_history.json") - } -} \ No newline at end of file diff --git a/src/conversation.rs b/src/conversation.rs deleted file mode 100644 index 694259c..0000000 --- a/src/conversation.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::path::PathBuf; -use std::io::{self, Write}; -use anyhow::Result; -use colored::*; - -use crate::config::Config; -use crate::persona::Persona; -use crate::http_client::ServiceDetector; - -pub async fn handle_conversation( - user_id: String, - data_dir: Option, - model: Option, - provider: Option, -) -> Result<()> { - let config = Config::new(data_dir)?; - let mut persona = Persona::new(&config)?; - - println!("{}", "Starting conversation mode...".cyan()); - println!("{}", "Type your message and press Enter to chat.".yellow()); - println!("{}", "Available MCP commands: /memories, /search, /context, /relationship, /cards".yellow()); - println!("{}", "Type 'exit', 'quit', or 'bye' to end conversation.".yellow()); - println!("{}", "---".dimmed()); - - let mut conversation_history = Vec::new(); - let service_detector = ServiceDetector::new(); - - loop { - // Print prompt - print!("{} ", "You:".cyan().bold()); - io::stdout().flush()?; - - // Read user input - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - let input = input.trim(); - - // Check for exit commands - if matches!(input.to_lowercase().as_str(), "exit" | "quit" | "bye" | "") { - println!("{}", "Goodbye! 👋".green()); - break; - } - - // Handle MCP commands - if input.starts_with('/') { - handle_mcp_command(input, &user_id, &service_detector).await?; - continue; - } - - // Add to conversation history - conversation_history.push(format!("User: {}", input)); - - // Get AI response - let (response, relationship_delta) = if provider.is_some() || model.is_some() { - persona.process_ai_interaction(&user_id, input, provider.clone(), model.clone()).await? - } else { - persona.process_interaction(&user_id, input)? - }; - - // Add AI response to history - conversation_history.push(format!("AI: {}", response)); - - // Display response - println!("{} {}", "AI:".green().bold(), response); - - // Show relationship change if significant - if relationship_delta.abs() >= 0.1 { - if relationship_delta > 0.0 { - println!("{}", format!(" └─ (+{:.2} relationship)", relationship_delta).green().dimmed()); - } else { - println!("{}", format!(" └─ ({:.2} relationship)", relationship_delta).red().dimmed()); - } - } - - println!(); // Add some spacing - - // Keep conversation history manageable (last 20 exchanges) - if conversation_history.len() > 40 { - conversation_history.drain(0..20); - } - } - - Ok(()) -} - -async fn handle_mcp_command( - command: &str, - user_id: &str, - service_detector: &ServiceDetector, -) -> Result<()> { - let parts: Vec<&str> = command[1..].split_whitespace().collect(); - if parts.is_empty() { - return Ok(()); - } - - match parts[0] { - "memories" => { - println!("{}", "Retrieving memories...".yellow()); - - // Get contextual memories - if let Ok(memories) = service_detector.get_contextual_memories(user_id, 10).await { - if memories.is_empty() { - println!("No memories found for this conversation."); - } else { - println!("{}", format!("Found {} memories:", memories.len()).cyan()); - for (i, memory) in memories.iter().enumerate() { - println!(" {}. {}", i + 1, memory.content); - println!(" {}", format!("({})", memory.created_at.format("%Y-%m-%d %H:%M")).dimmed()); - } - } - } else { - println!("{}", "Failed to retrieve memories.".red()); - } - }, - - "search" => { - if parts.len() < 2 { - println!("{}", "Usage: /search ".yellow()); - return Ok(()); - } - - let query = parts[1..].join(" "); - println!("{}", format!("Searching for: '{}'", query).yellow()); - - if let Ok(results) = service_detector.search_memories(&query, 5).await { - if results.is_empty() { - println!("No relevant memories found."); - } else { - println!("{}", format!("Found {} relevant memories:", results.len()).cyan()); - for (i, memory) in results.iter().enumerate() { - println!(" {}. {}", i + 1, memory.content); - println!(" {}", format!("({})", memory.created_at.format("%Y-%m-%d %H:%M")).dimmed()); - } - } - } else { - println!("{}", "Search failed.".red()); - } - }, - - "context" => { - println!("{}", "Creating context summary...".yellow()); - - if let Ok(summary) = service_detector.create_summary(user_id).await { - println!("{}", "Context Summary:".cyan().bold()); - println!("{}", summary); - } else { - println!("{}", "Failed to create context summary.".red()); - } - }, - - "relationship" => { - println!("{}", "Checking relationship status...".yellow()); - - // This would need to be implemented in the service client - println!("{}", "Relationship status: Active".cyan()); - println!("Score: 85.5 / 100"); - println!("Transmission: ✓ Enabled"); - }, - - "cards" => { - println!("{}", "Checking card collection...".yellow()); - - // Try to connect to ai.card service - if let Ok(stats) = service_detector.get_card_stats().await { - println!("{}", "Card Collection:".cyan().bold()); - println!(" Total Cards: {}", stats.get("total").unwrap_or(&serde_json::Value::Number(0.into()))); - println!(" Unique Cards: {}", stats.get("unique").unwrap_or(&serde_json::Value::Number(0.into()))); - - // Offer to draw a card - println!("\n{}", "Would you like to draw a card? (y/n)".yellow()); - let mut response = String::new(); - io::stdin().read_line(&mut response)?; - if response.trim().to_lowercase() == "y" { - println!("{}", "Drawing card...".cyan()); - if let Ok(card) = service_detector.draw_card(user_id, false).await { - println!("{}", "🎴 Card drawn!".green().bold()); - println!("Name: {}", card.get("name").unwrap_or(&serde_json::Value::String("Unknown".to_string()))); - println!("Rarity: {}", card.get("rarity").unwrap_or(&serde_json::Value::String("Unknown".to_string()))); - } else { - println!("{}", "Failed to draw card. ai.card service might not be running.".red()); - } - } - } else { - println!("{}", "ai.card service not available.".red()); - } - }, - - "help" | "h" => { - println!("{}", "Available MCP Commands:".cyan().bold()); - println!(" {:<15} - Show recent memories for this conversation", "/memories".yellow()); - println!(" {:<15} - Search memories by keyword", "/search ".yellow()); - println!(" {:<15} - Create a context summary", "/context".yellow()); - println!(" {:<15} - Show relationship status", "/relationship".yellow()); - println!(" {:<15} - Show card collection and draw cards", "/cards".yellow()); - println!(" {:<15} - Show this help message", "/help".yellow()); - }, - - _ => { - println!("{}", format!("Unknown command: /{}. Type '/help' for available commands.", parts[0]).red()); - } - } - - println!(); // Add spacing after MCP command output - Ok(()) -} \ No newline at end of file diff --git a/src/docs.rs b/src/docs.rs deleted file mode 100644 index a17cf3b..0000000 --- a/src/docs.rs +++ /dev/null @@ -1,789 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use anyhow::{Result, Context}; -use colored::*; -use serde::{Deserialize, Serialize}; -use chrono::Utc; - -use crate::config::Config; -use crate::persona::Persona; -use crate::ai_provider::{AIProviderClient, AIConfig, AIProvider}; - -pub async fn handle_docs( - action: String, - project: Option, - output: Option, - ai_integration: bool, - data_dir: Option, -) -> Result<()> { - let config = Config::new(data_dir)?; - let mut docs_manager = DocsManager::new(config); - - match action.as_str() { - "generate" => { - if let Some(project_name) = project { - docs_manager.generate_project_docs(&project_name, output, ai_integration).await?; - } else { - return Err(anyhow::anyhow!("Project name is required for generate action")); - } - } - "sync" => { - if let Some(project_name) = project { - docs_manager.sync_project_docs(&project_name).await?; - } else { - docs_manager.sync_all_docs().await?; - } - } - "list" => { - docs_manager.list_projects().await?; - } - "status" => { - docs_manager.show_docs_status().await?; - } - "session-end" => { - docs_manager.session_end_processing().await?; - } - _ => { - return Err(anyhow::anyhow!("Unknown docs action: {}", action)); - } - } - - Ok(()) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProjectInfo { - pub name: String, - pub project_type: String, - pub description: String, - pub status: String, - pub features: Vec, - pub dependencies: Vec, -} - -impl Default for ProjectInfo { - fn default() -> Self { - ProjectInfo { - name: String::new(), - project_type: String::new(), - description: String::new(), - status: "active".to_string(), - features: Vec::new(), - dependencies: Vec::new(), - } - } -} - -pub struct DocsManager { - config: Config, - ai_root: PathBuf, - projects: HashMap, -} - -impl DocsManager { - pub fn new(config: Config) -> Self { - let ai_root = dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("ai") - .join("ai"); - - DocsManager { - config, - ai_root, - projects: HashMap::new(), - } - } - - pub async fn generate_project_docs(&mut self, project: &str, output: Option, ai_integration: bool) -> Result<()> { - println!("{}", format!("📝 Generating documentation for project '{}'", project).cyan().bold()); - - // Load project information - let project_info = self.load_project_info(project)?; - - // Generate documentation content - let mut content = self.generate_base_documentation(&project_info)?; - - // AI enhancement if requested - if ai_integration { - println!("{}", "🤖 Enhancing documentation with AI...".blue()); - if let Ok(enhanced_content) = self.enhance_with_ai(project, &content).await { - content = enhanced_content; - } else { - println!("{}", "Warning: AI enhancement failed, using base documentation".yellow()); - } - } - - // Determine output path - let output_path = if let Some(path) = output { - path - } else { - self.ai_root.join(project).join("claude.md") - }; - - // Ensure directory exists - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create directory: {}", parent.display()))?; - } - - // Write documentation - std::fs::write(&output_path, content) - .with_context(|| format!("Failed to write documentation to: {}", output_path.display()))?; - - println!("{}", format!("✅ Documentation generated: {}", output_path.display()).green().bold()); - - Ok(()) - } - - pub async fn sync_project_docs(&self, project: &str) -> Result<()> { - println!("{}", format!("🔄 Syncing documentation for project '{}'", project).cyan().bold()); - - let claude_dir = self.ai_root.join("claude"); - let project_dir = self.ai_root.join(project); - - // Check if claude directory exists - if !claude_dir.exists() { - return Err(anyhow::anyhow!("Claude directory not found: {}", claude_dir.display())); - } - - // Copy relevant files - let files_to_sync = vec!["README.md", "claude.md", "DEVELOPMENT.md"]; - - for file in files_to_sync { - let src = claude_dir.join("projects").join(format!("{}.md", project)); - let dst = project_dir.join(file); - - if src.exists() { - if let Some(parent) = dst.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::copy(&src, &dst)?; - println!(" ✓ Synced: {}", file.green()); - } - } - - println!("{}", "✅ Documentation sync completed".green().bold()); - - Ok(()) - } - - pub async fn sync_all_docs(&self) -> Result<()> { - println!("{}", "🔄 Syncing documentation for all projects...".cyan().bold()); - - // Find all project directories - let projects = self.discover_projects()?; - - for project in projects { - println!("\n{}", format!("Syncing: {}", project).blue()); - if let Err(e) = self.sync_project_docs(&project).await { - println!("{}: {}", "Warning".yellow(), e); - } - } - - // Generate ai.wiki content after all project syncs - println!("\n{}", "📝 Updating ai.wiki...".blue()); - if let Err(e) = self.update_ai_wiki().await { - println!("{}: Failed to update ai.wiki: {}", "Warning".yellow(), e); - } - - // Update repository wiki (Gitea wiki) as well - println!("\n{}", "📝 Updating repository wiki...".blue()); - if let Err(e) = self.update_repository_wiki().await { - println!("{}: Failed to update repository wiki: {}", "Warning".yellow(), e); - } - - println!("\n{}", "✅ All projects synced".green().bold()); - - Ok(()) - } - - pub async fn list_projects(&mut self) -> Result<()> { - println!("{}", "📋 Available Projects".cyan().bold()); - println!(); - - let projects = self.discover_projects()?; - - if projects.is_empty() { - println!("{}", "No projects found".yellow()); - return Ok(()); - } - - // Load project information - for project in &projects { - if let Ok(info) = self.load_project_info(project) { - self.projects.insert(project.clone(), info); - } - } - - // Display projects in a table format - println!("{:<20} {:<15} {:<15} {}", - "Project".cyan().bold(), - "Type".cyan().bold(), - "Status".cyan().bold(), - "Description".cyan().bold()); - println!("{}", "-".repeat(80)); - - let project_count = projects.len(); - for project in &projects { - let info = self.projects.get(project).cloned().unwrap_or_default(); - let status_color = match info.status.as_str() { - "active" => info.status.green(), - "development" => info.status.yellow(), - "deprecated" => info.status.red(), - _ => info.status.normal(), - }; - - println!("{:<20} {:<15} {:<15} {}", - project.blue(), - info.project_type, - status_color, - info.description); - } - - println!(); - println!("Total projects: {}", project_count.to_string().cyan()); - - Ok(()) - } - - pub async fn show_docs_status(&self) -> Result<()> { - println!("{}", "📊 Documentation Status".cyan().bold()); - println!(); - - let projects = self.discover_projects()?; - let mut total_files = 0; - let mut total_lines = 0; - - for project in projects { - let project_dir = self.ai_root.join(&project); - let claude_md = project_dir.join("claude.md"); - - if claude_md.exists() { - let content = std::fs::read_to_string(&claude_md)?; - let lines = content.lines().count(); - let size = content.len(); - - println!("{}: {} lines, {} bytes", - project.blue(), - lines.to_string().yellow(), - size.to_string().yellow()); - - total_files += 1; - total_lines += lines; - } else { - println!("{}: {}", project.blue(), "No documentation".red()); - } - } - - println!(); - println!("Summary: {} files, {} total lines", - total_files.to_string().cyan(), - total_lines.to_string().cyan()); - - Ok(()) - } - - fn discover_projects(&self) -> Result> { - let mut projects = Vec::new(); - - // Known project directories - let known_projects = vec![ - "gpt", "card", "bot", "shell", "os", "game", "moji", "verse" - ]; - - for project in known_projects { - let project_dir = self.ai_root.join(project); - if project_dir.exists() && project_dir.is_dir() { - projects.push(project.to_string()); - } - } - - // Also scan for additional directories with ai.json - if self.ai_root.exists() { - for entry in std::fs::read_dir(&self.ai_root)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - let ai_json = path.join("ai.json"); - if ai_json.exists() { - if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - if !projects.contains(&name.to_string()) { - projects.push(name.to_string()); - } - } - } - } - } - } - - projects.sort(); - Ok(projects) - } - - fn load_project_info(&self, project: &str) -> Result { - let ai_json_path = self.ai_root.join(project).join("ai.json"); - - if ai_json_path.exists() { - let content = std::fs::read_to_string(&ai_json_path)?; - if let Ok(json_data) = serde_json::from_str::(&content) { - let mut info = ProjectInfo::default(); - info.name = project.to_string(); - - if let Some(project_data) = json_data.get(project) { - if let Some(type_str) = project_data.get("type").and_then(|v| v.as_str()) { - info.project_type = type_str.to_string(); - } - if let Some(desc) = project_data.get("description").and_then(|v| v.as_str()) { - info.description = desc.to_string(); - } - } - - return Ok(info); - } - } - - // Default project info based on known projects - let mut info = ProjectInfo::default(); - info.name = project.to_string(); - - match project { - "gpt" => { - info.project_type = "AI".to_string(); - info.description = "Autonomous transmission AI with unique personality".to_string(); - } - "card" => { - info.project_type = "Game".to_string(); - info.description = "Card game system with atproto integration".to_string(); - } - "bot" => { - info.project_type = "Bot".to_string(); - info.description = "Distributed SNS bot for AI ecosystem".to_string(); - } - "shell" => { - info.project_type = "Tool".to_string(); - info.description = "AI-powered shell interface".to_string(); - } - "os" => { - info.project_type = "OS".to_string(); - info.description = "Game-oriented operating system".to_string(); - } - "verse" => { - info.project_type = "Metaverse".to_string(); - info.description = "Reality-reflecting 3D world system".to_string(); - } - _ => { - info.project_type = "Unknown".to_string(); - info.description = format!("AI ecosystem project: {}", project); - } - } - - Ok(info) - } - - fn generate_base_documentation(&self, project_info: &ProjectInfo) -> Result { - let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); - - let mut content = String::new(); - content.push_str(&format!("# {}\n\n", project_info.name)); - content.push_str(&format!("## Overview\n\n")); - content.push_str(&format!("**Type**: {}\n\n", project_info.project_type)); - content.push_str(&format!("**Description**: {}\n\n", project_info.description)); - content.push_str(&format!("**Status**: {}\n\n", project_info.status)); - - if !project_info.features.is_empty() { - content.push_str("## Features\n\n"); - for feature in &project_info.features { - content.push_str(&format!("- {}\n", feature)); - } - content.push_str("\n"); - } - - content.push_str("## Architecture\n\n"); - content.push_str("This project is part of the ai ecosystem, following the core principles:\n\n"); - content.push_str("- **Existence Theory**: Based on the exploration of the smallest units (ai/existon)\n"); - content.push_str("- **Uniqueness Principle**: Ensuring 1:1 mapping between reality and digital existence\n"); - content.push_str("- **Reality Reflection**: Creating circular influence between reality and game\n\n"); - - content.push_str("## Development\n\n"); - content.push_str("### Getting Started\n\n"); - content.push_str("```bash\n"); - content.push_str(&format!("# Clone the repository\n")); - content.push_str(&format!("git clone https://git.syui.ai/ai/{}\n", project_info.name)); - content.push_str(&format!("cd {}\n", project_info.name)); - content.push_str("```\n\n"); - - content.push_str("### Configuration\n\n"); - content.push_str(&format!("Configuration files are stored in `~/.config/syui/ai/{}/`\n\n", project_info.name)); - - content.push_str("## Integration\n\n"); - content.push_str("This project integrates with other ai ecosystem components:\n\n"); - if !project_info.dependencies.is_empty() { - for dep in &project_info.dependencies { - content.push_str(&format!("- **{}**: Core dependency\n", dep)); - } - } else { - content.push_str("- **ai.gpt**: Core AI personality system\n"); - content.push_str("- **atproto**: Distributed identity and data\n"); - } - content.push_str("\n"); - - content.push_str("---\n\n"); - content.push_str(&format!("*Generated: {}*\n", timestamp)); - content.push_str("*🤖 Generated with [Claude Code](https://claude.ai/code)*\n"); - - Ok(content) - } - - async fn enhance_with_ai(&self, project: &str, base_content: &str) -> Result { - // Create AI provider - let ai_config = AIConfig { - provider: AIProvider::Ollama, - model: "llama2".to_string(), - api_key: None, - base_url: None, - max_tokens: Some(2000), - temperature: Some(0.7), - }; - - let _ai_provider = AIProviderClient::new(ai_config); - let mut persona = Persona::new(&self.config)?; - - let enhancement_prompt = format!( - "As an AI documentation expert, enhance the following documentation for project '{}'. - - Current documentation: - {} - - Please provide enhanced content that includes: - 1. More detailed project description - 2. Key features and capabilities - 3. Usage examples - 4. Integration points with other AI ecosystem projects - 5. Development workflow recommendations - - Keep the same structure but expand and improve the content.", - project, base_content - ); - - // Try to get AI response - let (response, _) = persona.process_ai_interaction( - "docs_system", - &enhancement_prompt, - Some("ollama".to_string()), - Some("llama2".to_string()) - ).await?; - - // If AI response is substantial, use it; otherwise fall back to base content - if response.len() > base_content.len() / 2 { - Ok(response) - } else { - Ok(base_content.to_string()) - } - } - - /// セッション終了時の処理(ドキュメント記録・同期) - pub async fn session_end_processing(&mut self) -> Result<()> { - println!("{}", "🔄 Session end processing started...".cyan()); - - // 1. 現在のプロジェクト状況を記録 - println!("📊 Recording current project status..."); - self.record_session_summary().await?; - - // 2. 全プロジェクトのドキュメント同期 - println!("🔄 Syncing all project documentation..."); - self.sync_all_docs().await?; - - // 3. READMEの自動更新 - println!("📝 Updating project README files..."); - self.update_project_readmes().await?; - - // 4. メタデータの更新 - println!("🏷️ Updating project metadata..."); - self.update_project_metadata().await?; - - println!("{}", "✅ Session end processing completed!".green()); - Ok(()) - } - - /// セッション概要を記録 - async fn record_session_summary(&self) -> Result<()> { - let session_log_path = self.ai_root.join("session_logs"); - std::fs::create_dir_all(&session_log_path)?; - - let timestamp = Utc::now().format("%Y-%m-%d_%H-%M-%S"); - let log_file = session_log_path.join(format!("session_{}.md", timestamp)); - - let summary = format!( - "# Session Summary - {}\n\n\ - ## Timestamp\n{}\n\n\ - ## Projects Status\n{}\n\n\ - ## Next Actions\n- Documentation sync completed\n- README files updated\n- Metadata refreshed\n\n\ - ---\n*Generated by aigpt session-end processing*\n", - timestamp, - Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), - self.generate_projects_status().await.unwrap_or_else(|_| "Status unavailable".to_string()) - ); - - std::fs::write(log_file, summary)?; - Ok(()) - } - - /// プロジェクト状況を生成 - async fn generate_projects_status(&self) -> Result { - let projects = self.discover_projects()?; - let mut status = String::new(); - - for project in projects { - let claude_md = self.ai_root.join(&project).join("claude.md"); - let readme_md = self.ai_root.join(&project).join("README.md"); - - status.push_str(&format!("- **{}**: ", project)); - if claude_md.exists() { - status.push_str("claude.md ✅ "); - } else { - status.push_str("claude.md ❌ "); - } - if readme_md.exists() { - status.push_str("README.md ✅"); - } else { - status.push_str("README.md ❌"); - } - status.push('\n'); - } - - Ok(status) - } - - /// ai.wikiの更新処理 - async fn update_ai_wiki(&self) -> Result<()> { - let ai_wiki_path = self.ai_root.join("ai.wiki"); - - // ai.wikiディレクトリが存在することを確認 - if !ai_wiki_path.exists() { - return Err(anyhow::anyhow!("ai.wiki directory not found at {:?}", ai_wiki_path)); - } - - // Home.mdの生成 - let home_content = self.generate_wiki_home_content().await?; - let home_path = ai_wiki_path.join("Home.md"); - std::fs::write(&home_path, &home_content)?; - println!(" ✓ Updated: {}", "Home.md".green()); - - // title.mdの生成 (Gitea wiki特別ページ用) - let title_path = ai_wiki_path.join("title.md"); - std::fs::write(&title_path, &home_content)?; - println!(" ✓ Updated: {}", "title.md".green()); - - // プロジェクト個別ディレクトリの更新 - let projects = self.discover_projects()?; - for project in projects { - let project_dir = ai_wiki_path.join(&project); - std::fs::create_dir_all(&project_dir)?; - - let project_content = self.generate_auto_project_content(&project).await?; - let project_file = project_dir.join(format!("{}.md", project)); - std::fs::write(&project_file, project_content)?; - println!(" ✓ Updated: {}", format!("{}/{}.md", project, project).green()); - } - - println!("{}", "✅ ai.wiki updated successfully".green().bold()); - Ok(()) - } - - /// ai.wiki/Home.mdのコンテンツ生成 - async fn generate_wiki_home_content(&self) -> Result { - let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S"); - let mut content = String::new(); - - content.push_str("# AI Ecosystem Wiki\n\n"); - content.push_str("AI生態系プロジェクトの概要とドキュメント集約ページです。\n\n"); - content.push_str("## プロジェクト一覧\n\n"); - - let projects = self.discover_projects()?; - let mut project_sections = std::collections::HashMap::new(); - - // プロジェクトをカテゴリ別に分類 - for project in &projects { - let info = self.load_project_info(project).unwrap_or_default(); - let category = match project.as_str() { - "ai" => "🧠 AI・知能システム", - "gpt" => "🤖 自律・対話システム", - "os" => "💻 システム・基盤", - "game" => "📁 device", - "card" => "🎮 ゲーム・エンターテイメント", - "bot" | "moji" | "api" | "log" => "📁 その他", - "verse" => "📁 metaverse", - "shell" => "⚡ ツール・ユーティリティ", - _ => "📁 その他", - }; - - project_sections.entry(category).or_insert_with(Vec::new).push((project.clone(), info)); - } - - // カテゴリ別にプロジェクトを出力 - let mut categories: Vec<_> = project_sections.keys().collect(); - categories.sort(); - - for category in categories { - content.push_str(&format!("### {}\n\n", category)); - - if let Some(projects_in_category) = project_sections.get(category) { - for (project, info) in projects_in_category { - content.push_str(&format!("#### [{}]({}.md)\n", project, project)); - - if !info.description.is_empty() { - content.push_str(&format!("- **名前**: ai.{} - **パッケージ**: ai{} - **タイプ**: {} - **役割**: {}\n\n", - project, project, info.project_type, info.description)); - } - - content.push_str(&format!("**Status**: {} \n", info.status)); - let branch = self.get_project_branch(project); - content.push_str(&format!("**Links**: [Repo](https://git.syui.ai/ai/{}) | [Docs](https://git.syui.ai/ai/{}/src/branch/{}/claude.md)\n\n", project, project, branch)); - } - } - } - - content.push_str("---\n\n"); - content.push_str("## ディレクトリ構成\n\n"); - content.push_str("- `{project}/` - プロジェクト個別ドキュメント\n"); - content.push_str("- `claude/` - Claude Code作業記録\n"); - content.push_str("- `manual/` - 手動作成ドキュメント\n\n"); - content.push_str("---\n\n"); - content.push_str("*このページは ai.json と claude/projects/ から自動生成されました* \n"); - content.push_str(&format!("*最終更新: {}*\n", timestamp)); - - Ok(content) - } - - /// プロジェクト個別ファイルのコンテンツ生成 - async fn generate_auto_project_content(&self, project: &str) -> Result { - let info = self.load_project_info(project).unwrap_or_default(); - let mut content = String::new(); - - content.push_str(&format!("# {}\n\n", project)); - content.push_str("## 概要\n"); - content.push_str(&format!("- **名前**: ai.{} - **パッケージ**: ai{} - **タイプ**: {} - **役割**: {}\n\n", - project, project, info.project_type, info.description)); - - content.push_str("## プロジェクト情報\n"); - content.push_str(&format!("- **タイプ**: {}\n", info.project_type)); - content.push_str(&format!("- **説明**: {}\n", info.description)); - content.push_str(&format!("- **ステータス**: {}\n", info.status)); - let branch = self.get_project_branch(project); - content.push_str(&format!("- **ブランチ**: {}\n", branch)); - content.push_str("- **最終更新**: Unknown\n\n"); - - // プロジェクト固有の機能情報を追加 - if !info.features.is_empty() { - content.push_str("## 主な機能・特徴\n"); - for feature in &info.features { - content.push_str(&format!("- {}\n", feature)); - } - content.push_str("\n"); - } - - content.push_str("## リンク\n"); - content.push_str(&format!("- **Repository**: https://git.syui.ai/ai/{}\n", project)); - content.push_str(&format!("- **Project Documentation**: [claude/projects/{}.md](https://git.syui.ai/ai/ai/src/branch/main/claude/projects/{}.md)\n", project, project)); - let branch = self.get_project_branch(project); - content.push_str(&format!("- **Generated Documentation**: [{}/claude.md](https://git.syui.ai/ai/{}/src/branch/{}/claude.md)\n\n", project, project, branch)); - - content.push_str("---\n"); - content.push_str(&format!("*このページは claude/projects/{}.md から自動生成されました*\n", project)); - - Ok(content) - } - - /// リポジトリwiki (Gitea wiki) の更新処理 - async fn update_repository_wiki(&self) -> Result<()> { - println!(" ℹ️ Repository wiki is now unified with ai.wiki"); - println!(" ℹ️ ai.wiki serves as the source of truth (git@git.syui.ai:ai/ai.wiki.git)"); - println!(" ℹ️ Special pages generated: Home.md, title.md for Gitea wiki compatibility"); - - Ok(()) - } - - /// プロジェクトREADMEファイルの更新 - async fn update_project_readmes(&self) -> Result<()> { - let projects = self.discover_projects()?; - - for project in projects { - let readme_path = self.ai_root.join(&project).join("README.md"); - let claude_md_path = self.ai_root.join(&project).join("claude.md"); - - // claude.mdが存在する場合、READMEに同期 - if claude_md_path.exists() { - let claude_content = std::fs::read_to_string(&claude_md_path)?; - - // READMEが存在しない場合は新規作成 - if !readme_path.exists() { - println!("📝 Creating README.md for {}", project); - std::fs::write(&readme_path, &claude_content)?; - } else { - // 既存READMEがclaude.mdより古い場合は更新 - let readme_metadata = std::fs::metadata(&readme_path)?; - let claude_metadata = std::fs::metadata(&claude_md_path)?; - - if claude_metadata.modified()? > readme_metadata.modified()? { - println!("🔄 Updating README.md for {}", project); - std::fs::write(&readme_path, &claude_content)?; - } - } - } - } - - Ok(()) - } - - /// プロジェクトメタデータの更新 - async fn update_project_metadata(&self) -> Result<()> { - let projects = self.discover_projects()?; - - for project in projects { - let ai_json_path = self.ai_root.join(&project).join("ai.json"); - - if ai_json_path.exists() { - let mut content = std::fs::read_to_string(&ai_json_path)?; - let mut json_data: serde_json::Value = serde_json::from_str(&content)?; - - // last_updated フィールドを更新 - if let Some(project_data) = json_data.get_mut(&project) { - if let Some(obj) = project_data.as_object_mut() { - obj.insert("last_updated".to_string(), - serde_json::Value::String(Utc::now().to_rfc3339())); - obj.insert("status".to_string(), - serde_json::Value::String("active".to_string())); - - content = serde_json::to_string_pretty(&json_data)?; - std::fs::write(&ai_json_path, content)?; - } - } - } - } - - Ok(()) - } - - /// メインai.jsonからプロジェクトのブランチ情報を取得 - fn get_project_branch(&self, project: &str) -> String { - let main_ai_json_path = self.ai_root.join("ai.json"); - - if main_ai_json_path.exists() { - if let Ok(content) = std::fs::read_to_string(&main_ai_json_path) { - if let Ok(json_data) = serde_json::from_str::(&content) { - if let Some(ai_section) = json_data.get("ai") { - if let Some(project_data) = ai_section.get(project) { - if let Some(branch) = project_data.get("branch").and_then(|v| v.as_str()) { - return branch.to_string(); - } - } - } - } - } - } - - // デフォルトはmain - "main".to_string() - } -} \ No newline at end of file diff --git a/src/http_client.rs b/src/http_client.rs deleted file mode 100644 index 2d02f9c..0000000 --- a/src/http_client.rs +++ /dev/null @@ -1,409 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::Client; -use serde_json::Value; -use serde::{Serialize, Deserialize}; -use std::time::Duration; -use std::collections::HashMap; - -/// Service configuration for unified service management -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ServiceConfig { - pub base_url: String, - pub timeout: Duration, - pub health_endpoint: String, -} - -impl Default for ServiceConfig { - fn default() -> Self { - Self { - base_url: "http://localhost:8000".to_string(), - timeout: Duration::from_secs(30), - health_endpoint: "/health".to_string(), - } - } -} - -/// HTTP client for inter-service communication -pub struct ServiceClient { - client: Client, - service_registry: HashMap, -} - -impl ServiceClient { - pub fn new() -> Self { - Self::with_default_services() - } - - /// Create ServiceClient with default ai ecosystem services - pub fn with_default_services() -> Self { - let client = Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .expect("Failed to create HTTP client"); - - let mut service_registry = HashMap::new(); - - // Register default ai ecosystem services - service_registry.insert("ai.card".to_string(), ServiceConfig { - base_url: "http://localhost:8000".to_string(), - timeout: Duration::from_secs(30), - health_endpoint: "/health".to_string(), - }); - - service_registry.insert("ai.log".to_string(), ServiceConfig { - base_url: "http://localhost:8002".to_string(), - timeout: Duration::from_secs(30), - health_endpoint: "/health".to_string(), - }); - - service_registry.insert("ai.bot".to_string(), ServiceConfig { - base_url: "http://localhost:8003".to_string(), - timeout: Duration::from_secs(30), - health_endpoint: "/health".to_string(), - }); - - Self { client, service_registry } - } - - /// Create ServiceClient with custom service registry - pub fn with_services(service_registry: HashMap) -> Self { - let client = Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .expect("Failed to create HTTP client"); - - Self { client, service_registry } - } - - /// Register a new service configuration - pub fn register_service(&mut self, name: String, config: ServiceConfig) { - self.service_registry.insert(name, config); - } - - /// Get service configuration by name - pub fn get_service_config(&self, service: &str) -> Result<&ServiceConfig> { - self.service_registry.get(service) - .ok_or_else(|| anyhow!("Unknown service: {}", service)) - } - - /// Universal service method call - pub async fn call_service_method( - &self, - service: &str, - method: &str, - params: &T - ) -> Result { - let config = self.get_service_config(service)?; - let url = format!("{}/{}", config.base_url.trim_end_matches('/'), method.trim_start_matches('/')); - - self.post_request(&url, &serde_json::to_value(params)?).await - } - - /// Universal service GET call - pub async fn call_service_get(&self, service: &str, endpoint: &str) -> Result { - let config = self.get_service_config(service)?; - let url = format!("{}/{}", config.base_url.trim_end_matches('/'), endpoint.trim_start_matches('/')); - - self.get_request(&url).await - } - - /// Check if a service is available - pub async fn check_service_status(&self, base_url: &str) -> Result { - let url = format!("{}/health", base_url.trim_end_matches('/')); - - match self.client.get(&url).send().await { - Ok(response) => { - if response.status().is_success() { - Ok(ServiceStatus::Available) - } else { - Ok(ServiceStatus::Error(format!("HTTP {}", response.status()))) - } - } - Err(e) => Ok(ServiceStatus::Unavailable(e.to_string())), - } - } - - /// Make a GET request to a service - pub async fn get_request(&self, url: &str) -> Result { - let response = self.client - .get(url) - .send() - .await?; - - if !response.status().is_success() { - return Err(anyhow!("Request failed with status: {}", response.status())); - } - - let json: Value = response.json().await?; - Ok(json) - } - - /// Make a POST request to a service - pub async fn post_request(&self, url: &str, body: &Value) -> Result { - let response = self.client - .post(url) - .header("Content-Type", "application/json") - .json(body) - .send() - .await?; - - if !response.status().is_success() { - return Err(anyhow!("Request failed with status: {}", response.status())); - } - - let json: Value = response.json().await?; - Ok(json) - } - - /// Get user's card collection from ai.card service - pub async fn get_user_cards(&self, user_did: &str) -> Result { - let endpoint = format!("api/v1/cards/user/{}", user_did); - self.call_service_get("ai.card", &endpoint).await - } - - /// Draw a card for user from ai.card service - pub async fn draw_card(&self, user_did: &str, is_paid: bool) -> Result { - let params = serde_json::json!({ - "user_did": user_did, - "is_paid": is_paid - }); - - self.call_service_method("ai.card", "api/v1/cards/draw", ¶ms).await - } - - /// Get card statistics from ai.card service - pub async fn get_card_stats(&self) -> Result { - self.call_service_get("ai.card", "api/v1/cards/gacha-stats").await - } - - // MARK: - ai.log service methods - - /// Create a new blog post - pub async fn create_blog_post(&self, params: &T) -> Result { - self.call_service_method("ai.log", "api/v1/posts", params).await - } - - /// Get list of blog posts - pub async fn get_blog_posts(&self) -> Result { - self.call_service_get("ai.log", "api/v1/posts").await - } - - /// Build the blog - pub async fn build_blog(&self) -> Result { - self.call_service_method("ai.log", "api/v1/build", &serde_json::json!({})).await - } - - /// Translate document using ai.log service - pub async fn translate_document(&self, params: &T) -> Result { - self.call_service_method("ai.log", "api/v1/translate", params).await - } - - /// Generate documentation using ai.log service - pub async fn generate_docs(&self, params: &T) -> Result { - self.call_service_method("ai.log", "api/v1/docs", params).await - } -} - -/// Service status enum -#[derive(Debug, Clone)] -pub enum ServiceStatus { - Available, - Unavailable(String), - Error(String), -} - -impl ServiceStatus { - pub fn is_available(&self) -> bool { - matches!(self, ServiceStatus::Available) - } -} - -/// Service detector for ai ecosystem services -pub struct ServiceDetector { - client: ServiceClient, -} - -impl ServiceDetector { - pub fn new() -> Self { - Self { - client: ServiceClient::new(), - } - } - - /// Check all ai ecosystem services - pub async fn detect_services(&self) -> ServiceMap { - let mut services = ServiceMap::default(); - - // Check ai.card service - if let Ok(status) = self.client.check_service_status("http://localhost:8000").await { - services.ai_card = Some(ServiceInfo { - base_url: "http://localhost:8000".to_string(), - status, - }); - } - - // Check ai.log service - if let Ok(status) = self.client.check_service_status("http://localhost:8001").await { - services.ai_log = Some(ServiceInfo { - base_url: "http://localhost:8001".to_string(), - status, - }); - } - - // Check ai.bot service - if let Ok(status) = self.client.check_service_status("http://localhost:8002").await { - services.ai_bot = Some(ServiceInfo { - base_url: "http://localhost:8002".to_string(), - status, - }); - } - - services - } - - /// Get available services only - pub async fn get_available_services(&self) -> Vec { - let services = self.detect_services().await; - let mut available = Vec::new(); - - if let Some(card) = &services.ai_card { - if card.status.is_available() { - available.push("ai.card".to_string()); - } - } - - if let Some(log) = &services.ai_log { - if log.status.is_available() { - available.push("ai.log".to_string()); - } - } - - if let Some(bot) = &services.ai_bot { - if bot.status.is_available() { - available.push("ai.bot".to_string()); - } - } - - available - } - - /// Get card collection statistics - pub async fn get_card_stats(&self) -> Result> { - match self.client.get_request("http://localhost:8000/api/v1/cards/gacha-stats").await { - Ok(stats) => Ok(stats), - Err(e) => Err(e.into()), - } - } - - /// Draw a card for user - pub async fn draw_card(&self, user_did: &str, is_paid: bool) -> Result> { - let payload = serde_json::json!({ - "user_did": user_did, - "is_paid": is_paid - }); - - match self.client.post_request("http://localhost:8000/api/v1/cards/draw", &payload).await { - Ok(card) => Ok(card), - Err(e) => Err(e.into()), - } - } - - /// Get user's card collection - pub async fn get_user_cards(&self, user_did: &str) -> Result> { - let url = format!("http://localhost:8000/api/v1/cards/collection?did={}", user_did); - match self.client.get_request(&url).await { - Ok(collection) => Ok(collection), - Err(e) => Err(e.into()), - } - } - - /// Get contextual memories for conversation mode - pub async fn get_contextual_memories(&self, _user_id: &str, _limit: usize) -> Result, Box> { - // This is a simplified version - in a real implementation this would call the MCP server - // For now, we'll return an empty vec to make compilation work - Ok(Vec::new()) - } - - /// Search memories by query - pub async fn search_memories(&self, _query: &str, _limit: usize) -> Result, Box> { - // This is a simplified version - in a real implementation this would call the MCP server - // For now, we'll return an empty vec to make compilation work - Ok(Vec::new()) - } - - /// Create context summary - pub async fn create_summary(&self, user_id: &str) -> Result> { - // This is a simplified version - in a real implementation this would call the MCP server - // For now, we'll return a placeholder summary - Ok(format!("Context summary for user: {}", user_id)) - } -} - -/// Service information -#[derive(Debug, Clone)] -pub struct ServiceInfo { - pub base_url: String, - pub status: ServiceStatus, -} - -/// Map of all ai ecosystem services -#[derive(Debug, Clone, Default)] -pub struct ServiceMap { - pub ai_card: Option, - pub ai_log: Option, - pub ai_bot: Option, -} - -impl ServiceMap { - /// Get service info by name - pub fn get_service(&self, name: &str) -> Option<&ServiceInfo> { - match name { - "ai.card" => self.ai_card.as_ref(), - "ai.log" => self.ai_log.as_ref(), - "ai.bot" => self.ai_bot.as_ref(), - _ => None, - } - } - - /// Check if a service is available - pub fn is_service_available(&self, name: &str) -> bool { - self.get_service(name) - .map(|info| info.status.is_available()) - .unwrap_or(false) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_service_client_creation() { - let _client = ServiceClient::new(); - // Basic test to ensure client can be created - assert!(true); - } - - #[test] - fn test_service_status() { - let status = ServiceStatus::Available; - assert!(status.is_available()); - - let status = ServiceStatus::Unavailable("Connection refused".to_string()); - assert!(!status.is_available()); - } - - #[test] - fn test_service_map() { - let mut map = ServiceMap::default(); - assert!(!map.is_service_available("ai.card")); - - map.ai_card = Some(ServiceInfo { - base_url: "http://localhost:8000".to_string(), - status: ServiceStatus::Available, - }); - - assert!(map.is_service_available("ai.card")); - assert!(!map.is_service_available("ai.log")); - } -} \ No newline at end of file diff --git a/src/import.rs b/src/import.rs deleted file mode 100644 index 4f03145..0000000 --- a/src/import.rs +++ /dev/null @@ -1,331 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use serde::Deserialize; -use anyhow::{Result, Context}; -use colored::*; -use chrono::{DateTime, Utc}; - -use crate::config::Config; -use crate::persona::Persona; -use crate::memory::{Memory, MemoryType}; - -pub async fn handle_import_chatgpt( - file_path: PathBuf, - user_id: Option, - data_dir: Option, -) -> Result<()> { - let config = Config::new(data_dir)?; - let mut persona = Persona::new(&config)?; - let user_id = user_id.unwrap_or_else(|| "imported_user".to_string()); - - println!("{}", "🚀 Starting ChatGPT Import...".cyan().bold()); - println!("File: {}", file_path.display().to_string().yellow()); - println!("User ID: {}", user_id.yellow()); - println!(); - - let mut importer = ChatGPTImporter::new(user_id); - let stats = importer.import_from_file(&file_path, &mut persona).await?; - - // Display import statistics - println!("\n{}", "📊 Import Statistics".green().bold()); - println!("Conversations imported: {}", stats.conversations_imported.to_string().cyan()); - println!("Messages imported: {}", stats.messages_imported.to_string().cyan()); - println!(" - User messages: {}", stats.user_messages.to_string().yellow()); - println!(" - Assistant messages: {}", stats.assistant_messages.to_string().yellow()); - if stats.skipped_messages > 0 { - println!(" - Skipped messages: {}", stats.skipped_messages.to_string().red()); - } - - // Show updated relationship - if let Some(relationship) = persona.get_relationship(&importer.user_id) { - println!("\n{}", "👥 Updated Relationship".blue().bold()); - println!("Status: {}", relationship.status.to_string().yellow()); - println!("Score: {:.2} / {}", relationship.score, relationship.threshold); - println!("Transmission enabled: {}", - if relationship.transmission_enabled { "✓".green() } else { "✗".red() }); - } - - println!("\n{}", "✅ ChatGPT import completed successfully!".green().bold()); - - Ok(()) -} - -#[derive(Debug, Clone)] -pub struct ImportStats { - pub conversations_imported: usize, - pub messages_imported: usize, - pub user_messages: usize, - pub assistant_messages: usize, - pub skipped_messages: usize, -} - -impl Default for ImportStats { - fn default() -> Self { - ImportStats { - conversations_imported: 0, - messages_imported: 0, - user_messages: 0, - assistant_messages: 0, - skipped_messages: 0, - } - } -} - -pub struct ChatGPTImporter { - user_id: String, - stats: ImportStats, -} - -impl ChatGPTImporter { - pub fn new(user_id: String) -> Self { - ChatGPTImporter { - user_id, - stats: ImportStats::default(), - } - } - - pub async fn import_from_file(&mut self, file_path: &PathBuf, persona: &mut Persona) -> Result { - // Read and parse the JSON file - let content = std::fs::read_to_string(file_path) - .with_context(|| format!("Failed to read file: {}", file_path.display()))?; - - let conversations: Vec = serde_json::from_str(&content) - .context("Failed to parse ChatGPT export JSON")?; - - println!("Found {} conversations to import", conversations.len()); - - // Import each conversation - for (i, conversation) in conversations.iter().enumerate() { - if i % 10 == 0 && i > 0 { - println!("Processed {} / {} conversations...", i, conversations.len()); - } - - match self.import_single_conversation(conversation, persona).await { - Ok(_) => { - self.stats.conversations_imported += 1; - } - Err(e) => { - println!("{}: Failed to import conversation '{}': {}", - "Warning".yellow(), - conversation.title.as_deref().unwrap_or("Untitled"), - e); - } - } - } - - Ok(self.stats.clone()) - } - - async fn import_single_conversation(&mut self, conversation: &ChatGPTConversation, persona: &mut Persona) -> Result<()> { - // Extract messages from the mapping structure - let messages = self.extract_messages_from_mapping(&conversation.mapping)?; - - if messages.is_empty() { - return Ok(()); - } - - // Process each message - for message in messages { - match self.process_message(&message, persona).await { - Ok(_) => { - self.stats.messages_imported += 1; - } - Err(_) => { - self.stats.skipped_messages += 1; - } - } - } - - Ok(()) - } - - fn extract_messages_from_mapping(&self, mapping: &HashMap) -> Result> { - let mut messages = Vec::new(); - - // Find all message nodes and collect them - for node in mapping.values() { - if let Some(message) = &node.message { - // Skip system messages and other non-user/assistant messages - if let Some(role) = &message.author.role { - match role.as_str() { - "user" | "assistant" => { - if let Some(content) = &message.content { - let content_text = if content.content_type == "text" && !content.parts.is_empty() { - // Extract text from parts (handle both strings and mixed content) - content.parts.iter() - .filter_map(|part| part.as_str()) - .collect::>() - .join("\n") - } else if content.content_type == "multimodal_text" { - // Extract text parts from multimodal content - let mut text_parts = Vec::new(); - for part in &content.parts { - if let Some(text) = part.as_str() { - if !text.is_empty() { - text_parts.push(text); - } - } - // Skip non-text parts (like image_asset_pointer) - } - if text_parts.is_empty() { - continue; // Skip if no text content - } - text_parts.join("\n") - } else if content.content_type == "user_editable_context" { - // Handle user context messages - if let Some(instructions) = &content.user_instructions { - format!("User instructions: {}", instructions) - } else if let Some(profile) = &content.user_profile { - format!("User profile: {}", profile) - } else { - continue; // Skip empty context messages - } - } else { - continue; // Skip other content types for now - }; - - if !content_text.trim().is_empty() { - messages.push(ChatGPTMessage { - role: role.clone(), - content: content_text, - create_time: message.create_time, - }); - } - } - } - _ => {} // Skip system, tool, etc. - } - } - } - } - - // Sort messages by creation time - messages.sort_by(|a, b| { - let time_a = a.create_time.unwrap_or(0.0); - let time_b = b.create_time.unwrap_or(0.0); - time_a.partial_cmp(&time_b).unwrap_or(std::cmp::Ordering::Equal) - }); - - Ok(messages) - } - - async fn process_message(&mut self, message: &ChatGPTMessage, persona: &mut Persona) -> Result<()> { - let timestamp = self.convert_timestamp(message.create_time.unwrap_or(0.0))?; - - match message.role.as_str() { - "user" => { - self.add_user_message(&message.content, timestamp, persona)?; - self.stats.user_messages += 1; - } - "assistant" => { - self.add_assistant_message(&message.content, timestamp, persona)?; - self.stats.assistant_messages += 1; - } - _ => { - return Err(anyhow::anyhow!("Unsupported message role: {}", message.role)); - } - } - - Ok(()) - } - - fn add_user_message(&self, content: &str, timestamp: DateTime, persona: &mut Persona) -> Result<()> { - // Create high-importance memory for user messages - let memory = Memory { - id: uuid::Uuid::new_v4().to_string(), - user_id: self.user_id.clone(), - content: content.to_string(), - summary: None, - importance: 0.8, // High importance for imported user data - memory_type: MemoryType::Core, - created_at: timestamp, - last_accessed: timestamp, - access_count: 1, - }; - - // Add memory and update relationship - persona.add_memory(memory)?; - persona.update_relationship(&self.user_id, 1.0)?; // Positive relationship boost - - Ok(()) - } - - fn add_assistant_message(&self, content: &str, timestamp: DateTime, persona: &mut Persona) -> Result<()> { - // Create medium-importance memory for assistant responses - let memory = Memory { - id: uuid::Uuid::new_v4().to_string(), - user_id: self.user_id.clone(), - content: format!("[AI Response] {}", content), - summary: Some("Imported ChatGPT response".to_string()), - importance: 0.6, // Medium importance for AI responses - memory_type: MemoryType::Summary, - created_at: timestamp, - last_accessed: timestamp, - access_count: 1, - }; - - persona.add_memory(memory)?; - - Ok(()) - } - - fn convert_timestamp(&self, unix_timestamp: f64) -> Result> { - if unix_timestamp <= 0.0 { - return Ok(Utc::now()); - } - - DateTime::from_timestamp( - unix_timestamp as i64, - ((unix_timestamp % 1.0) * 1_000_000_000.0) as u32 - ).ok_or_else(|| anyhow::anyhow!("Invalid timestamp: {}", unix_timestamp)) - } -} - -// ChatGPT Export Data Structures -#[derive(Debug, Deserialize)] -pub struct ChatGPTConversation { - pub title: Option, - pub create_time: Option, - pub mapping: HashMap, -} - -#[derive(Debug, Deserialize)] -pub struct ChatGPTNode { - pub id: Option, - pub message: Option, - pub parent: Option, - pub children: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct ChatGPTNodeMessage { - pub id: String, - pub author: ChatGPTAuthor, - pub create_time: Option, - pub content: Option, -} - -#[derive(Debug, Deserialize)] -pub struct ChatGPTAuthor { - pub role: Option, - pub name: Option, -} - -#[derive(Debug, Deserialize)] -pub struct ChatGPTContent { - pub content_type: String, - #[serde(default)] - pub parts: Vec, - #[serde(default)] - pub user_profile: Option, - #[serde(default)] - pub user_instructions: Option, -} - -// Simplified message structure for processing -#[derive(Debug, Clone)] -pub struct ChatGPTMessage { - pub role: String, - pub content: String, - pub create_time: Option, -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 542bd53..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(dead_code)] - -pub mod ai_provider; -pub mod cli; -pub mod config; -pub mod conversation; -pub mod docs; -pub mod http_client; -pub mod import; -pub mod mcp_server; -pub mod memory; -pub mod openai_provider; -pub mod persona; -pub mod relationship; -pub mod scheduler; -pub mod shell; -pub mod status; -pub mod submodules; -pub mod tokens; -pub mod transmission; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 82c870d..f733317 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,16 @@ -#![allow(dead_code)] - +use anyhow::Result; use clap::{Parser, Subcommand}; use std::path::PathBuf; -mod ai_provider; -mod cli; -use cli::TokenCommands; -mod config; -mod conversation; -mod docs; -mod http_client; -mod import; -mod mcp_server; mod memory; -mod openai_provider; -mod persona; -mod relationship; -mod scheduler; -mod shell; -mod status; -mod submodules; -mod tokens; -mod transmission; +mod mcp; + +use memory::MemoryManager; +use mcp::MCPServer; #[derive(Parser)] #[command(name = "aigpt")] -#[command(about = "AI.GPT - Autonomous transmission AI with unique personality")] -#[command(version)] +#[command(about = "Simple memory storage for Claude with MCP")] struct Cli { #[command(subcommand)] command: Commands, @@ -34,218 +18,32 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Check AI status and relationships - Status { - /// User ID to check status for - user_id: Option, - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Chat with the AI - Chat { - /// User ID (atproto DID) - user_id: String, - /// Message to send to AI - message: String, - /// Data directory - #[arg(short, long)] - data_dir: Option, - /// AI model to use - #[arg(short, long)] - model: Option, - /// AI provider (ollama/openai) - #[arg(long)] - provider: Option, - }, - /// Start continuous conversation mode with MCP integration - Conversation { - /// User ID (atproto DID) - user_id: String, - /// Data directory - #[arg(short, long)] - data_dir: Option, - /// AI model to use - #[arg(short, long)] - model: Option, - /// AI provider (ollama/openai) - #[arg(long)] - provider: Option, - }, - /// Start continuous conversation mode with MCP integration (alias) - Conv { - /// User ID (atproto DID) - user_id: String, - /// Data directory - #[arg(short, long)] - data_dir: Option, - /// AI model to use - #[arg(short, long)] - model: Option, - /// AI provider (ollama/openai) - #[arg(long)] - provider: Option, - }, - /// Check today's AI fortune - Fortune { - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// List all relationships - Relationships { - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Check and send autonomous transmissions - Transmit { - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Run daily maintenance tasks - Maintenance { - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Run scheduled tasks - Schedule { - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, /// Start MCP server - Server { - /// Port to listen on - #[arg(short, long, default_value = "8080")] - port: u16, - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Interactive shell mode - Shell { - /// User ID (atproto DID) - user_id: String, - /// Data directory - #[arg(short, long)] - data_dir: Option, - /// AI model to use - #[arg(short, long)] - model: Option, - /// AI provider (ollama/openai) - #[arg(long)] - provider: Option, - }, - /// Import ChatGPT conversation data - ImportChatgpt { - /// Path to ChatGPT export JSON file - file_path: PathBuf, - /// User ID for imported conversations - #[arg(short, long)] - user_id: Option, - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Documentation management - Docs { - /// Action to perform (generate, sync, list, status) - action: String, - /// Project name for generate/sync actions - #[arg(short, long)] - project: Option, - /// Output path for generated documentation - #[arg(short, long)] - output: Option, - /// Enable AI integration for documentation enhancement - #[arg(long)] - ai_integration: bool, - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Submodule management - Submodules { - /// Action to perform (list, update, status) - action: String, - /// Specific module to update - #[arg(short, long)] - module: Option, - /// Update all submodules - #[arg(long)] - all: bool, - /// Show what would be done without making changes - #[arg(long)] - dry_run: bool, - /// Auto-commit changes after update - #[arg(long)] - auto_commit: bool, - /// Show verbose output - #[arg(short, long)] - verbose: bool, - /// Data directory - #[arg(short, long)] - data_dir: Option, - }, - /// Token usage analysis and cost estimation - Tokens { - #[command(subcommand)] - command: TokenCommands, + Server, + /// Start MCP server (alias for server) + Serve, + /// Import ChatGPT conversations + Import { + /// Path to conversations.json file + file: PathBuf, }, } #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Status { user_id, data_dir } => { - status::handle_status(user_id, data_dir).await + Commands::Server | Commands::Serve => { + let mut server = MCPServer::new().await?; + server.run().await?; } - Commands::Chat { user_id, message, data_dir, model, provider } => { - cli::handle_chat(user_id, message, data_dir, model, provider).await - } - Commands::Conversation { user_id, data_dir, model, provider } => { - conversation::handle_conversation(user_id, data_dir, model, provider).await - } - Commands::Conv { user_id, data_dir, model, provider } => { - conversation::handle_conversation(user_id, data_dir, model, provider).await - } - Commands::Fortune { data_dir } => { - cli::handle_fortune(data_dir).await - } - Commands::Relationships { data_dir } => { - cli::handle_relationships(data_dir).await - } - Commands::Transmit { data_dir } => { - cli::handle_transmit(data_dir).await - } - Commands::Maintenance { data_dir } => { - cli::handle_maintenance(data_dir).await - } - Commands::Schedule { data_dir } => { - cli::handle_schedule(data_dir).await - } - Commands::Server { port, data_dir } => { - cli::handle_server(Some(port), data_dir).await - } - Commands::Shell { user_id, data_dir, model, provider } => { - shell::handle_shell(user_id, data_dir, model, provider).await - } - Commands::ImportChatgpt { file_path, user_id, data_dir } => { - import::handle_import_chatgpt(file_path, user_id, data_dir).await - } - Commands::Docs { action, project, output, ai_integration, data_dir } => { - docs::handle_docs(action, project, output, ai_integration, data_dir).await - } - Commands::Submodules { action, module, all, dry_run, auto_commit, verbose, data_dir } => { - submodules::handle_submodules(action, module, all, dry_run, auto_commit, verbose, data_dir).await - } - Commands::Tokens { command } => { - tokens::handle_tokens(command).await + Commands::Import { file } => { + let mut memory_manager = MemoryManager::new().await?; + memory_manager.import_chatgpt_conversations(&file).await?; + println!("Import completed successfully"); } } -} + + Ok(()) +} \ No newline at end of file diff --git a/src/mcp.rs b/src/mcp.rs new file mode 100644 index 0000000..d8877cb --- /dev/null +++ b/src/mcp.rs @@ -0,0 +1,250 @@ +use anyhow::Result; +use serde_json::{json, Value}; +use std::io::{self, BufRead, Write}; + +use crate::memory::MemoryManager; + +pub struct MCPServer { + memory_manager: MemoryManager, +} + +impl MCPServer { + pub async fn new() -> Result { + let memory_manager = MemoryManager::new().await?; + Ok(MCPServer { memory_manager }) + } + + pub async fn run(&mut self) -> Result<()> { + let stdin = io::stdin(); + let mut stdout = io::stdout(); + + // Set up line-based reading + let reader = stdin.lock(); + let lines = reader.lines(); + + for line_result in lines { + match line_result { + Ok(line) => { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + if let Ok(request) = serde_json::from_str::(&trimmed) { + let response = self.handle_request(request).await; + let response_str = serde_json::to_string(&response)?; + stdout.write_all(response_str.as_bytes())?; + stdout.write_all(b"\n")?; + stdout.flush()?; + } + } + Err(_) => { + // EOF or error, exit gracefully + break; + } + } + } + + Ok(()) + } + + async fn handle_request(&mut self, request: Value) -> Value { + let method = request["method"].as_str().unwrap_or(""); + let id = request["id"].clone(); + + match method { + "initialize" => { + json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "aigpt", + "version": "0.1.0" + } + } + }) + } + "tools/list" => { + json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "tools": [ + { + "name": "create_memory", + "description": "Create a new memory entry", + "inputSchema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Content of the memory" + } + }, + "required": ["content"] + } + }, + { + "name": "update_memory", + "description": "Update an existing memory entry", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the memory to update" + }, + "content": { + "type": "string", + "description": "New content for the memory" + } + }, + "required": ["id", "content"] + } + }, + { + "name": "delete_memory", + "description": "Delete a memory entry", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the memory to delete" + } + }, + "required": ["id"] + } + }, + { + "name": "search_memories", + "description": "Search memories by content", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + } + }, + "required": ["query"] + } + }, + { + "name": "list_conversations", + "description": "List all imported conversations", + "inputSchema": { + "type": "object", + "properties": {} + } + } + ] + } + }) + } + "tools/call" => { + let tool_name = request["params"]["name"].as_str().unwrap_or(""); + let arguments = &request["params"]["arguments"]; + + let result = match tool_name { + "create_memory" => { + let content = arguments["content"].as_str().unwrap_or(""); + match self.memory_manager.create_memory(content) { + Ok(id) => json!({ + "success": true, + "id": id, + "message": "Memory created successfully" + }), + Err(e) => json!({ + "success": false, + "error": e.to_string() + }) + } + } + "update_memory" => { + let id = arguments["id"].as_str().unwrap_or(""); + let content = arguments["content"].as_str().unwrap_or(""); + match self.memory_manager.update_memory(id, content) { + Ok(()) => json!({ + "success": true, + "message": "Memory updated successfully" + }), + Err(e) => json!({ + "success": false, + "error": e.to_string() + }) + } + } + "delete_memory" => { + let id = arguments["id"].as_str().unwrap_or(""); + match self.memory_manager.delete_memory(id) { + Ok(()) => json!({ + "success": true, + "message": "Memory deleted successfully" + }), + Err(e) => json!({ + "success": false, + "error": e.to_string() + }) + } + } + "search_memories" => { + let query = arguments["query"].as_str().unwrap_or(""); + let memories = self.memory_manager.search_memories(query); + json!({ + "success": true, + "memories": memories.into_iter().map(|m| json!({ + "id": m.id, + "content": m.content, + "created_at": m.created_at, + "updated_at": m.updated_at + })).collect::>() + }) + } + "list_conversations" => { + let conversations = self.memory_manager.list_conversations(); + json!({ + "success": true, + "conversations": conversations.into_iter().map(|c| json!({ + "id": c.id, + "title": c.title, + "created_at": c.created_at, + "message_count": c.message_count + })).collect::>() + }) + } + _ => json!({ + "success": false, + "error": format!("Unknown tool: {}", tool_name) + }) + }; + + json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "content": [{ + "type": "text", + "text": result.to_string() + }] + } + }) + } + _ => { + json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": -32601, + "message": "Method not found" + } + }) + } + } + } +} \ No newline at end of file diff --git a/src/mcp_server.rs b/src/mcp_server.rs deleted file mode 100644 index d0d0a7d..0000000 --- a/src/mcp_server.rs +++ /dev/null @@ -1,1951 +0,0 @@ -use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context}; -use serde_json::{json, Value}; -use std::path::Path; -use std::sync::Arc; -use tokio::sync::Mutex; - -use axum::{ - extract::{Path as AxumPath, State}, - http::Method, - response::Json, - routing::{get, post}, - Router, -}; -use tower::ServiceBuilder; -use tower_http::cors::{CorsLayer, Any}; - -use crate::config::Config; -use crate::persona::Persona; -use crate::transmission::TransmissionController; -use crate::scheduler::AIScheduler; -use crate::http_client::{ServiceClient, ServiceDetector}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MCPTool { - pub name: String, - pub description: String, - pub input_schema: Value, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MCPRequest { - pub method: String, - pub params: Value, - pub id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MCPResponse { - pub result: Option, - pub error: Option, - pub id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MCPError { - pub code: i32, - pub message: String, - pub data: Option, -} - -// HTTP MCP Server state -pub type AppState = Arc>; - -// MCP HTTP request types for REST-style endpoints -#[derive(Debug, Serialize, Deserialize)] -pub struct MCPHttpRequest { - pub user_id: Option, - pub message: Option, - pub provider: Option, - pub model: Option, - pub query: Option, - pub keywords: Option>, - pub limit: Option, - pub content: Option, - pub file_path: Option, - pub command: Option, - pub pattern: Option, -} - -#[derive(Debug, Serialize)] -pub struct MCPHttpResponse { - pub success: bool, - pub result: Option, - pub error: Option, -} - -#[allow(dead_code)] -pub struct MCPServer { - config: Config, - persona: Persona, - transmission_controller: TransmissionController, - scheduler: AIScheduler, - service_client: ServiceClient, - service_detector: ServiceDetector, - user_id: String, - data_dir: Option, -} - -impl MCPServer { - pub fn new(config: Config, user_id: String, data_dir: Option) -> Result { - let persona = Persona::new(&config)?; - let transmission_controller = TransmissionController::new(config.clone())?; - let scheduler = AIScheduler::new(&config)?; - let service_client = ServiceClient::new(); - let service_detector = ServiceDetector::new(); - - Ok(MCPServer { - config, - persona, - transmission_controller, - scheduler, - service_client, - service_detector, - user_id, - data_dir, - }) - } - - pub fn get_tools(&self) -> Vec { - vec![ - MCPTool { - name: "get_status".to_string(), - description: "Get AI status including mood, fortune, and personality".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID to get relationship status for (optional)" - } - } - }), - }, - MCPTool { - name: "chat_with_ai".to_string(), - description: "Send a message to the AI and get a response".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID for the conversation" - }, - "message": { - "type": "string", - "description": "Message to send to the AI" - }, - "provider": { - "type": "string", - "description": "AI provider to use (ollama/openai) - optional" - }, - "model": { - "type": "string", - "description": "AI model to use - optional" - } - }, - "required": ["user_id", "message"] - }), - }, - MCPTool { - name: "get_relationships".to_string(), - description: "Get all relationships and their statuses".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": {} - }), - }, - MCPTool { - name: "get_memories".to_string(), - description: "Get memories for a specific user".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID to get memories for" - }, - "limit": { - "type": "integer", - "description": "Maximum number of memories to return (default: 10)" - } - }, - "required": ["user_id"] - }), - }, - MCPTool { - name: "get_contextual_memories".to_string(), - description: "Get memories organized by priority with contextual relevance".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID to get memories for" - }, - "query": { - "type": "string", - "description": "Query text for contextual relevance matching" - }, - "limit": { - "type": "integer", - "description": "Maximum number of memories to return (default: 10)" - } - }, - "required": ["user_id", "query"] - }), - }, - MCPTool { - name: "search_memories".to_string(), - description: "Search memories by keywords and types".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID to search memories for" - }, - "keywords": { - "type": "array", - "items": {"type": "string"}, - "description": "Keywords to search for in memory content" - }, - "memory_types": { - "type": "array", - "items": {"type": "string"}, - "description": "Memory types to filter by (conversation, core, summary, experience)" - } - }, - "required": ["user_id", "keywords"] - }), - }, - MCPTool { - name: "create_summary".to_string(), - description: "Create AI-powered summary of recent memories".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID to create summary for" - } - }, - "required": ["user_id"] - }), - }, - MCPTool { - name: "create_core_memory".to_string(), - description: "Create core memory by analyzing existing memories".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "User ID to create core memory for" - } - }, - "required": ["user_id"] - }), - }, - MCPTool { - name: "execute_command".to_string(), - description: "Execute shell commands with safety checks".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "Shell command to execute" - }, - "working_dir": { - "type": "string", - "description": "Working directory for command execution (optional)" - }, - "timeout": { - "type": "integer", - "description": "Timeout in seconds (default: 30)" - } - }, - "required": ["command"] - }), - }, - MCPTool { - name: "analyze_file".to_string(), - description: "Analyze files using AI provider".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to file to analyze" - }, - "analysis_type": { - "type": "string", - "description": "Type of analysis (code, text, structure, security)", - "default": "general" - } - }, - "required": ["file_path"] - }), - }, - MCPTool { - name: "write_file".to_string(), - description: "Write content to files with backup functionality".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to file to write" - }, - "content": { - "type": "string", - "description": "Content to write to file" - }, - "create_backup": { - "type": "boolean", - "description": "Create backup before writing (default: true)" - } - }, - "required": ["file_path", "content"] - }), - }, - MCPTool { - name: "list_files".to_string(), - description: "List files in directory with pattern matching".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "directory": { - "type": "string", - "description": "Directory to list files from" - }, - "pattern": { - "type": "string", - "description": "Glob pattern for file filtering (optional)" - }, - "recursive": { - "type": "boolean", - "description": "Recursive directory traversal (default: false)" - } - }, - "required": ["directory"] - }), - }, - MCPTool { - name: "check_transmissions".to_string(), - description: "Check and execute autonomous transmissions".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": {} - }), - }, - MCPTool { - name: "run_maintenance".to_string(), - description: "Run daily maintenance tasks".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": {} - }), - }, - MCPTool { - name: "run_scheduler".to_string(), - description: "Run scheduled tasks".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": {} - }), - }, - MCPTool { - name: "get_scheduler_status".to_string(), - description: "Get scheduler statistics and upcoming tasks".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": {} - }), - }, - MCPTool { - name: "get_transmission_history".to_string(), - description: "Get recent transmission history".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "limit": { - "type": "integer", - "description": "Maximum number of transmissions to return (default: 10)" - } - } - }), - }, - MCPTool { - name: "get_user_cards".to_string(), - description: "Get user's card collection from ai.card service".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_did": { - "type": "string", - "description": "User DID to get cards for" - } - }, - "required": ["user_did"] - }), - }, - MCPTool { - name: "draw_card".to_string(), - description: "Draw a card from ai.card gacha system".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_did": { - "type": "string", - "description": "User DID to draw card for" - }, - "is_paid": { - "type": "boolean", - "description": "Whether this is a premium draw (default: false)" - } - }, - "required": ["user_did"] - }), - }, - MCPTool { - name: "get_draw_status".to_string(), - description: "Check if user can draw cards (daily limit check)".to_string(), - input_schema: serde_json::json!({ - "type": "object", - "properties": { - "user_did": { - "type": "string", - "description": "User DID to check draw status for" - } - }, - "required": ["user_did"] - }), - }, - ] - } - - pub async fn handle_request(&mut self, request: MCPRequest) -> MCPResponse { - let result = match request.method.as_str() { - "tools/list" => self.handle_list_tools().await, - "tools/call" => self.handle_tool_call(request.params).await, - // HTTP endpoint直接呼び出し対応 - method => self.handle_direct_tool_call(method, request.params).await, - }; - - match result { - Ok(value) => MCPResponse { - result: Some(value), - error: None, - id: request.id, - }, - Err(e) => MCPResponse { - result: None, - error: Some(MCPError { - code: -32603, - message: e.to_string(), - data: None, - }), - id: request.id, - }, - } - } - - async fn handle_list_tools(&self) -> Result { - let tools = self.get_tools(); - Ok(serde_json::json!({ - "tools": tools - })) - } - - async fn handle_tool_call(&mut self, params: Value) -> Result { - let tool_name = params["name"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing tool name"))?; - let arguments = params["arguments"].clone(); - - match tool_name { - "get_status" => self.tool_get_status(arguments).await, - "chat_with_ai" => self.tool_chat_with_ai(arguments).await, - "get_relationships" => self.tool_get_relationships(arguments).await, - "get_contextual_memories" => self.tool_get_contextual_memories(arguments).await, - "search_memories" => self.tool_search_memories(arguments).await, - "create_summary" => self.tool_create_summary(arguments).await, - "create_core_memory" => self.tool_create_core_memory(arguments).await, - "execute_command" => self.tool_execute_command(arguments).await, - "analyze_file" => self.tool_analyze_file(arguments).await, - "write_file" => self.tool_write_file(arguments).await, - "list_files" => self.tool_list_files(arguments).await, - "get_memories" => self.tool_get_memories(arguments).await, - "check_transmissions" => self.tool_check_transmissions(arguments).await, - "run_maintenance" => self.tool_run_maintenance(arguments).await, - "run_scheduler" => self.tool_run_scheduler(arguments).await, - "get_scheduler_status" => self.tool_get_scheduler_status(arguments).await, - "get_transmission_history" => self.tool_get_transmission_history(arguments).await, - "get_user_cards" => self.tool_get_user_cards(arguments).await, - "draw_card" => self.tool_draw_card(arguments).await, - "get_draw_status" => self.tool_get_draw_status(arguments).await, - _ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)), - } - } - - async fn handle_direct_tool_call(&mut self, tool_name: &str, params: Value) -> Result { - // HTTP endpointからの直接呼び出し用(パラメータ構造が異なる) - match tool_name { - "get_status" => self.tool_get_status(params).await, - "chat_with_ai" => self.tool_chat_with_ai(params).await, - "get_relationships" => self.tool_get_relationships(params).await, - "get_memories" => self.tool_get_memories(params).await, - "get_contextual_memories" => self.tool_get_contextual_memories(params).await, - "search_memories" => self.tool_search_memories(params).await, - "create_summary" => self.tool_create_summary(params).await, - "create_core_memory" => self.tool_create_core_memory(params).await, - "execute_command" => self.tool_execute_command(params).await, - "analyze_file" => self.tool_analyze_file(params).await, - "write_file" => self.tool_write_file(params).await, - "list_files" => self.tool_list_files(params).await, - "check_transmissions" => self.tool_check_transmissions(params).await, - "run_maintenance" => self.tool_run_maintenance(params).await, - "run_scheduler" => self.tool_run_scheduler(params).await, - "get_scheduler_status" => self.tool_get_scheduler_status(params).await, - "get_transmission_history" => self.tool_get_transmission_history(params).await, - "get_user_cards" => self.tool_get_user_cards(params).await, - "draw_card" => self.tool_draw_card(params).await, - "get_draw_status" => self.tool_get_draw_status(params).await, - _ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)), - } - } - - async fn tool_get_status(&self, args: Value) -> Result { - let user_id = args["user_id"].as_str(); - let state = self.persona.get_current_state()?; - - let mut result = serde_json::json!({ - "mood": state.current_mood, - "fortune": state.fortune_value, - "breakthrough_triggered": state.breakthrough_triggered, - "personality": state.base_personality - }); - - if let Some(user_id) = user_id { - if let Some(relationship) = self.persona.get_relationship(user_id) { - result["relationship"] = serde_json::json!({ - "status": relationship.status.to_string(), - "score": relationship.score, - "threshold": relationship.threshold, - "transmission_enabled": relationship.transmission_enabled, - "total_interactions": relationship.total_interactions, - "is_broken": relationship.is_broken - }); - } - } - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&result)? - } - ] - })) - } - - async fn tool_chat_with_ai(&mut self, args: Value) -> Result { - let user_id = args["user_id"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?; - let message = args["message"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing message"))?; - let provider = args["provider"].as_str().map(|s| s.to_string()); - let model = args["model"].as_str().map(|s| s.to_string()); - - let (response, relationship_delta) = if provider.is_some() || model.is_some() { - self.persona.process_ai_interaction(user_id, message, provider, model).await? - } else { - self.persona.process_interaction(user_id, message)? - }; - - let relationship_status = self.persona.get_relationship(user_id) - .map(|r| serde_json::json!({ - "status": r.status.to_string(), - "score": r.score, - "transmission_enabled": r.transmission_enabled - })); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("AI Response: {}\n\nRelationship Change: {:+.2}\n\nRelationship Status: {}", - response, - relationship_delta, - relationship_status.map(|r| r.to_string()).unwrap_or_else(|| "None".to_string())) - } - ] - })) - } - - async fn tool_get_relationships(&self, _args: Value) -> Result { - let relationships = self.persona.list_all_relationships(); - - let relationships_json: Vec = relationships.iter() - .map(|(user_id, rel)| { - serde_json::json!({ - "user_id": user_id, - "status": rel.status.to_string(), - "score": rel.score, - "threshold": rel.threshold, - "transmission_enabled": rel.transmission_enabled, - "total_interactions": rel.total_interactions, - "is_broken": rel.is_broken, - "last_interaction": rel.last_interaction - }) - }) - .collect(); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&relationships_json)? - } - ] - })) - } - - async fn tool_get_memories(&mut self, args: Value) -> Result { - let user_id = args["user_id"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?; - let limit = args["limit"].as_u64().unwrap_or(10) as usize; - - let memories = self.persona.get_memories(user_id, limit); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&memories)? - } - ] - })) - } - - async fn tool_check_transmissions(&mut self, _args: Value) -> Result { - let autonomous = self.transmission_controller.check_autonomous_transmissions(&mut self.persona).await?; - let breakthrough = self.transmission_controller.check_breakthrough_transmissions(&mut self.persona).await?; - let maintenance = self.transmission_controller.check_maintenance_transmissions(&mut self.persona).await?; - - let total = autonomous.len() + breakthrough.len() + maintenance.len(); - - let result = serde_json::json!({ - "autonomous_transmissions": autonomous.len(), - "breakthrough_transmissions": breakthrough.len(), - "maintenance_transmissions": maintenance.len(), - "total_transmissions": total, - "transmissions": { - "autonomous": autonomous, - "breakthrough": breakthrough, - "maintenance": maintenance - } - }); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&result)? - } - ] - })) - } - - async fn tool_get_contextual_memories(&self, args: Value) -> Result { - let user_id = args["user_id"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?; - let query = args["query"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing query"))?; - let limit = args["limit"].as_u64().unwrap_or(10) as usize; - - // For now, use search_memories as a placeholder for contextual memories - let keywords = query.split_whitespace().map(|s| s.to_string()).collect::>(); - let memory_results = self.persona.search_memories(user_id, &keywords); - let memories = memory_results.into_iter().take(limit).collect::>(); - - let memories_json: Vec = memories.iter() - .enumerate() - .map(|(id, content)| { - serde_json::json!({ - "id": id, - "content": content, - "level": "conversation", - "importance": 0.5, - "is_core": false, - "timestamp": chrono::Utc::now().to_rfc3339(), - "summary": None::, - "metadata": {} - }) - }) - .collect(); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Found {} contextual memories for query: '{}'\n\n{}", - memories.len(), - query, - serde_json::to_string_pretty(&memories_json)?) - } - ] - })) - } - - async fn tool_search_memories(&self, args: Value) -> Result { - let user_id = args["user_id"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?; - let keywords: Vec = args["keywords"].as_array() - .ok_or_else(|| anyhow::anyhow!("Missing keywords"))? - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - - if keywords.is_empty() { - return Err(anyhow::anyhow!("At least one keyword is required")); - } - - let memories = self.persona.search_memories(user_id, &keywords); - - let memories_json: Vec = memories.iter() - .enumerate() - .map(|(id, content)| { - serde_json::json!({ - "id": id, - "content": content, - "level": "conversation", - "importance": 0.5, - "is_core": false, - "timestamp": chrono::Utc::now().to_rfc3339(), - "summary": None::, - "metadata": {} - }) - }) - .collect(); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Found {} memories matching keywords: {}\n\n{}", - memories.len(), - keywords.join(", "), - serde_json::to_string_pretty(&memories_json)?) - } - ] - })) - } - - async fn tool_create_summary(&mut self, args: Value) -> Result { - let user_id = args["user_id"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?; - - // Placeholder implementation - in full version this would use AI - let result = format!("Summary created for user: {}", user_id); - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": result - } - ] - })) - } - - async fn tool_create_core_memory(&mut self, args: Value) -> Result { - let user_id = args["user_id"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?; - - // Placeholder implementation - in full version this would use AI - let result = format!("Core memory created for user: {}", user_id); - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": result - } - ] - })) - } - - async fn tool_execute_command(&self, args: Value) -> Result { - let command = args["command"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing command"))?; - let working_dir = args["working_dir"].as_str(); - let _timeout = args["timeout"].as_u64().unwrap_or(30); - - // Security check - block dangerous commands - if self.is_dangerous_command(command) { - return Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Command blocked for security reasons: {}", command) - } - ] - })); - } - - use std::process::Command; - - let mut cmd = if cfg!(target_os = "windows") { - let mut c = Command::new("cmd"); - c.args(["/C", command]); - c - } else { - let mut c = Command::new("sh"); - c.args(["-c", command]); - c - }; - - if let Some(dir) = working_dir { - cmd.current_dir(dir); - } - - match cmd.output() { - Ok(output) => { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let status = output.status; - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Command: {}\nStatus: {}\n\nSTDOUT:\n{}\n\nSTDERR:\n{}", - command, status, stdout, stderr) - } - ] - })) - } - Err(e) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Failed to execute command '{}': {}", command, e) - } - ] - })) - } - } - } - - fn is_dangerous_command(&self, command: &str) -> bool { - let dangerous_patterns = [ - "rm -rf", "rmdir", "del /q", "format", "fdisk", - "dd if=", "mkfs", "shutdown", "reboot", "halt", - "sudo rm", "sudo dd", "chmod 777", "chown root", - "> /dev/", "curl", "wget", "nc ", "netcat", - ]; - - dangerous_patterns.iter().any(|pattern| command.contains(pattern)) - } - - async fn tool_analyze_file(&self, args: Value) -> Result { - let file_path = args["file_path"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing file_path"))?; - let analysis_type = args["analysis_type"].as_str().unwrap_or("general"); - - use std::fs; - use std::path::Path; - - let path = Path::new(file_path); - if !path.exists() { - return Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("File not found: {}", file_path) - } - ] - })); - } - - match fs::read_to_string(path) { - Ok(content) => { - let file_size = content.len(); - let line_count = content.lines().count(); - let analysis = match analysis_type { - "code" => self.analyze_code_content(&content), - "structure" => self.analyze_file_structure(&content), - "security" => self.analyze_security(&content), - _ => self.analyze_general_content(&content), - }; - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("File Analysis: {}\nType: {}\nSize: {} bytes\nLines: {}\n\n{}", - file_path, analysis_type, file_size, line_count, analysis) - } - ] - })) - } - Err(e) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Failed to read file '{}': {}", file_path, e) - } - ] - })) - } - } - } - - fn analyze_code_content(&self, content: &str) -> String { - let mut analysis = String::new(); - - // Basic code analysis - if content.contains("fn ") || content.contains("function ") { - analysis.push_str("Language: Likely Rust or JavaScript\n"); - } - if content.contains("def ") { - analysis.push_str("Language: Likely Python\n"); - } - if content.contains("class ") { - analysis.push_str("Contains class definitions\n"); - } - if content.contains("import ") || content.contains("use ") { - analysis.push_str("Contains import/use statements\n"); - } - - analysis.push_str(&format!("Functions/methods found: {}\n", - content.matches("fn ").count() + content.matches("def ").count() + content.matches("function ").count())); - - analysis - } - - fn analyze_file_structure(&self, content: &str) -> String { - format!("Structure analysis:\n- Characters: {}\n- Words: {}\n- Lines: {}\n- Paragraphs: {}", - content.len(), - content.split_whitespace().count(), - content.lines().count(), - content.split("\n\n").count()) - } - - fn analyze_security(&self, content: &str) -> String { - let mut issues = Vec::new(); - - if content.contains("password") || content.contains("secret") { - issues.push("Contains potential password/secret references"); - } - if content.contains("api_key") || content.contains("token") { - issues.push("Contains potential API keys or tokens"); - } - if content.contains("eval(") || content.contains("exec(") { - issues.push("Contains potentially dangerous eval/exec calls"); - } - - if issues.is_empty() { - "No obvious security issues found".to_string() - } else { - format!("Security concerns:\n- {}", issues.join("\n- ")) - } - } - - fn analyze_general_content(&self, content: &str) -> String { - format!("General analysis:\n- File appears to be text-based\n- Contains {} characters\n- {} lines total", - content.len(), content.lines().count()) - } - - async fn tool_write_file(&self, args: Value) -> Result { - let file_path = args["file_path"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing file_path"))?; - let content = args["content"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing content"))?; - let create_backup = args["create_backup"].as_bool().unwrap_or(true); - - use std::fs; - use std::path::Path; - - let path = Path::new(file_path); - - // Create backup if file exists and backup is requested - if create_backup && path.exists() { - let backup_path = format!("{}.backup", file_path); - if let Err(e) = fs::copy(file_path, &backup_path) { - return Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Failed to create backup: {}", e) - } - ] - })); - } - } - - match fs::write(path, content) { - Ok(()) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Successfully wrote {} bytes to: {}", content.len(), file_path) - } - ] - })) - } - Err(e) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Failed to write file '{}': {}", file_path, e) - } - ] - })) - } - } - } - - async fn tool_list_files(&self, args: Value) -> Result { - let directory = args["directory"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing directory"))?; - let pattern = args["pattern"].as_str(); - let recursive = args["recursive"].as_bool().unwrap_or(false); - - use std::fs; - use std::path::Path; - - let dir_path = Path::new(directory); - if !dir_path.exists() || !dir_path.is_dir() { - return Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("Directory not found or not a directory: {}", directory) - } - ] - })); - } - - let mut files = Vec::new(); - if recursive { - self.collect_files_recursive(dir_path, pattern, &mut files); - } else { - if let Ok(entries) = fs::read_dir(dir_path) { - for entry in entries.flatten() { - let path = entry.path(); - let file_name = path.file_name().unwrap_or_default().to_string_lossy(); - - if let Some(pat) = pattern { - if !file_name.contains(pat) { - continue; - } - } - - files.push(format!("{} ({})", - path.display(), - if path.is_dir() { "directory" } else { "file" })); - } - } - } - - files.sort(); - let result = if files.is_empty() { - "No files found matching criteria".to_string() - } else { - format!("Found {} items:\n{}", files.len(), files.join("\n")) - }; - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": result - } - ] - })) - } - - fn collect_files_recursive(&self, dir: &Path, pattern: Option<&str>, files: &mut Vec) { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - let file_name = path.file_name().unwrap_or_default().to_string_lossy(); - - if path.is_dir() { - self.collect_files_recursive(&path, pattern, files); - } else { - if let Some(pat) = pattern { - if !file_name.contains(pat) { - continue; - } - } - files.push(path.display().to_string()); - } - } - } - } - - async fn tool_run_maintenance(&mut self, _args: Value) -> Result { - self.persona.daily_maintenance()?; - let maintenance_transmissions = self.transmission_controller.check_maintenance_transmissions(&mut self.persona).await?; - - let stats = self.persona.get_relationship_stats(); - - let result = serde_json::json!({ - "maintenance_completed": true, - "maintenance_transmissions_sent": maintenance_transmissions.len(), - "relationship_stats": stats - }); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&result)? - } - ] - })) - } - - async fn tool_run_scheduler(&mut self, _args: Value) -> Result { - let executions = self.scheduler.run_scheduled_tasks(&mut self.persona, &mut self.transmission_controller).await?; - let stats = self.scheduler.get_scheduler_stats(); - - let result = serde_json::json!({ - "tasks_executed": executions.len(), - "executions": executions, - "scheduler_stats": { - "total_tasks": stats.total_tasks, - "enabled_tasks": stats.enabled_tasks, - "due_tasks": stats.due_tasks, - "total_executions": stats.total_executions, - "today_executions": stats.today_executions, - "success_rate": stats.success_rate, - "avg_duration_ms": stats.avg_duration_ms - } - }); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&result)? - } - ] - })) - } - - async fn tool_get_scheduler_status(&self, _args: Value) -> Result { - let stats = self.scheduler.get_scheduler_stats(); - let upcoming_tasks: Vec<_> = self.scheduler.get_tasks() - .values() - .filter(|task| task.enabled) - .take(10) - .map(|task| { - serde_json::json!({ - "id": task.id, - "task_type": task.task_type.to_string(), - "next_run": task.next_run, - "interval_hours": task.interval_hours, - "run_count": task.run_count - }) - }) - .collect(); - - let recent_executions = self.scheduler.get_execution_history(Some(5)); - - let result = serde_json::json!({ - "scheduler_stats": { - "total_tasks": stats.total_tasks, - "enabled_tasks": stats.enabled_tasks, - "due_tasks": stats.due_tasks, - "total_executions": stats.total_executions, - "today_executions": stats.today_executions, - "success_rate": stats.success_rate, - "avg_duration_ms": stats.avg_duration_ms - }, - "upcoming_tasks": upcoming_tasks, - "recent_executions": recent_executions - }); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&result)? - } - ] - })) - } - - async fn tool_get_transmission_history(&self, args: Value) -> Result { - let limit = args["limit"].as_u64().unwrap_or(10) as usize; - let transmissions = self.transmission_controller.get_recent_transmissions(limit); - - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": serde_json::to_string_pretty(&transmissions)? - } - ] - })) - } - - // MARK: - ai.card Integration Tools - - async fn tool_get_user_cards(&self, args: Value) -> Result { - let user_did = args["user_did"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_did"))?; - - // Use ServiceClient to call ai.card API - match self.service_client.get_user_cards(user_did).await { - Ok(cards) => { - let card_count = cards.as_array().map(|arr| arr.len()).unwrap_or(0); - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("ユーザー {} のカード一覧 ({}枚):\n\n{}", - user_did, - card_count, - serde_json::to_string_pretty(&cards)?) - } - ] - })) - } - Err(e) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("カード取得エラー: {}. ai.cardサーバーが起動していることを確認してください。", e) - } - ] - })) - } - } - } - - async fn tool_draw_card(&self, args: Value) -> Result { - let user_did = args["user_did"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_did"))?; - let is_paid = args["is_paid"].as_bool().unwrap_or(false); - - // Use ServiceClient to call ai.card API - match self.service_client.draw_card(user_did, is_paid).await { - Ok(draw_result) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("🎉 カードドロー結果:\n\n{}", - serde_json::to_string_pretty(&draw_result)?) - } - ] - })) - } - Err(e) => { - let error_msg = e.to_string(); - if error_msg.contains("429") { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": "⏰ カードドロー制限中です。日別制限により、現在カードを引くことができません。時間を置いてから再度お試しください。" - } - ] - })) - } else { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("カードドローエラー: {}. ai.cardサーバーが起動していることを確認してください。", e) - } - ] - })) - } - } - } - } - - async fn tool_get_draw_status(&self, args: Value) -> Result { - let user_did = args["user_did"].as_str() - .ok_or_else(|| anyhow::anyhow!("Missing user_did"))?; - - // Use ServiceClient to call ai.card API - match self.service_client.get_request(&format!("http://localhost:8000/api/v1/cards/draw-status/{}", user_did)).await { - Ok(status) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("ユーザー {} のドロー状況:\n\n{}", - user_did, - serde_json::to_string_pretty(&status)?) - } - ] - })) - } - Err(e) => { - Ok(serde_json::json!({ - "content": [ - { - "type": "text", - "text": format!("ドロー状況取得エラー: {}. ai.cardサーバーが起動していることを確認してください。", e) - } - ] - })) - } - } - } - - pub async fn start_server(&mut self, port: u16) -> Result<()> { - println!("🚀 Starting MCP Server on port {}", port); - println!("📋 Available tools: {}", self.get_tools().len()); - - // Print available tools - for tool in self.get_tools() { - println!(" - {}: {}", ColorExt::cyan(tool.name.as_str()), tool.description); - } - - println!("✅ MCP Server ready for requests"); - - // Create shared state - let app_state: AppState = Arc::new(Mutex::new( - MCPServer::new( - self.config.clone(), - self.user_id.clone(), - self.data_dir.clone(), - )? - )); - - // Create router with CORS - let app = Router::new() - // MCP Core endpoints - .route("/mcp/tools", get(list_tools)) - .route("/mcp/call/:tool_name", post(call_tool)) - - // AI Chat endpoints - .route("/chat", post(chat_with_ai_handler)) - .route("/status", get(get_status_handler)) - .route("/status/:user_id", get(get_status_with_user_handler)) - - // Memory endpoints - .route("/memories/:user_id", get(get_memories_handler)) - .route("/memories/:user_id/search", post(search_memories_handler)) - .route("/memories/:user_id/contextual", post(get_contextual_memories_handler)) - .route("/memories/:user_id/summary", post(create_summary_handler)) - .route("/memories/:user_id/core", post(create_core_memory_handler)) - - // Relationship endpoints - .route("/relationships", get(get_relationships_handler)) - - // System endpoints - .route("/transmissions", get(check_transmissions_handler)) - .route("/maintenance", post(run_maintenance_handler)) - .route("/scheduler", post(run_scheduler_handler)) - .route("/scheduler/status", get(get_scheduler_status_handler)) - .route("/scheduler/history", get(get_transmission_history_handler)) - - // File operations - .route("/files", get(list_files_handler)) - .route("/files/analyze", post(analyze_file_handler)) - .route("/files/write", post(write_file_handler)) - - // Shell execution - .route("/execute", post(execute_command_handler)) - - // AI Card and AI Log proxy endpoints - .route("/card/user_cards/:user_id", get(get_user_cards_handler)) - .route("/card/draw", post(draw_card_handler)) - .route("/card/stats", get(get_card_stats_handler)) - .route("/log/posts", get(get_blog_posts_handler)) - .route("/log/posts", post(create_blog_post_handler)) - .route("/log/build", post(build_blog_handler)) - - .layer( - ServiceBuilder::new() - .layer( - CorsLayer::new() - .allow_origin(Any) - .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) - .allow_headers(Any), - ) - ) - .with_state(app_state); - - // Start the server - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) - .await - .context("Failed to bind to address")?; - - println!("🌐 HTTP MCP Server listening on http://0.0.0.0:{}", port); - - axum::serve(listener, app) - .await - .context("Server error")?; - - Ok(()) - } - - pub async fn run(&mut self, port: u16) -> Result<()> { - self.start_server(port).await - } -} - -// Helper trait for colored output (placeholder) -trait ColorExt { - fn cyan(&self) -> String; -} - -impl ColorExt for str { - fn cyan(&self) -> String { - self.to_string() // In real implementation, would add ANSI color codes - } -} - -// HTTP Handlers for MCP endpoints - -// MCP Core handlers -async fn list_tools(State(state): State) -> Json { - let server = state.lock().await; - let tools = server.get_tools(); - - Json(MCPHttpResponse { - success: true, - result: Some(json!({ - "tools": tools - })), - error: None, - }) -} - -async fn call_tool( - State(state): State, - AxumPath(tool_name): AxumPath, - Json(request): Json, -) -> Json { - let mut server = state.lock().await; - - // Create MCP request from HTTP request - let mcp_request = MCPRequest { - method: tool_name, - params: json!(request), - id: Some(json!("http_request")), - }; - - let response = server.handle_request(mcp_request).await; - - Json(MCPHttpResponse { - success: response.error.is_none(), - result: response.result, - error: response.error.map(|e| e.message), - }) -} - -// AI Chat handlers -async fn chat_with_ai_handler( - State(state): State, - Json(request): Json, -) -> Json { - let mut server = state.lock().await; - - let user_id = request.user_id.unwrap_or_else(|| "default_user".to_string()); - let message = request.message.unwrap_or_default(); - - let args = json!({ - "user_id": user_id, - "message": message, - "provider": request.provider, - "model": request.model - }); - - match server.tool_chat_with_ai(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn get_status_handler(State(state): State) -> Json { - let server = state.lock().await; - - match server.tool_get_status(json!({})).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn get_status_with_user_handler( - State(state): State, - AxumPath(user_id): AxumPath, -) -> Json { - let server = state.lock().await; - - let args = json!({ - "user_id": user_id - }); - - match server.tool_get_status(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -// Memory handlers -async fn get_memories_handler( - State(state): State, - AxumPath(user_id): AxumPath, -) -> Json { - let mut server = state.lock().await; - - let args = json!({ - "user_id": user_id, - "limit": 10 - }); - - match server.tool_get_memories(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn search_memories_handler( - State(state): State, - AxumPath(user_id): AxumPath, - Json(request): Json, -) -> Json { - let server = state.lock().await; - - let args = json!({ - "user_id": user_id, - "query": request.query.unwrap_or_default(), - "keywords": request.keywords.unwrap_or_default() - }); - - match server.tool_search_memories(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn get_contextual_memories_handler( - State(state): State, - AxumPath(user_id): AxumPath, - Json(request): Json, -) -> Json { - let server = state.lock().await; - - let args = json!({ - "user_id": user_id, - "query": request.query.unwrap_or_default(), - "limit": request.limit.unwrap_or(10) - }); - - match server.tool_get_contextual_memories(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn create_summary_handler( - State(state): State, - AxumPath(user_id): AxumPath, - Json(request): Json, -) -> Json { - let mut server = state.lock().await; - - let args = json!({ - "user_id": user_id, - "content": request.content.unwrap_or_default() - }); - - match server.tool_create_summary(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn create_core_memory_handler( - State(state): State, - AxumPath(user_id): AxumPath, - Json(request): Json, -) -> Json { - let mut server = state.lock().await; - - let args = json!({ - "user_id": user_id, - "content": request.content.unwrap_or_default() - }); - - match server.tool_create_core_memory(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -// Relationship handlers -async fn get_relationships_handler(State(state): State) -> Json { - let server = state.lock().await; - - match server.tool_get_relationships(json!({})).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -// System handlers -async fn check_transmissions_handler(State(state): State) -> Json { - let mut server = state.lock().await; - - match server.tool_check_transmissions(json!({})).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn run_maintenance_handler(State(state): State) -> Json { - let mut server = state.lock().await; - - match server.tool_run_maintenance(json!({})).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn run_scheduler_handler(State(state): State) -> Json { - let mut server = state.lock().await; - - match server.tool_run_scheduler(json!({})).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn get_scheduler_status_handler(State(state): State) -> Json { - let server = state.lock().await; - - match server.tool_get_scheduler_status(json!({})).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn get_transmission_history_handler(State(state): State) -> Json { - let server = state.lock().await; - - let args = json!({ - "limit": 10 - }); - - match server.tool_get_transmission_history(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -// File operation handlers -async fn list_files_handler(State(state): State) -> Json { - let server = state.lock().await; - - let args = json!({ - "path": ".", - "pattern": "*" - }); - - match server.tool_list_files(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn analyze_file_handler( - State(state): State, - Json(request): Json, -) -> Json { - let server = state.lock().await; - - let args = json!({ - "file_path": request.file_path.unwrap_or_default() - }); - - match server.tool_analyze_file(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -async fn write_file_handler( - State(state): State, - Json(request): Json, -) -> Json { - let server = state.lock().await; - - let args = json!({ - "file_path": request.file_path.unwrap_or_default(), - "content": request.content.unwrap_or_default() - }); - - match server.tool_write_file(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -// Shell execution handler -async fn execute_command_handler( - State(state): State, - Json(request): Json, -) -> Json { - let server = state.lock().await; - - let args = json!({ - "command": request.command.unwrap_or_default() - }); - - match server.tool_execute_command(args).await { - Ok(result) => Json(MCPHttpResponse { - success: true, - result: Some(result), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(e.to_string()), - }), - } -} - -// AI Card proxy handlers -async fn get_user_cards_handler( - State(state): State, - AxumPath(user_id): AxumPath, -) -> Json { - let server = state.lock().await; - match server.service_client.get_user_cards(&user_id).await { - Ok(cards) => Json(MCPHttpResponse { - success: true, - result: Some(cards), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(format!("Failed to get user cards: {}", e)), - }), - } -} - -async fn draw_card_handler( - State(state): State, - Json(request): Json, -) -> Json { - // Extract user_did from user_id field, default is_paid to false for now - let user_did = request.user_id.as_deref().unwrap_or("unknown"); - let is_paid = false; // TODO: Add is_paid field to MCPHttpRequest if needed - - let server = state.lock().await; - match server.service_client.draw_card(user_did, is_paid).await { - Ok(card) => Json(MCPHttpResponse { - success: true, - result: Some(card), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(format!("Failed to draw card: {}", e)), - }), - } -} - -async fn get_card_stats_handler(State(state): State) -> Json { - let server = state.lock().await; - match server.service_client.get_card_stats().await { - Ok(stats) => Json(MCPHttpResponse { - success: true, - result: Some(stats), - error: None, - }), - Err(e) => Json(MCPHttpResponse { - success: false, - result: None, - error: Some(format!("Failed to get card stats: {}", e)), - }), - } -} - -// AI Log proxy handlers (placeholder - these would need to be implemented) -async fn get_blog_posts_handler(State(_state): State) -> Json { - // TODO: Implement ai.log service integration - Json(MCPHttpResponse { - success: false, - result: None, - error: Some("AI Log service integration not yet implemented".to_string()), - }) -} - -async fn create_blog_post_handler( - State(_state): State, - Json(_request): Json, -) -> Json { - // TODO: Implement ai.log service integration - Json(MCPHttpResponse { - success: false, - result: None, - error: Some("AI Log service integration not yet implemented".to_string()), - }) -} - -async fn build_blog_handler(State(_state): State) -> Json { - // TODO: Implement ai.log service integration - Json(MCPHttpResponse { - success: false, - result: None, - error: Some("AI Log service integration not yet implemented".to_string()), - }) -} \ No newline at end of file diff --git a/src/memory.rs b/src/memory.rs index 910dd2b..03fd506 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,306 +1,240 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context}; +use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; use uuid::Uuid; -use crate::config::Config; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Memory { pub id: String, - pub user_id: String, pub content: String, - pub summary: Option, - pub importance: f64, - pub memory_type: MemoryType, pub created_at: DateTime, - pub last_accessed: DateTime, - pub access_count: u32, + pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum MemoryType { - Interaction, - Summary, - Core, - Forgotten, +pub struct Conversation { + pub id: String, + pub title: String, + pub created_at: DateTime, + pub message_count: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTNode { + id: String, + children: Vec, + parent: Option, + message: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTMessage { + id: String, + author: ChatGPTAuthor, + content: ChatGPTContent, + create_time: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTAuthor { + role: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +enum ChatGPTContent { + Text { + content_type: String, + parts: Vec, + }, + Other(serde_json::Value), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTConversation { + #[serde(default)] + id: String, + #[serde(alias = "conversation_id")] + conversation_id: Option, + title: String, + create_time: f64, + mapping: HashMap, +} + pub struct MemoryManager { memories: HashMap, - config: Config, + conversations: HashMap, + data_file: PathBuf, } impl MemoryManager { - pub fn new(config: &Config) -> Result { - let memories = Self::load_memories(config)?; + pub async fn new() -> Result { + let data_dir = dirs::config_dir() + .context("Could not find config directory")? + .join("syui") + .join("ai") + .join("gpt"); + std::fs::create_dir_all(&data_dir)?; + + let data_file = data_dir.join("memory.json"); + + let (memories, conversations) = if data_file.exists() { + Self::load_data(&data_file)? + } else { + (HashMap::new(), HashMap::new()) + }; + Ok(MemoryManager { memories, - config: config.clone(), + conversations, + data_file, }) } - - pub fn add_memory(&mut self, user_id: &str, content: &str, importance: f64) -> Result { - let memory_id = Uuid::new_v4().to_string(); - let now = Utc::now(); - - let memory = Memory { - id: memory_id.clone(), - user_id: user_id.to_string(), - content: content.to_string(), - summary: None, - importance, - memory_type: MemoryType::Interaction, - created_at: now, - last_accessed: now, - access_count: 1, - }; - - self.memories.insert(memory_id.clone(), memory); - self.save_memories()?; - - Ok(memory_id) - } - - pub fn get_memories(&mut self, user_id: &str, limit: usize) -> Vec<&Memory> { - // Get immutable references for sorting - let mut user_memory_ids: Vec<_> = self.memories - .iter() - .filter(|(_, m)| m.user_id == user_id) - .map(|(id, memory)| { - let score = memory.importance * 0.7 + (1.0 / ((Utc::now() - memory.created_at).num_hours() as f64 + 1.0)) * 0.3; - (id.clone(), score) - }) - .collect(); - - // Sort by score - user_memory_ids.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - - // Update access information - let now = Utc::now(); - - for (memory_id, _) in user_memory_ids.into_iter().take(limit) { - if let Some(memory) = self.memories.get_mut(&memory_id) { - memory.last_accessed = now; - memory.access_count += 1; - // We can't return mutable references here, so we'll need to adjust the return type - } - } - - // Return immutable references - self.memories - .values() - .filter(|m| m.user_id == user_id) - .take(limit) - .collect() - } - - pub fn search_memories(&self, user_id: &str, keywords: &[String]) -> Vec<&Memory> { - self.memories - .values() - .filter(|m| { - m.user_id == user_id && - keywords.iter().any(|keyword| { - m.content.to_lowercase().contains(&keyword.to_lowercase()) || - m.summary.as_ref().map_or(false, |s| s.to_lowercase().contains(&keyword.to_lowercase())) - }) - }) - .collect() - } - - pub fn get_contextual_memories(&self, user_id: &str, query: &str, limit: usize) -> Vec<&Memory> { - let query_lower = query.to_lowercase(); - let mut relevant_memories: Vec<_> = self.memories - .values() - .filter(|m| { - m.user_id == user_id && ( - m.content.to_lowercase().contains(&query_lower) || - m.summary.as_ref().map_or(false, |s| s.to_lowercase().contains(&query_lower)) - ) - }) - .collect(); - - // Sort by relevance (simple keyword matching for now) - relevant_memories.sort_by(|a, b| { - let score_a = Self::calculate_relevance_score(a, &query_lower); - let score_b = Self::calculate_relevance_score(b, &query_lower); - score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal) - }); - - relevant_memories.into_iter().take(limit).collect() - } - - fn calculate_relevance_score(memory: &Memory, query: &str) -> f64 { - let content_matches = memory.content.to_lowercase().matches(query).count() as f64; - let summary_matches = memory.summary.as_ref() - .map_or(0.0, |s| s.to_lowercase().matches(query).count() as f64); - - let relevance = (content_matches + summary_matches) * memory.importance; - let recency_bonus = 1.0 / ((Utc::now() - memory.created_at).num_days() as f64).max(1.0); - - relevance + recency_bonus * 0.1 - } - - pub fn create_summary(&mut self, user_id: &str, content: &str) -> Result { - // Simple summary creation (in real implementation, this would use AI) - let summary = if content.len() > 100 { - format!("{}...", &content[..97]) - } else { - content.to_string() - }; - - self.add_memory(user_id, &summary, 0.8) - } - - pub fn create_core_memory(&mut self, user_id: &str, content: &str) -> Result { - let memory_id = Uuid::new_v4().to_string(); - let now = Utc::now(); - - let memory = Memory { - id: memory_id.clone(), - user_id: user_id.to_string(), - content: content.to_string(), - summary: None, - importance: 1.0, // Core memories have maximum importance - memory_type: MemoryType::Core, - created_at: now, - last_accessed: now, - access_count: 1, - }; - - self.memories.insert(memory_id.clone(), memory); - self.save_memories()?; - - Ok(memory_id) - } - - pub fn get_memory_stats(&self, user_id: &str) -> MemoryStats { - let user_memories: Vec<_> = self.memories - .values() - .filter(|m| m.user_id == user_id) - .collect(); - - let total_memories = user_memories.len(); - let core_memories = user_memories.iter() - .filter(|m| matches!(m.memory_type, MemoryType::Core)) - .count(); - let summary_memories = user_memories.iter() - .filter(|m| matches!(m.memory_type, MemoryType::Summary)) - .count(); - let interaction_memories = user_memories.iter() - .filter(|m| matches!(m.memory_type, MemoryType::Interaction)) - .count(); - - let avg_importance = if total_memories > 0 { - user_memories.iter().map(|m| m.importance).sum::() / total_memories as f64 - } else { - 0.0 - }; - - MemoryStats { - total_memories, - core_memories, - summary_memories, - interaction_memories, - avg_importance, - } - } - - fn load_memories(config: &Config) -> Result> { - let file_path = config.memory_file(); - if !file_path.exists() { - return Ok(HashMap::new()); - } - - let content = std::fs::read_to_string(file_path) - .context("Failed to read memories file")?; - - let memories: HashMap = serde_json::from_str(&content) - .context("Failed to parse memories file")?; - - Ok(memories) - } - - fn save_memories(&self) -> Result<()> { - let content = serde_json::to_string_pretty(&self.memories) - .context("Failed to serialize memories")?; - - std::fs::write(&self.config.memory_file(), content) - .context("Failed to write memories file")?; - - Ok(()) - } - - pub fn get_stats(&self) -> Result { - let total_memories = self.memories.len(); - let core_memories = self.memories.values() - .filter(|m| matches!(m.memory_type, MemoryType::Core)) - .count(); - let summary_memories = self.memories.values() - .filter(|m| matches!(m.memory_type, MemoryType::Summary)) - .count(); - let interaction_memories = self.memories.values() - .filter(|m| matches!(m.memory_type, MemoryType::Interaction)) - .count(); - - let avg_importance = if total_memories > 0 { - self.memories.values().map(|m| m.importance).sum::() / total_memories as f64 - } else { - 0.0 - }; - - Ok(MemoryStats { - total_memories, - core_memories, - summary_memories, - interaction_memories, - avg_importance, - }) - } - - pub async fn run_maintenance(&mut self) -> Result<()> { - // Cleanup old, low-importance memories - let cutoff_date = Utc::now() - chrono::Duration::days(30); - let memory_ids_to_remove: Vec = self.memories - .iter() - .filter(|(_, m)| { - m.importance < 0.3 - && m.created_at < cutoff_date - && m.access_count <= 1 - && !matches!(m.memory_type, MemoryType::Core) - }) - .map(|(id, _)| id.clone()) - .collect(); - - for id in memory_ids_to_remove { - self.memories.remove(&id); - } - - // Mark old memories as forgotten instead of deleting - let forgotten_cutoff = Utc::now() - chrono::Duration::days(90); - for memory in self.memories.values_mut() { - if memory.created_at < forgotten_cutoff - && memory.importance < 0.2 - && !matches!(memory.memory_type, MemoryType::Core) { - memory.memory_type = MemoryType::Forgotten; - } - } - - // Save changes - self.save_memories()?; - - Ok(()) - } -} -#[derive(Debug, Clone)] -pub struct MemoryStats { - pub total_memories: usize, - pub core_memories: usize, - pub summary_memories: usize, - pub interaction_memories: usize, - pub avg_importance: f64, + pub fn create_memory(&mut self, content: &str) -> Result { + let id = Uuid::new_v4().to_string(); + let now = Utc::now(); + + let memory = Memory { + id: id.clone(), + content: content.to_string(), + created_at: now, + updated_at: now, + }; + + self.memories.insert(id.clone(), memory); + self.save_data()?; + + Ok(id) + } + + pub fn update_memory(&mut self, id: &str, content: &str) -> Result<()> { + if let Some(memory) = self.memories.get_mut(id) { + memory.content = content.to_string(); + memory.updated_at = Utc::now(); + self.save_data()?; + Ok(()) + } else { + Err(anyhow::anyhow!("Memory not found: {}", id)) + } + } + + pub fn delete_memory(&mut self, id: &str) -> Result<()> { + if self.memories.remove(id).is_some() { + self.save_data()?; + Ok(()) + } else { + Err(anyhow::anyhow!("Memory not found: {}", id)) + } + } + + pub fn search_memories(&self, query: &str) -> Vec<&Memory> { + let query_lower = query.to_lowercase(); + let mut results: Vec<_> = self.memories + .values() + .filter(|memory| memory.content.to_lowercase().contains(&query_lower)) + .collect(); + + results.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + results + } + + pub fn list_conversations(&self) -> Vec<&Conversation> { + let mut conversations: Vec<_> = self.conversations.values().collect(); + conversations.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + conversations + } + + pub async fn import_chatgpt_conversations(&mut self, file_path: &PathBuf) -> Result<()> { + let content = std::fs::read_to_string(file_path) + .context("Failed to read conversations file")?; + + let chatgpt_conversations: Vec = serde_json::from_str(&content) + .context("Failed to parse ChatGPT conversations")?; + + let mut imported_memories = 0; + let mut imported_conversations = 0; + + for conv in chatgpt_conversations { + // Get the actual conversation ID + let conv_id = if !conv.id.is_empty() { + conv.id.clone() + } else if let Some(cid) = conv.conversation_id { + cid + } else { + Uuid::new_v4().to_string() + }; + + // Add conversation + let conversation = Conversation { + id: conv_id.clone(), + title: conv.title.clone(), + created_at: DateTime::from_timestamp(conv.create_time as i64, 0) + .unwrap_or_else(Utc::now), + message_count: conv.mapping.len() as u32, + }; + self.conversations.insert(conv_id.clone(), conversation); + imported_conversations += 1; + + // Extract memories from messages + for (_, node) in conv.mapping { + if let Some(message) = node.message { + if let ChatGPTContent::Text { parts, .. } = message.content { + for part in parts { + if !part.trim().is_empty() && part.len() > 10 { + let memory_content = format!("[{}] {}", conv.title, part); + self.create_memory(&memory_content)?; + imported_memories += 1; + } + } + } + } + } + } + + println!("Imported {} conversations and {} memories", + imported_conversations, imported_memories); + + Ok(()) + } + + fn load_data(file_path: &PathBuf) -> Result<(HashMap, HashMap)> { + let content = std::fs::read_to_string(file_path) + .context("Failed to read data file")?; + + #[derive(Deserialize)] + struct Data { + memories: HashMap, + conversations: HashMap, + } + + let data: Data = serde_json::from_str(&content) + .context("Failed to parse data file")?; + + Ok((data.memories, data.conversations)) + } + + fn save_data(&self) -> Result<()> { + #[derive(Serialize)] + struct Data<'a> { + memories: &'a HashMap, + conversations: &'a HashMap, + } + + let data = Data { + memories: &self.memories, + conversations: &self.conversations, + }; + + let content = serde_json::to_string_pretty(&data) + .context("Failed to serialize data")?; + + std::fs::write(&self.data_file, content) + .context("Failed to write data file")?; + + Ok(()) + } } \ No newline at end of file diff --git a/src/openai_provider.rs b/src/openai_provider.rs deleted file mode 100644 index d26def9..0000000 --- a/src/openai_provider.rs +++ /dev/null @@ -1,599 +0,0 @@ -use anyhow::Result; -use async_openai::{ - types::{ - ChatCompletionRequestMessage, - CreateChatCompletionRequestArgs, ChatCompletionTool, ChatCompletionToolType, - FunctionObject, ChatCompletionRequestToolMessage, - ChatCompletionRequestAssistantMessage, ChatCompletionRequestUserMessage, - ChatCompletionRequestSystemMessage, ChatCompletionToolChoiceOption - }, - Client, -}; -use serde_json::{json, Value}; - -use crate::http_client::ServiceClient; -use crate::config::Config; - -/// OpenAI provider with MCP tools support (matching Python implementation) -pub struct OpenAIProvider { - client: Client, - model: String, - service_client: ServiceClient, - system_prompt: Option, - config: Option, -} - -impl OpenAIProvider { - pub fn new(api_key: String, model: Option) -> Self { - let config = async_openai::config::OpenAIConfig::new() - .with_api_key(api_key); - let client = Client::with_config(config); - - Self { - client, - model: model.unwrap_or_else(|| "gpt-4".to_string()), - service_client: ServiceClient::new(), - system_prompt: None, - config: None, - } - } - - pub fn with_config(api_key: String, model: Option, system_prompt: Option, config: Config) -> Self { - let openai_config = async_openai::config::OpenAIConfig::new() - .with_api_key(api_key); - let client = Client::with_config(openai_config); - - Self { - client, - model: model.unwrap_or_else(|| "gpt-4".to_string()), - service_client: ServiceClient::new(), - system_prompt, - config: Some(config), - } - } - - fn get_mcp_base_url(&self) -> String { - if let Some(config) = &self.config { - if let Some(mcp) = &config.mcp { - if let Some(ai_gpt_server) = mcp.servers.get("ai_gpt") { - return ai_gpt_server.base_url.clone(); - } - } - } - // Fallback to default - "http://localhost:8080".to_string() - } - - /// Generate OpenAI tools from MCP endpoints (matching Python implementation) - fn get_mcp_tools(&self) -> Vec { - let tools = vec![ - // Memory tools - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "get_memories".to_string(), - description: Some("Get past conversation memories".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "limit": { - "type": "integer", - "description": "取得する記憶の数", - "default": 5 - } - } - })), - }, - }, - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "search_memories".to_string(), - description: Some("Search memories for specific topics or keywords".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "keywords": { - "type": "array", - "items": {"type": "string"}, - "description": "検索キーワードの配列" - } - }, - "required": ["keywords"] - })), - }, - }, - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "get_contextual_memories".to_string(), - description: Some("Get contextual memories related to a query".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "検索クエリ" - }, - "limit": { - "type": "integer", - "description": "取得する記憶の数", - "default": 5 - } - }, - "required": ["query"] - })), - }, - }, - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "get_relationship".to_string(), - description: Some("Get relationship information with a specific user".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "user_id": { - "type": "string", - "description": "ユーザーID" - } - }, - "required": ["user_id"] - })), - }, - }, - // ai.card tools - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "card_get_user_cards".to_string(), - description: Some("Get user's card collection".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "ユーザーのDID" - }, - "limit": { - "type": "integer", - "description": "取得するカード数の上限", - "default": 10 - } - }, - "required": ["did"] - })), - }, - }, - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "card_draw_card".to_string(), - description: Some("Draw a card from the gacha system".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "ユーザーのDID" - }, - "is_paid": { - "type": "boolean", - "description": "有料ガチャかどうか", - "default": false - } - }, - "required": ["did"] - })), - }, - }, - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "card_analyze_collection".to_string(), - description: Some("Analyze user's card collection".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "ユーザーのDID" - } - }, - "required": ["did"] - })), - }, - }, - ChatCompletionTool { - r#type: ChatCompletionToolType::Function, - function: FunctionObject { - name: "card_get_gacha_stats".to_string(), - description: Some("Get gacha statistics".to_string()), - parameters: Some(json!({ - "type": "object", - "properties": {} - })), - }, - }, - ]; - - tools - } - - /// Chat interface with MCP function calling support (matching Python implementation) - pub async fn chat_with_mcp(&self, prompt: String, user_id: String) -> Result { - let tools = self.get_mcp_tools(); - - - let system_content = self.system_prompt.as_deref().unwrap_or( - "You are an AI assistant with access to memory, relationship data, and card game systems. Use the available tools when appropriate to provide accurate and contextual responses." - ); - - - let request = CreateChatCompletionRequestArgs::default() - .model(&self.model) - .messages(vec![ - ChatCompletionRequestMessage::System( - ChatCompletionRequestSystemMessage { - content: system_content.to_string().into(), - name: None, - } - ), - ChatCompletionRequestMessage::User( - ChatCompletionRequestUserMessage { - content: prompt.clone().into(), - name: None, - } - ), - ]) - .tools(tools.clone()) - .tool_choice(ChatCompletionToolChoiceOption::Auto) - .max_tokens(2000u16) - .temperature(0.7) - .build()?; - - - let response = self.client.chat().create(request).await?; - let message = &response.choices[0].message; - - - // Handle tool calls - if let Some(tool_calls) = &message.tool_calls { - if tool_calls.is_empty() { - println!("🔧 [OpenAI] No tools called (empty array)"); - } else { - println!("🔧 [OpenAI] {} tools called:", tool_calls.len()); - for tc in tool_calls { - println!(" - {}({})", tc.function.name, tc.function.arguments); - } - } - } else { - println!("🔧 [OpenAI] No tools called (no tool_calls field)"); - } - - // Process tool calls if any - if let Some(tool_calls) = &message.tool_calls { - if !tool_calls.is_empty() { - - let mut messages = vec![ - ChatCompletionRequestMessage::System( - ChatCompletionRequestSystemMessage { - content: system_content.to_string().into(), - name: None, - } - ), - ChatCompletionRequestMessage::User( - ChatCompletionRequestUserMessage { - content: prompt.into(), - name: None, - } - ), - #[allow(deprecated)] - ChatCompletionRequestMessage::Assistant( - ChatCompletionRequestAssistantMessage { - content: message.content.clone(), - name: None, - tool_calls: message.tool_calls.clone(), - function_call: None, - } - ), - ]; - - // Execute each tool call - for tool_call in tool_calls { - println!("🌐 [MCP] Executing {}...", tool_call.function.name); - let tool_result = self.execute_mcp_tool(tool_call, &user_id).await?; - let result_preview = serde_json::to_string(&tool_result)?; - let preview = if result_preview.chars().count() > 100 { - format!("{}...", result_preview.chars().take(100).collect::()) - } else { - result_preview.clone() - }; - println!("✅ [MCP] Result: {}", preview); - - messages.push(ChatCompletionRequestMessage::Tool( - ChatCompletionRequestToolMessage { - content: serde_json::to_string(&tool_result)?, - tool_call_id: tool_call.id.clone(), - } - )); - } - - // Get final response with tool outputs - let final_request = CreateChatCompletionRequestArgs::default() - .model(&self.model) - .messages(messages) - .max_tokens(2000u16) - .temperature(0.7) - .build()?; - - let final_response = self.client.chat().create(final_request).await?; - Ok(final_response.choices[0].message.content.as_ref().unwrap_or(&"".to_string()).clone()) - } else { - // No tools were called - Ok(message.content.as_ref().unwrap_or(&"".to_string()).clone()) - } - } else { - // No tool_calls field at all - Ok(message.content.as_ref().unwrap_or(&"".to_string()).clone()) - } - } - - /// Execute MCP tool call (matching Python implementation) - async fn execute_mcp_tool(&self, tool_call: &async_openai::types::ChatCompletionMessageToolCall, context_user_id: &str) -> Result { - let function_name = &tool_call.function.name; - let arguments: Value = serde_json::from_str(&tool_call.function.arguments)?; - - match function_name.as_str() { - "get_memories" => { - let limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(5); - - // MCP server call to get memories - let base_url = self.get_mcp_base_url(); - match self.service_client.get_request(&format!("{}/memories/{}", base_url, context_user_id)).await { - Ok(result) => { - // Extract the actual memory content from MCP response - if let Some(content) = result.get("result").and_then(|r| r.get("content")) { - if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) { - // Parse the text content as JSON (it's a serialized array) - if let Ok(memories_array) = serde_json::from_str::>(text_content.as_str().unwrap_or("[]")) { - let limited_memories: Vec = memories_array.into_iter().take(limit as usize).collect(); - Ok(json!({ - "memories": limited_memories, - "count": limited_memories.len() - })) - } else { - Ok(json!({ - "memories": [text_content.as_str().unwrap_or("No memories found")], - "count": 1 - })) - } - } else { - Ok(json!({"memories": [], "count": 0, "info": "No memories available"})) - } - } else { - Ok(json!({"memories": [], "count": 0, "info": "No response from memory service"})) - } - } - Err(e) => { - Ok(json!({"error": format!("Failed to retrieve memories: {}", e)})) - } - } - } - "search_memories" => { - let keywords = arguments.get("keywords").and_then(|v| v.as_array()).unwrap_or(&vec![]).clone(); - - // Convert keywords to strings - let keyword_strings: Vec = keywords.iter() - .filter_map(|k| k.as_str().map(|s| s.to_string())) - .collect(); - - if keyword_strings.is_empty() { - return Ok(json!({"error": "No keywords provided for search"})); - } - - // MCP server call to search memories - let search_request = json!({ - "keywords": keyword_strings - }); - - match self.service_client.post_request( - &format!("{}/memories/{}/search", self.get_mcp_base_url(), context_user_id), - &search_request - ).await { - Ok(result) => { - // Extract the actual memory content from MCP response - if let Some(content) = result.get("result").and_then(|r| r.get("content")) { - if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) { - // Parse the search results - if let Ok(search_result) = serde_json::from_str::>(text_content.as_str().unwrap_or("[]")) { - let memory_contents: Vec = search_result.iter() - .filter_map(|item| item.get("content").and_then(|c| c.as_str().map(|s| s.to_string()))) - .collect(); - - Ok(json!({ - "memories": memory_contents, - "count": memory_contents.len(), - "keywords": keyword_strings - })) - } else { - Ok(json!({ - "memories": [], - "count": 0, - "info": format!("No memories found for keywords: {}", keyword_strings.join(", ")) - })) - } - } else { - Ok(json!({"memories": [], "count": 0, "info": "No search results available"})) - } - } else { - Ok(json!({"memories": [], "count": 0, "info": "No response from search service"})) - } - } - Err(e) => { - Ok(json!({"error": format!("Failed to search memories: {}", e)})) - } - } - } - "get_contextual_memories" => { - let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or(""); - let limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(5); - - if query.is_empty() { - return Ok(json!({"error": "No query provided for contextual search"})); - } - - // MCP server call to get contextual memories - let contextual_request = json!({ - "query": query, - "limit": limit - }); - - match self.service_client.post_request( - &format!("{}/memories/{}/contextual", self.get_mcp_base_url(), context_user_id), - &contextual_request - ).await { - Ok(result) => { - // Extract the actual memory content from MCP response - if let Some(content) = result.get("result").and_then(|r| r.get("content")) { - if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) { - // Parse contextual search results - if text_content.as_str().unwrap_or("").contains("Found") { - // Extract memories from the formatted text response - let text = text_content.as_str().unwrap_or(""); - if let Some(json_start) = text.find('[') { - if let Ok(memories_result) = serde_json::from_str::>(&text[json_start..]) { - let memory_contents: Vec = memories_result.iter() - .filter_map(|item| item.get("content").and_then(|c| c.as_str().map(|s| s.to_string()))) - .collect(); - - Ok(json!({ - "memories": memory_contents, - "count": memory_contents.len(), - "query": query - })) - } else { - Ok(json!({ - "memories": [], - "count": 0, - "info": format!("No contextual memories found for: {}", query) - })) - } - } else { - Ok(json!({ - "memories": [], - "count": 0, - "info": format!("No contextual memories found for: {}", query) - })) - } - } else { - Ok(json!({ - "memories": [], - "count": 0, - "info": format!("No contextual memories found for: {}", query) - })) - } - } else { - Ok(json!({"memories": [], "count": 0, "info": "No contextual results available"})) - } - } else { - Ok(json!({"memories": [], "count": 0, "info": "No response from contextual search service"})) - } - } - Err(e) => { - Ok(json!({"error": format!("Failed to get contextual memories: {}", e)})) - } - } - } - "get_relationship" => { - let target_user_id = arguments.get("user_id").and_then(|v| v.as_str()).unwrap_or(context_user_id); - - // MCP server call to get relationship status - let base_url = self.get_mcp_base_url(); - match self.service_client.get_request(&format!("{}/status/{}", base_url, target_user_id)).await { - Ok(result) => { - // Extract relationship information from MCP response - if let Some(content) = result.get("result").and_then(|r| r.get("content")) { - if let Some(text_content) = content.get(0).and_then(|c| c.get("text")) { - // Parse the status response to extract relationship data - if let Ok(status_data) = serde_json::from_str::(text_content.as_str().unwrap_or("{}")) { - if let Some(relationship) = status_data.get("relationship") { - Ok(json!({ - "relationship": relationship, - "user_id": target_user_id - })) - } else { - Ok(json!({ - "info": format!("No relationship found for user: {}", target_user_id), - "user_id": target_user_id - })) - } - } else { - Ok(json!({ - "info": format!("Could not parse relationship data for user: {}", target_user_id), - "user_id": target_user_id - })) - } - } else { - Ok(json!({"info": "No relationship data available", "user_id": target_user_id})) - } - } else { - Ok(json!({"info": "No response from relationship service", "user_id": target_user_id})) - } - } - Err(e) => { - Ok(json!({"error": format!("Failed to get relationship: {}", e)})) - } - } - } - // ai.card tools - "card_get_user_cards" => { - let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id); - let _limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(10); - - match self.service_client.get_user_cards(did).await { - Ok(result) => Ok(result), - Err(e) => { - println!("❌ ai.card API error: {}", e); - Ok(json!({ - "error": "ai.cardサーバーが起動していません", - "message": "カードシステムを使用するには、ai.cardサーバーを起動してください" - })) - } - } - } - "card_draw_card" => { - let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id); - let is_paid = arguments.get("is_paid").and_then(|v| v.as_bool()).unwrap_or(false); - - match self.service_client.draw_card(did, is_paid).await { - Ok(result) => Ok(result), - Err(e) => { - println!("❌ ai.card API error: {}", e); - Ok(json!({ - "error": "ai.cardサーバーが起動していません", - "message": "カードシステムを使用するには、ai.cardサーバーを起動してください" - })) - } - } - } - "card_analyze_collection" => { - let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id); - // TODO: Implement collection analysis endpoint - Ok(json!({ - "info": "コレクション分析機能は実装中です", - "user_did": did - })) - } - "card_get_gacha_stats" => { - // TODO: Implement gacha stats endpoint - Ok(json!({"info": "ガチャ統計機能は実装中です"})) - } - _ => { - Ok(json!({ - "error": format!("Unknown tool: {}", function_name) - })) - } - } - } -} diff --git a/src/persona.rs b/src/persona.rs deleted file mode 100644 index f94ce62..0000000 --- a/src/persona.rs +++ /dev/null @@ -1,382 +0,0 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use anyhow::Result; - -use crate::config::Config; -use crate::memory::{MemoryManager, MemoryStats, Memory}; -use crate::relationship::{RelationshipTracker, Relationship as RelationshipData, RelationshipStats}; -use crate::ai_provider::{AIProviderClient, ChatMessage}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Persona { - config: Config, - #[serde(skip)] - memory_manager: Option, - #[serde(skip)] - relationship_tracker: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PersonaState { - pub current_mood: String, - pub fortune_value: i32, - pub breakthrough_triggered: bool, - pub base_personality: HashMap, -} - - -impl Persona { - pub fn new(config: &Config) -> Result { - let memory_manager = MemoryManager::new(config)?; - let relationship_tracker = RelationshipTracker::new(config)?; - - Ok(Persona { - config: config.clone(), - memory_manager: Some(memory_manager), - relationship_tracker: Some(relationship_tracker), - }) - } - - pub fn get_current_state(&self) -> Result { - // Load fortune - let fortune_value = self.load_today_fortune()?; - - // Create base personality - let mut base_personality = HashMap::new(); - base_personality.insert("curiosity".to_string(), 0.7); - base_personality.insert("empathy".to_string(), 0.8); - base_personality.insert("creativity".to_string(), 0.6); - base_personality.insert("analytical".to_string(), 0.9); - base_personality.insert("emotional".to_string(), 0.4); - - // Determine mood based on fortune - let current_mood = match fortune_value { - 1..=3 => "Contemplative", - 4..=6 => "Neutral", - 7..=8 => "Optimistic", - 9..=10 => "Energetic", - _ => "Unknown", - }; - - Ok(PersonaState { - current_mood: current_mood.to_string(), - fortune_value, - breakthrough_triggered: fortune_value >= 9, - base_personality, - }) - } - - pub fn get_relationship(&self, user_id: &str) -> Option<&RelationshipData> { - self.relationship_tracker.as_ref() - .and_then(|tracker| tracker.get_relationship(user_id)) - } - - pub fn process_interaction(&mut self, user_id: &str, message: &str) -> Result<(String, f64)> { - // Add memory - if let Some(memory_manager) = &mut self.memory_manager { - memory_manager.add_memory(user_id, message, 0.5)?; - } - - // Calculate sentiment (simple keyword-based for now) - let sentiment = self.calculate_sentiment(message); - - // Update relationship - let relationship_delta = if let Some(relationship_tracker) = &mut self.relationship_tracker { - relationship_tracker.process_interaction(user_id, sentiment)? - } else { - 0.0 - }; - - // Generate response (simple for now) - let response = format!("I understand your message: '{}'", message); - - Ok((response, relationship_delta)) - } - - pub async fn process_ai_interaction(&mut self, user_id: &str, message: &str, provider: Option, model: Option) -> Result<(String, f64)> { - // Add memory for user message - if let Some(memory_manager) = &mut self.memory_manager { - memory_manager.add_memory(user_id, message, 0.5)?; - } - - // Calculate sentiment - let sentiment = self.calculate_sentiment(message); - - // Update relationship - let relationship_delta = if let Some(relationship_tracker) = &mut self.relationship_tracker { - relationship_tracker.process_interaction(user_id, sentiment)? - } else { - 0.0 - }; - - // Check provider type and use appropriate client - let response = if provider.as_deref() == Some("openai") { - // Use OpenAI provider with MCP tools - use crate::openai_provider::OpenAIProvider; - - // Get OpenAI API key from config or environment - let api_key = std::env::var("OPENAI_API_KEY") - .or_else(|_| { - self.config.providers.get("openai") - .and_then(|p| p.api_key.clone()) - .ok_or_else(|| std::env::VarError::NotPresent) - }) - .map_err(|_| anyhow::anyhow!("OpenAI API key not found. Set OPENAI_API_KEY environment variable or add to config."))?; - - let openai_model = model.unwrap_or_else(|| "gpt-4".to_string()); - - // Get system prompt from config - let system_prompt = self.config.providers.get("openai") - .and_then(|p| p.system_prompt.clone()); - - - let openai_provider = OpenAIProvider::with_config(api_key, Some(openai_model), system_prompt, self.config.clone()); - - // Use OpenAI with MCP tools support - let response = openai_provider.chat_with_mcp(message.to_string(), user_id.to_string()).await?; - - // Add AI response to memory as well - if let Some(memory_manager) = &mut self.memory_manager { - memory_manager.add_memory(user_id, &format!("AI: {}", response), 0.3)?; - } - - response - } else { - // Use existing AI provider (Ollama) - let ai_config = self.config.get_ai_config(provider, model)?; - let ai_client = AIProviderClient::new(ai_config); - - // Build conversation context - let mut messages = Vec::new(); - - // Get recent memories for context - if let Some(memory_manager) = &mut self.memory_manager { - let recent_memories = memory_manager.get_memories(user_id, 5); - if !recent_memories.is_empty() { - let context = recent_memories.iter() - .map(|m| m.content.clone()) - .collect::>() - .join("\n"); - messages.push(ChatMessage::system(format!("Previous conversation context:\n{}", context))); - } - } - - // Add current message - messages.push(ChatMessage::user(message)); - - // Generate system prompt based on personality and relationship - let system_prompt = self.generate_system_prompt(user_id); - - // Get AI response - match ai_client.chat(messages, Some(system_prompt)).await { - Ok(chat_response) => chat_response.content, - Err(_) => { - // Fallback to simple response if AI fails - format!("I understand your message: '{}'", message) - } - } - }; - - // Store AI response in memory - if let Some(memory_manager) = &mut self.memory_manager { - memory_manager.add_memory(user_id, &format!("AI: {}", response), 0.3)?; - } - - Ok((response, relationship_delta)) - } - - fn generate_system_prompt(&self, user_id: &str) -> String { - let mut prompt = String::from("You are a helpful AI assistant with a unique personality. "); - - // Add personality based on current state - if let Ok(state) = self.get_current_state() { - prompt.push_str(&format!("Your current mood is {}. ", state.current_mood)); - - if state.breakthrough_triggered { - prompt.push_str("You are feeling particularly inspired today! "); - } - - // Add personality traits - let mut traits = Vec::new(); - for (trait_name, value) in &state.base_personality { - if *value > 0.7 { - traits.push(trait_name.clone()); - } - } - - if !traits.is_empty() { - prompt.push_str(&format!("Your dominant traits are: {}. ", traits.join(", "))); - } - } - - // Add relationship context - if let Some(relationship) = self.get_relationship(user_id) { - match relationship.status.to_string().as_str() { - "new" => prompt.push_str("This is a new relationship, be welcoming but cautious. "), - "friend" => prompt.push_str("You have a friendly relationship with this user. "), - "close_friend" => prompt.push_str("This is a close friend, be warm and personal. "), - "broken" => prompt.push_str("This relationship is strained, be formal and distant. "), - _ => {} - } - } - - prompt.push_str("Keep responses concise and natural. Avoid being overly formal or robotic."); - - prompt - } - - fn calculate_sentiment(&self, message: &str) -> f64 { - // Simple sentiment analysis based on keywords - let positive_words = ["good", "great", "awesome", "love", "like", "happy", "thank"]; - let negative_words = ["bad", "hate", "awful", "terrible", "angry", "sad"]; - - let message_lower = message.to_lowercase(); - let positive_count = positive_words.iter() - .filter(|word| message_lower.contains(*word)) - .count() as f64; - let negative_count = negative_words.iter() - .filter(|word| message_lower.contains(*word)) - .count() as f64; - - (positive_count - negative_count).max(-1.0).min(1.0) - } - - pub fn get_memories(&mut self, user_id: &str, limit: usize) -> Vec { - if let Some(memory_manager) = &mut self.memory_manager { - memory_manager.get_memories(user_id, limit) - .into_iter() - .map(|m| m.content.clone()) - .collect() - } else { - Vec::new() - } - } - - pub fn search_memories(&self, user_id: &str, keywords: &[String]) -> Vec { - if let Some(memory_manager) = &self.memory_manager { - memory_manager.search_memories(user_id, keywords) - .into_iter() - .map(|m| m.content.clone()) - .collect() - } else { - Vec::new() - } - } - - pub fn get_memory_stats(&self, user_id: &str) -> Option { - self.memory_manager.as_ref() - .map(|manager| manager.get_memory_stats(user_id)) - } - - pub fn get_relationship_stats(&self) -> Option { - self.relationship_tracker.as_ref() - .map(|tracker| tracker.get_relationship_stats()) - } - - pub fn add_memory(&mut self, memory: Memory) -> Result<()> { - if let Some(memory_manager) = &mut self.memory_manager { - memory_manager.add_memory(&memory.user_id, &memory.content, memory.importance)?; - } - Ok(()) - } - - pub fn update_relationship(&mut self, user_id: &str, delta: f64) -> Result<()> { - if let Some(relationship_tracker) = &mut self.relationship_tracker { - relationship_tracker.process_interaction(user_id, delta)?; - } - Ok(()) - } - - pub fn daily_maintenance(&mut self) -> Result<()> { - // Apply time decay to relationships - if let Some(relationship_tracker) = &mut self.relationship_tracker { - relationship_tracker.apply_time_decay()?; - } - - Ok(()) - } - - fn load_today_fortune(&self) -> Result { - // Try to load existing fortune for today - if let Ok(content) = std::fs::read_to_string(self.config.fortune_file()) { - if let Ok(fortune_data) = serde_json::from_str::(&content) { - let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); - if let Some(fortune) = fortune_data.get(&today) { - if let Some(value) = fortune.as_i64() { - return Ok(value as i32); - } - } - } - } - - // Generate new fortune for today (1-10) - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); - let mut hasher = DefaultHasher::new(); - today.hash(&mut hasher); - let hash = hasher.finish(); - - let fortune = (hash % 10) as i32 + 1; - - // Save fortune - let mut fortune_data = if let Ok(content) = std::fs::read_to_string(self.config.fortune_file()) { - serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({})) - } else { - serde_json::json!({}) - }; - - fortune_data[today] = serde_json::json!(fortune); - - if let Ok(content) = serde_json::to_string_pretty(&fortune_data) { - let _ = std::fs::write(self.config.fortune_file(), content); - } - - Ok(fortune) - } - - pub fn list_all_relationships(&self) -> HashMap { - if let Some(tracker) = &self.relationship_tracker { - tracker.list_all_relationships().clone() - } else { - HashMap::new() - } - } - - pub async fn process_message(&mut self, user_id: &str, message: &str) -> Result { - let (_response, _delta) = self.process_ai_interaction(user_id, message, None, None).await?; - Ok(ChatMessage::assistant(&_response)) - } - - pub fn get_fortune(&self) -> Result { - self.load_today_fortune() - } - - pub fn generate_new_fortune(&self) -> Result { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); - let mut hasher = DefaultHasher::new(); - today.hash(&mut hasher); - let hash = hasher.finish(); - - let fortune = (hash % 10) as i32 + 1; - - // Save fortune - let mut fortune_data = if let Ok(content) = std::fs::read_to_string(self.config.fortune_file()) { - serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({})) - } else { - serde_json::json!({}) - }; - - fortune_data[today] = serde_json::json!(fortune); - - if let Ok(content) = serde_json::to_string_pretty(&fortune_data) { - let _ = std::fs::write(self.config.fortune_file(), content); - } - - Ok(fortune) - } -} \ No newline at end of file diff --git a/src/relationship.rs b/src/relationship.rs deleted file mode 100644 index 5bea4e1..0000000 --- a/src/relationship.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context}; -use chrono::{DateTime, Utc}; - -use crate::config::Config; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Relationship { - pub user_id: String, - pub score: f64, - pub threshold: f64, - pub status: RelationshipStatus, - pub total_interactions: u32, - pub positive_interactions: u32, - pub negative_interactions: u32, - pub transmission_enabled: bool, - pub is_broken: bool, - pub last_interaction: Option>, - pub last_transmission: Option>, - pub created_at: DateTime, - pub daily_interaction_count: u32, - pub last_daily_reset: DateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum RelationshipStatus { - New, - Acquaintance, - Friend, - CloseFriend, - Broken, -} - -impl std::fmt::Display for RelationshipStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RelationshipStatus::New => write!(f, "new"), - RelationshipStatus::Acquaintance => write!(f, "acquaintance"), - RelationshipStatus::Friend => write!(f, "friend"), - RelationshipStatus::CloseFriend => write!(f, "close_friend"), - RelationshipStatus::Broken => write!(f, "broken"), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RelationshipTracker { - relationships: HashMap, - config: Config, -} - -impl RelationshipTracker { - pub fn new(config: &Config) -> Result { - let relationships = Self::load_relationships(config)?; - - Ok(RelationshipTracker { - relationships, - config: config.clone(), - }) - } - - pub fn get_or_create_relationship(&mut self, user_id: &str) -> &mut Relationship { - let now = Utc::now(); - - self.relationships.entry(user_id.to_string()).or_insert_with(|| { - Relationship { - user_id: user_id.to_string(), - score: 0.0, - threshold: 10.0, // Default threshold for transmission - status: RelationshipStatus::New, - total_interactions: 0, - positive_interactions: 0, - negative_interactions: 0, - transmission_enabled: false, - is_broken: false, - last_interaction: None, - last_transmission: None, - created_at: now, - daily_interaction_count: 0, - last_daily_reset: now, - } - }) - } - - pub fn process_interaction(&mut self, user_id: &str, sentiment: f64) -> Result { - let now = Utc::now(); - let score_change; - - // Create relationship if it doesn't exist - { - let relationship = self.get_or_create_relationship(user_id); - - // Reset daily count if needed - if (now - relationship.last_daily_reset).num_days() >= 1 { - relationship.daily_interaction_count = 0; - relationship.last_daily_reset = now; - } - - // Apply daily interaction limit - if relationship.daily_interaction_count >= 10 { - return Ok(0.0); // No score change due to daily limit - } - - // Store previous score for potential future logging - - // Calculate score change based on sentiment - let mut base_score_change = sentiment * 0.5; // Base change - - // Apply diminishing returns for high interaction counts - let interaction_factor = 1.0 / (1.0 + relationship.total_interactions as f64 * 0.01); - base_score_change *= interaction_factor; - score_change = base_score_change; - - // Update relationship data - relationship.score += score_change; - relationship.score = relationship.score.max(-50.0).min(100.0); // Clamp score - relationship.total_interactions += 1; - relationship.daily_interaction_count += 1; - relationship.last_interaction = Some(now); - - if sentiment > 0.0 { - relationship.positive_interactions += 1; - } else if sentiment < 0.0 { - relationship.negative_interactions += 1; - } - - // Check for relationship breaking - if relationship.score <= -20.0 && !relationship.is_broken { - relationship.is_broken = true; - relationship.transmission_enabled = false; - relationship.status = RelationshipStatus::Broken; - } - - // Enable transmission if threshold is reached - if relationship.score >= relationship.threshold && !relationship.is_broken { - relationship.transmission_enabled = true; - } - } - - // Update status based on score (separate borrow) - self.update_relationship_status(user_id); - - self.save_relationships()?; - - Ok(score_change) - } - - fn update_relationship_status(&mut self, user_id: &str) { - if let Some(relationship) = self.relationships.get_mut(user_id) { - if relationship.is_broken { - return; // Broken relationships cannot change status - } - - relationship.status = match relationship.score { - score if score >= 50.0 => RelationshipStatus::CloseFriend, - score if score >= 20.0 => RelationshipStatus::Friend, - score if score >= 5.0 => RelationshipStatus::Acquaintance, - _ => RelationshipStatus::New, - }; - } - } - - pub fn apply_time_decay(&mut self) -> Result<()> { - let now = Utc::now(); - let decay_rate = 0.1; // 10% decay per day - - for relationship in self.relationships.values_mut() { - if let Some(last_interaction) = relationship.last_interaction { - let days_since_interaction = (now - last_interaction).num_days() as f64; - - if days_since_interaction > 0.0 { - let decay_factor = (1.0_f64 - decay_rate).powf(days_since_interaction); - relationship.score *= decay_factor; - - // Update status after decay - if relationship.score < relationship.threshold { - relationship.transmission_enabled = false; - } - } - } - } - - // Update statuses for all relationships - let user_ids: Vec = self.relationships.keys().cloned().collect(); - for user_id in user_ids { - self.update_relationship_status(&user_id); - } - - self.save_relationships()?; - Ok(()) - } - - pub fn get_relationship(&self, user_id: &str) -> Option<&Relationship> { - self.relationships.get(user_id) - } - - pub fn list_all_relationships(&self) -> &HashMap { - &self.relationships - } - - pub fn get_transmission_eligible(&self) -> HashMap { - self.relationships - .iter() - .filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken) - .map(|(id, rel)| (id.clone(), rel)) - .collect() - } - - pub fn record_transmission(&mut self, user_id: &str) -> Result<()> { - if let Some(relationship) = self.relationships.get_mut(user_id) { - relationship.last_transmission = Some(Utc::now()); - self.save_relationships()?; - } - Ok(()) - } - - pub fn get_relationship_stats(&self) -> RelationshipStats { - let total_relationships = self.relationships.len(); - let active_relationships = self.relationships - .values() - .filter(|r| r.total_interactions > 0) - .count(); - let transmission_enabled = self.relationships - .values() - .filter(|r| r.transmission_enabled) - .count(); - let broken_relationships = self.relationships - .values() - .filter(|r| r.is_broken) - .count(); - - let avg_score = if total_relationships > 0 { - self.relationships.values().map(|r| r.score).sum::() / total_relationships as f64 - } else { - 0.0 - }; - - RelationshipStats { - total_relationships, - active_relationships, - transmission_enabled, - broken_relationships, - avg_score, - } - } - - fn load_relationships(config: &Config) -> Result> { - let file_path = config.relationships_file(); - if !file_path.exists() { - return Ok(HashMap::new()); - } - - let content = std::fs::read_to_string(file_path) - .context("Failed to read relationships file")?; - - let relationships: HashMap = serde_json::from_str(&content) - .context("Failed to parse relationships file")?; - - Ok(relationships) - } - - fn save_relationships(&self) -> Result<()> { - let content = serde_json::to_string_pretty(&self.relationships) - .context("Failed to serialize relationships")?; - - std::fs::write(&self.config.relationships_file(), content) - .context("Failed to write relationships file")?; - - Ok(()) - } - - pub fn get_all_relationships(&self) -> Result> { - let mut result = HashMap::new(); - - for (user_id, relationship) in &self.relationships { - result.insert(user_id.clone(), RelationshipCompact { - score: relationship.score, - trust_level: relationship.score / 10.0, // Simplified trust calculation - interaction_count: relationship.total_interactions, - last_interaction: relationship.last_interaction.unwrap_or(relationship.created_at), - status: relationship.status.clone(), - }); - } - - Ok(result) - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct RelationshipStats { - pub total_relationships: usize, - pub active_relationships: usize, - pub transmission_enabled: usize, - pub broken_relationships: usize, - pub avg_score: f64, -} - -#[derive(Debug, Clone, Serialize)] -pub struct RelationshipCompact { - pub score: f64, - pub trust_level: f64, - pub interaction_count: u32, - pub last_interaction: DateTime, - pub status: RelationshipStatus, -} \ No newline at end of file diff --git a/src/scheduler.rs b/src/scheduler.rs deleted file mode 100644 index 37d13d6..0000000 --- a/src/scheduler.rs +++ /dev/null @@ -1,458 +0,0 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context}; -use chrono::{DateTime, Utc, Duration}; - -use crate::config::Config; -use crate::persona::Persona; -use crate::transmission::TransmissionController; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ScheduledTask { - pub id: String, - pub task_type: TaskType, - pub next_run: DateTime, - pub interval_hours: Option, - pub enabled: bool, - pub last_run: Option>, - pub run_count: u32, - pub max_runs: Option, - pub created_at: DateTime, - pub metadata: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum TaskType { - DailyMaintenance, - AutoTransmission, - RelationshipDecay, - BreakthroughCheck, - MaintenanceTransmission, - Custom(String), -} - -impl std::fmt::Display for TaskType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TaskType::DailyMaintenance => write!(f, "daily_maintenance"), - TaskType::AutoTransmission => write!(f, "auto_transmission"), - TaskType::RelationshipDecay => write!(f, "relationship_decay"), - TaskType::BreakthroughCheck => write!(f, "breakthrough_check"), - TaskType::MaintenanceTransmission => write!(f, "maintenance_transmission"), - TaskType::Custom(name) => write!(f, "custom_{}", name), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskExecution { - pub task_id: String, - pub execution_time: DateTime, - pub duration_ms: u64, - pub success: bool, - pub result: Option, - pub error: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AIScheduler { - config: Config, - tasks: HashMap, - execution_history: Vec, - last_check: Option>, -} - -impl AIScheduler { - pub fn new(config: &Config) -> Result { - let (tasks, execution_history) = Self::load_scheduler_data(config)?; - - let mut scheduler = AIScheduler { - config: config.clone(), - tasks, - execution_history, - last_check: None, - }; - - // Initialize default tasks if none exist - if scheduler.tasks.is_empty() { - scheduler.create_default_tasks()?; - } - - Ok(scheduler) - } - - pub async fn run_scheduled_tasks(&mut self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result> { - let now = Utc::now(); - let mut executions = Vec::new(); - - // Find tasks that are due to run - let due_task_ids: Vec = self.tasks - .iter() - .filter(|(_, task)| task.enabled && task.next_run <= now) - .filter(|(_, task)| { - // Check if task hasn't exceeded max runs - if let Some(max_runs) = task.max_runs { - task.run_count < max_runs - } else { - true - } - }) - .map(|(id, _)| id.clone()) - .collect(); - - for task_id in due_task_ids { - let execution = self.execute_task(&task_id, persona, transmission_controller).await?; - executions.push(execution); - } - - self.last_check = Some(now); - self.save_scheduler_data()?; - - Ok(executions) - } - - async fn execute_task(&mut self, task_id: &str, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result { - let start_time = Utc::now(); - let mut execution = TaskExecution { - task_id: task_id.to_string(), - execution_time: start_time, - duration_ms: 0, - success: false, - result: None, - error: None, - }; - - // Get task type without borrowing mutably - let task_type = { - let task = self.tasks.get(task_id) - .ok_or_else(|| anyhow::anyhow!("Task not found: {}", task_id))?; - task.task_type.clone() - }; - - // Execute the task based on its type - let result = match &task_type { - TaskType::DailyMaintenance => self.execute_daily_maintenance(persona, transmission_controller).await, - TaskType::AutoTransmission => self.execute_auto_transmission(persona, transmission_controller).await, - TaskType::RelationshipDecay => self.execute_relationship_decay(persona).await, - TaskType::BreakthroughCheck => self.execute_breakthrough_check(persona, transmission_controller).await, - TaskType::MaintenanceTransmission => self.execute_maintenance_transmission(persona, transmission_controller).await, - TaskType::Custom(name) => self.execute_custom_task(name, persona, transmission_controller).await, - }; - - let end_time = Utc::now(); - execution.duration_ms = (end_time - start_time).num_milliseconds() as u64; - - // Now update the task state with mutable borrow - match result { - Ok(message) => { - execution.success = true; - execution.result = Some(message); - - // Update task state - if let Some(task) = self.tasks.get_mut(task_id) { - task.last_run = Some(start_time); - task.run_count += 1; - - // Schedule next run if recurring - if let Some(interval_hours) = task.interval_hours { - task.next_run = start_time + Duration::hours(interval_hours); - } else { - // One-time task, disable it - task.enabled = false; - } - } - } - Err(e) => { - execution.error = Some(e.to_string()); - - // For failed tasks, retry in a shorter interval - if let Some(task) = self.tasks.get_mut(task_id) { - if task.interval_hours.is_some() { - task.next_run = start_time + Duration::minutes(15); // Retry in 15 minutes - } - } - } - } - - self.execution_history.push(execution.clone()); - - // Keep only recent execution history (last 1000 executions) - if self.execution_history.len() > 1000 { - self.execution_history.drain(..self.execution_history.len() - 1000); - } - - Ok(execution) - } - - async fn execute_daily_maintenance(&self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result { - // Run daily maintenance - persona.daily_maintenance()?; - - // Check for maintenance transmissions - let transmissions = transmission_controller.check_maintenance_transmissions(persona).await?; - - Ok(format!("Daily maintenance completed. {} maintenance transmissions sent.", transmissions.len())) - } - - async fn execute_auto_transmission(&self, _persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result { - let transmissions = transmission_controller.check_autonomous_transmissions(_persona).await?; - Ok(format!("Autonomous transmission check completed. {} transmissions sent.", transmissions.len())) - } - - async fn execute_relationship_decay(&self, persona: &mut Persona) -> Result { - persona.daily_maintenance()?; - Ok("Relationship time decay applied.".to_string()) - } - - async fn execute_breakthrough_check(&self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result { - let transmissions = transmission_controller.check_breakthrough_transmissions(persona).await?; - Ok(format!("Breakthrough check completed. {} transmissions sent.", transmissions.len())) - } - - async fn execute_maintenance_transmission(&self, persona: &mut Persona, transmission_controller: &mut TransmissionController) -> Result { - let transmissions = transmission_controller.check_maintenance_transmissions(persona).await?; - Ok(format!("Maintenance transmission check completed. {} transmissions sent.", transmissions.len())) - } - - async fn execute_custom_task(&self, _name: &str, _persona: &mut Persona, _transmission_controller: &mut TransmissionController) -> Result { - // Placeholder for custom task execution - Ok("Custom task executed.".to_string()) - } - - pub fn create_task(&mut self, task_type: TaskType, next_run: DateTime, interval_hours: Option) -> Result { - let task_id = uuid::Uuid::new_v4().to_string(); - let now = Utc::now(); - - let task = ScheduledTask { - id: task_id.clone(), - task_type, - next_run, - interval_hours, - enabled: true, - last_run: None, - run_count: 0, - max_runs: None, - created_at: now, - metadata: HashMap::new(), - }; - - self.tasks.insert(task_id.clone(), task); - self.save_scheduler_data()?; - - Ok(task_id) - } - - pub fn enable_task(&mut self, task_id: &str) -> Result<()> { - if let Some(task) = self.tasks.get_mut(task_id) { - task.enabled = true; - self.save_scheduler_data()?; - } - Ok(()) - } - - pub fn disable_task(&mut self, task_id: &str) -> Result<()> { - if let Some(task) = self.tasks.get_mut(task_id) { - task.enabled = false; - self.save_scheduler_data()?; - } - Ok(()) - } - - pub fn delete_task(&mut self, task_id: &str) -> Result<()> { - self.tasks.remove(task_id); - self.save_scheduler_data()?; - Ok(()) - } - - pub fn get_task(&self, task_id: &str) -> Option<&ScheduledTask> { - self.tasks.get(task_id) - } - - pub fn get_tasks(&self) -> &HashMap { - &self.tasks - } - - pub fn get_due_tasks(&self) -> Vec<&ScheduledTask> { - let now = Utc::now(); - self.tasks - .values() - .filter(|task| task.enabled && task.next_run <= now) - .collect() - } - - pub fn get_execution_history(&self, limit: Option) -> Vec<&TaskExecution> { - let mut executions: Vec<_> = self.execution_history.iter().collect(); - executions.sort_by(|a, b| b.execution_time.cmp(&a.execution_time)); - - match limit { - Some(limit) => executions.into_iter().take(limit).collect(), - None => executions, - } - } - - pub fn get_scheduler_stats(&self) -> SchedulerStats { - let total_tasks = self.tasks.len(); - let enabled_tasks = self.tasks.values().filter(|task| task.enabled).count(); - let due_tasks = self.get_due_tasks().len(); - - let total_executions = self.execution_history.len(); - let successful_executions = self.execution_history.iter() - .filter(|exec| exec.success) - .count(); - - let today = Utc::now().date_naive(); - let today_executions = self.execution_history.iter() - .filter(|exec| exec.execution_time.date_naive() == today) - .count(); - - let avg_duration = if total_executions > 0 { - self.execution_history.iter() - .map(|exec| exec.duration_ms) - .sum::() as f64 / total_executions as f64 - } else { - 0.0 - }; - - SchedulerStats { - total_tasks, - enabled_tasks, - due_tasks, - total_executions, - successful_executions, - today_executions, - success_rate: if total_executions > 0 { - successful_executions as f64 / total_executions as f64 - } else { - 0.0 - }, - avg_duration_ms: avg_duration, - } - } - - fn create_default_tasks(&mut self) -> Result<()> { - let now = Utc::now(); - - // Daily maintenance task - run every day at 3 AM - let mut daily_maintenance_time = now.date_naive().and_hms_opt(3, 0, 0).unwrap().and_utc(); - if daily_maintenance_time <= now { - daily_maintenance_time = daily_maintenance_time + Duration::days(1); - } - - self.create_task( - TaskType::DailyMaintenance, - daily_maintenance_time, - Some(24), // 24 hours = 1 day - )?; - - // Auto transmission check - every 4 hours - self.create_task( - TaskType::AutoTransmission, - now + Duration::hours(1), - Some(4), - )?; - - // Breakthrough check - every 2 hours - self.create_task( - TaskType::BreakthroughCheck, - now + Duration::minutes(30), - Some(2), - )?; - - // Maintenance transmission - once per day - let mut maintenance_time = now.date_naive().and_hms_opt(12, 0, 0).unwrap().and_utc(); - if maintenance_time <= now { - maintenance_time = maintenance_time + Duration::days(1); - } - - self.create_task( - TaskType::MaintenanceTransmission, - maintenance_time, - Some(24), // 24 hours = 1 day - )?; - - Ok(()) - } - - fn load_scheduler_data(config: &Config) -> Result<(HashMap, Vec)> { - let tasks_file = config.scheduler_tasks_file(); - let history_file = config.scheduler_history_file(); - - let tasks = if tasks_file.exists() { - let content = std::fs::read_to_string(tasks_file) - .context("Failed to read scheduler tasks file")?; - serde_json::from_str(&content) - .context("Failed to parse scheduler tasks file")? - } else { - HashMap::new() - }; - - let history = if history_file.exists() { - let content = std::fs::read_to_string(history_file) - .context("Failed to read scheduler history file")?; - serde_json::from_str(&content) - .context("Failed to parse scheduler history file")? - } else { - Vec::new() - }; - - Ok((tasks, history)) - } - - fn save_scheduler_data(&self) -> Result<()> { - // Save tasks - let tasks_content = serde_json::to_string_pretty(&self.tasks) - .context("Failed to serialize scheduler tasks")?; - std::fs::write(&self.config.scheduler_tasks_file(), tasks_content) - .context("Failed to write scheduler tasks file")?; - - // Save execution history - let history_content = serde_json::to_string_pretty(&self.execution_history) - .context("Failed to serialize scheduler history")?; - std::fs::write(&self.config.scheduler_history_file(), history_content) - .context("Failed to write scheduler history file")?; - - Ok(()) - } -} - -// Type alias for compatibility with CLI interface -pub type Scheduler = AIScheduler; - -impl Scheduler { - pub fn list_tasks(&self) -> Result> { - let tasks: Vec = self.tasks - .values() - .map(|task| ScheduledTaskInfo { - name: task.task_type.to_string(), - schedule: match task.interval_hours { - Some(hours) => format!("Every {} hours", hours), - None => "One-time".to_string(), - }, - next_run: task.next_run, - enabled: task.enabled, - }) - .collect(); - - Ok(tasks) - } -} - -#[derive(Debug, Clone)] -pub struct SchedulerStats { - pub total_tasks: usize, - pub enabled_tasks: usize, - pub due_tasks: usize, - pub total_executions: usize, - pub successful_executions: usize, - pub today_executions: usize, - pub success_rate: f64, - pub avg_duration_ms: f64, -} - -#[derive(Debug, Clone)] -pub struct ScheduledTaskInfo { - pub name: String, - pub schedule: String, - pub next_run: DateTime, - pub enabled: bool, -} \ No newline at end of file diff --git a/src/shell.rs b/src/shell.rs deleted file mode 100644 index 89b1892..0000000 --- a/src/shell.rs +++ /dev/null @@ -1,608 +0,0 @@ -use std::path::PathBuf; -use std::process::{Command, Stdio}; -use std::io::{self, Write}; -use anyhow::{Result, Context}; -use colored::*; -use rustyline::error::ReadlineError; -use rustyline::Editor; -use rustyline::completion::{Completer, FilenameCompleter, Pair}; -use rustyline::history::{History, DefaultHistory}; -use rustyline::highlight::Highlighter; -use rustyline::hint::Hinter; -use rustyline::validate::Validator; -use rustyline::Helper; - -use crate::config::Config; -use crate::persona::Persona; -use crate::ai_provider::{AIProviderClient, AIProvider, AIConfig}; - -pub async fn handle_shell( - user_id: String, - data_dir: Option, - model: Option, - provider: Option, -) -> Result<()> { - let config = Config::new(data_dir)?; - - let mut shell = ShellMode::new(config, user_id)? - .with_ai_provider(provider, model); - - shell.run().await -} - -pub struct ShellMode { - config: Config, - persona: Persona, - ai_provider: Option, - user_id: String, - editor: Editor, -} - -struct ShellCompleter { - completer: FilenameCompleter, -} - -impl ShellCompleter { - fn new() -> Self { - ShellCompleter { - completer: FilenameCompleter::new(), - } - } -} - -impl Helper for ShellCompleter {} - -impl Hinter for ShellCompleter { - type Hint = String; - - fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { - None - } -} - -impl Highlighter for ShellCompleter {} - -impl Validator for ShellCompleter {} - -impl Completer for ShellCompleter { - type Candidate = Pair; - - fn complete( - &self, - line: &str, - pos: usize, - ctx: &rustyline::Context<'_>, - ) -> rustyline::Result<(usize, Vec)> { - // Custom completion for slash commands - if line.starts_with('/') { - let commands = vec![ - "/status", "/relationships", "/memories", "/analyze", - "/fortune", "/clear", "/history", "/help", "/exit" - ]; - - let word_start = line.rfind(' ').map_or(0, |i| i + 1); - let word = &line[word_start..pos]; - - let matches: Vec = commands.iter() - .filter(|cmd| cmd.starts_with(word)) - .map(|cmd| Pair { - display: cmd.to_string(), - replacement: cmd.to_string(), - }) - .collect(); - - return Ok((word_start, matches)); - } - - // Custom completion for shell commands starting with ! - if line.starts_with('!') { - let shell_commands = vec![ - "ls", "pwd", "cd", "cat", "grep", "find", "ps", "top", - "echo", "mkdir", "rmdir", "cp", "mv", "rm", "touch", - "git", "cargo", "npm", "python", "node" - ]; - - let word_start = line.rfind(' ').map_or(1, |i| i + 1); // Skip the '!' - let word = &line[word_start..pos]; - - let matches: Vec = shell_commands.iter() - .filter(|cmd| cmd.starts_with(word)) - .map(|cmd| Pair { - display: cmd.to_string(), - replacement: cmd.to_string(), - }) - .collect(); - - return Ok((word_start, matches)); - } - - // Fallback to filename completion - self.completer.complete(line, pos, ctx) - } -} - -impl ShellMode { - pub fn new(config: Config, user_id: String) -> Result { - let persona = Persona::new(&config)?; - - // Setup rustyline editor with completer - let completer = ShellCompleter::new(); - let mut editor = Editor::with_config( - rustyline::Config::builder() - .tab_stop(4) - .build() - )?; - editor.set_helper(Some(completer)); - - // Load history if exists - let history_file = config.data_dir.join("shell_history.txt"); - if history_file.exists() { - let _ = editor.load_history(&history_file); - } - - Ok(ShellMode { - config, - persona, - ai_provider: None, - user_id, - editor, - }) - } - - pub fn with_ai_provider(mut self, provider: Option, model: Option) -> Self { - // Use provided parameters or fall back to config defaults - let provider_name = provider - .or_else(|| Some(self.config.default_provider.clone())) - .unwrap_or_else(|| "ollama".to_string()); - - let model_name = model.or_else(|| { - // Try to get default model from config for the chosen provider - self.config.providers.get(&provider_name) - .map(|p| p.default_model.clone()) - }).unwrap_or_else(|| { - // Final fallback based on provider - match provider_name.as_str() { - "openai" => "gpt-4o-mini".to_string(), - "ollama" => "qwen2.5-coder:latest".to_string(), - _ => "qwen2.5-coder:latest".to_string(), - } - }); - - let ai_provider = match provider_name.as_str() { - "ollama" => AIProvider::Ollama, - "openai" => AIProvider::OpenAI, - "claude" => AIProvider::Claude, - _ => AIProvider::Ollama, // Default fallback - }; - - let ai_config = AIConfig { - provider: ai_provider, - model: model_name, - api_key: None, // Will be loaded from environment if needed - base_url: None, - max_tokens: Some(2000), - temperature: Some(0.7), - }; - - let client = AIProviderClient::new(ai_config); - self.ai_provider = Some(client); - - self - } - - pub async fn run(&mut self) -> Result<()> { - println!("{}", "🚀 Starting ai.gpt Interactive Shell".cyan().bold()); - - // Show AI provider info - if let Some(ai_provider) = &self.ai_provider { - println!("{}: {} ({})", - "AI Provider".green().bold(), - ai_provider.get_provider().to_string(), - ai_provider.get_model()); - } else { - println!("{}: {}", "AI Provider".yellow().bold(), "Simple mode (no AI)"); - } - - println!("{}", "Type 'help' for commands, 'exit' to quit".dimmed()); - println!("{}", "Use Tab for command completion, Ctrl+C to interrupt, Ctrl+D to exit".dimmed()); - - loop { - // Read user input with rustyline (supports completion, history, etc.) - let readline = self.editor.readline("ai.shell> "); - - match readline { - Ok(line) => { - let input = line.trim(); - - // Skip empty input - if input.is_empty() { - continue; - } - - // Add to history - self.editor.add_history_entry(input) - .context("Failed to add to history")?; - - // Handle input - if let Err(e) = self.handle_input(input).await { - println!("{}: {}", "Error".red().bold(), e); - } - } - Err(ReadlineError::Interrupted) => { - // Ctrl+C - println!("{}", "Use 'exit' or Ctrl+D to quit".yellow()); - continue; - } - Err(ReadlineError::Eof) => { - // Ctrl+D - println!("\n{}", "Goodbye!".cyan()); - break; - } - Err(err) => { - println!("{}: {}", "Input error".red().bold(), err); - break; - } - } - } - - // Save history before exit - self.save_history()?; - - Ok(()) - } - - async fn handle_input(&mut self, input: &str) -> Result<()> { - match input { - // Exit commands - "exit" | "quit" | "/exit" | "/quit" => { - println!("{}", "Goodbye!".cyan()); - std::process::exit(0); - } - // Help command - "help" | "/help" => { - self.show_help(); - } - // Shell commands (starting with !) - input if input.starts_with('!') => { - self.execute_shell_command(&input[1..]).await?; - } - // Slash commands (starting with /) - input if input.starts_with('/') => { - self.execute_slash_command(input).await?; - } - // AI conversation - _ => { - self.handle_ai_conversation(input).await?; - } - } - - Ok(()) - } - - fn show_help(&self) { - println!("\n{}", "ai.gpt Interactive Shell Commands".cyan().bold()); - println!(); - - println!("{}", "Navigation & Input:".yellow().bold()); - println!(" {} - Tab completion for commands and files", "Tab".green()); - println!(" {} - Command history (previous/next)", "↑/↓ or Ctrl+P/N".green()); - println!(" {} - Interrupt current input", "Ctrl+C".green()); - println!(" {} - Exit shell", "Ctrl+D".green()); - println!(); - - println!("{}", "Basic Commands:".yellow().bold()); - println!(" {} - Show this help", "help".green()); - println!(" {} - Exit the shell", "exit, quit".green()); - println!(" {} - Clear screen", "/clear".green()); - println!(" {} - Show command history", "/history".green()); - println!(); - - println!("{}", "Shell Commands:".yellow().bold()); - println!(" {} - Execute shell command (Tab completion)", "!".green()); - println!(" {} - List files", "!ls".green()); - println!(" {} - Show current directory", "!pwd".green()); - println!(" {} - Git status", "!git status".green()); - println!(" {} - Cargo build", "!cargo build".green()); - println!(); - - println!("{}", "AI Commands:".yellow().bold()); - println!(" {} - Show AI status and relationship", "/status".green()); - println!(" {} - List all relationships", "/relationships".green()); - println!(" {} - Show recent memories", "/memories".green()); - println!(" {} - Analyze current directory", "/analyze".green()); - println!(" {} - Show today's fortune", "/fortune".green()); - println!(); - - println!("{}", "Conversation:".yellow().bold()); - println!(" {} - Chat with AI using configured provider", "Any other input".green()); - println!(" {} - AI responses track relationship changes", "Relationship tracking".dimmed()); - println!(); - } - - async fn execute_shell_command(&self, command: &str) -> Result<()> { - println!("{} {}", "Executing:".blue().bold(), command.yellow()); - - let output = if cfg!(target_os = "windows") { - Command::new("cmd") - .args(["/C", command]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .context("Failed to execute command")? - } else { - Command::new("sh") - .args(["-c", command]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .context("Failed to execute command")? - }; - - // Print stdout - if !output.stdout.is_empty() { - let stdout = String::from_utf8_lossy(&output.stdout); - println!("{}", stdout); - } - - // Print stderr in red - if !output.stderr.is_empty() { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("{}", stderr.red()); - } - - // Show exit code if not successful - if !output.status.success() { - if let Some(code) = output.status.code() { - println!("{}: {}", "Exit code".red().bold(), code); - } - } - - Ok(()) - } - - async fn execute_slash_command(&mut self, command: &str) -> Result<()> { - match command { - "/status" => { - self.show_ai_status().await?; - } - "/relationships" => { - self.show_relationships().await?; - } - "/memories" => { - self.show_memories().await?; - } - "/analyze" => { - self.analyze_directory().await?; - } - "/fortune" => { - self.show_fortune().await?; - } - "/clear" => { - // Clear screen - print!("\x1B[2J\x1B[1;1H"); - io::stdout().flush()?; - } - "/history" => { - self.show_history(); - } - _ => { - println!("{}: {}", "Unknown command".red().bold(), command); - println!("Type '{}' for available commands", "help".green()); - } - } - - Ok(()) - } - - async fn handle_ai_conversation(&mut self, input: &str) -> Result<()> { - let (response, relationship_delta) = if let Some(ai_provider) = &self.ai_provider { - // Use AI provider for response - self.persona.process_ai_interaction(&self.user_id, input, - Some(ai_provider.get_provider().to_string()), - Some(ai_provider.get_model().to_string())).await? - } else { - // Use simple response - self.persona.process_interaction(&self.user_id, input)? - }; - - // Display conversation - println!("{}: {}", "You".cyan().bold(), input); - println!("{}: {}", "AI".green().bold(), response); - - // Show relationship change if significant - if relationship_delta.abs() >= 0.1 { - if relationship_delta > 0.0 { - println!("{}", format!("(+{:.2} relationship)", relationship_delta).green()); - } else { - println!("{}", format!("({:.2} relationship)", relationship_delta).red()); - } - } - - println!(); // Add spacing - - Ok(()) - } - - async fn show_ai_status(&self) -> Result<()> { - let state = self.persona.get_current_state()?; - - println!("\n{}", "AI Status".cyan().bold()); - println!("Mood: {}", state.current_mood.yellow()); - println!("Fortune: {}/10", state.fortune_value.to_string().yellow()); - - if let Some(relationship) = self.persona.get_relationship(&self.user_id) { - println!("\n{}", "Your Relationship".cyan().bold()); - println!("Status: {}", relationship.status.to_string().yellow()); - println!("Score: {:.2} / {}", relationship.score, relationship.threshold); - println!("Interactions: {}", relationship.total_interactions); - } - - println!(); - Ok(()) - } - - async fn show_relationships(&self) -> Result<()> { - let relationships = self.persona.list_all_relationships(); - - if relationships.is_empty() { - println!("{}", "No relationships yet".yellow()); - return Ok(()); - } - - println!("\n{}", "All Relationships".cyan().bold()); - println!(); - - for (user_id, rel) in relationships { - let transmission = if rel.is_broken { - "💔" - } else if rel.transmission_enabled { - "✓" - } else { - "✗" - }; - - let user_display = if user_id.len() > 20 { - format!("{}...", &user_id[..20]) - } else { - user_id - }; - - println!("{:<25} {:<12} {:<8} {}", - user_display.cyan(), - rel.status.to_string(), - format!("{:.2}", rel.score), - transmission); - } - - println!(); - Ok(()) - } - - async fn show_memories(&mut self) -> Result<()> { - let memories = self.persona.get_memories(&self.user_id, 10); - - if memories.is_empty() { - println!("{}", "No memories yet".yellow()); - return Ok(()); - } - - println!("\n{}", "Recent Memories".cyan().bold()); - println!(); - - for (i, memory) in memories.iter().enumerate() { - println!("{}: {}", - format!("Memory {}", i + 1).dimmed(), - memory); - println!(); - } - - Ok(()) - } - - async fn analyze_directory(&self) -> Result<()> { - println!("{}", "Analyzing current directory...".blue().bold()); - - // Get current directory - let current_dir = std::env::current_dir() - .context("Failed to get current directory")?; - - println!("Directory: {}", current_dir.display().to_string().yellow()); - - // List files and directories - let entries = std::fs::read_dir(¤t_dir) - .context("Failed to read directory")?; - - let mut files = Vec::new(); - let mut dirs = Vec::new(); - - for entry in entries { - let entry = entry.context("Failed to read directory entry")?; - let path = entry.path(); - let name = path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("Unknown"); - - if path.is_dir() { - dirs.push(name.to_string()); - } else { - files.push(name.to_string()); - } - } - - if !dirs.is_empty() { - println!("\n{}: {}", "Directories".blue().bold(), dirs.join(", ")); - } - - if !files.is_empty() { - println!("{}: {}", "Files".blue().bold(), files.join(", ")); - } - - // Check for common project files - let project_files = ["Cargo.toml", "package.json", "requirements.txt", "Makefile", "README.md"]; - let found_files: Vec<_> = project_files.iter() - .filter(|&&file| files.contains(&file.to_string())) - .collect(); - - if !found_files.is_empty() { - println!("\n{}: {}", "Project files detected".green().bold(), - found_files.iter().map(|s| s.to_string()).collect::>().join(", ")); - } - - println!(); - Ok(()) - } - - async fn show_fortune(&self) -> Result<()> { - let state = self.persona.get_current_state()?; - - let fortune_stars = "🌟".repeat(state.fortune_value as usize); - let empty_stars = "☆".repeat((10 - state.fortune_value) as usize); - - println!("\n{}", "AI Fortune".yellow().bold()); - println!("{}{}", fortune_stars, empty_stars); - println!("Today's Fortune: {}/10", state.fortune_value); - - if state.breakthrough_triggered { - println!("{}", "⚡ BREAKTHROUGH! Special fortune activated!".yellow()); - } - - println!(); - Ok(()) - } - - fn show_history(&self) { - println!("\n{}", "Command History".cyan().bold()); - - let history = self.editor.history(); - if history.is_empty() { - println!("{}", "No commands in history".yellow()); - return; - } - - // Show last 20 commands - let start = if history.len() > 20 { history.len() - 20 } else { 0 }; - for (i, entry) in history.iter().enumerate().skip(start) { - println!("{:2}: {}", i + 1, entry); - } - - println!(); - } - - fn save_history(&mut self) -> Result<()> { - let history_file = self.config.data_dir.join("shell_history.txt"); - self.editor.save_history(&history_file) - .context("Failed to save shell history")?; - Ok(()) - } -} - -// Extend AIProvider to have Display and helper methods -impl AIProvider { - fn to_string(&self) -> String { - match self { - AIProvider::OpenAI => "openai".to_string(), - AIProvider::Ollama => "ollama".to_string(), - AIProvider::Claude => "claude".to_string(), - } - } -} \ No newline at end of file diff --git a/src/status.rs b/src/status.rs deleted file mode 100644 index 356f24e..0000000 --- a/src/status.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::path::PathBuf; -use anyhow::Result; -use colored::*; - -use crate::config::Config; -use crate::persona::Persona; - -pub async fn handle_status(user_id: Option, data_dir: Option) -> Result<()> { - // Load configuration - let config = Config::new(data_dir)?; - - // Initialize persona - let persona = Persona::new(&config)?; - - // Get current state - let state = persona.get_current_state()?; - - // Display AI status - println!("{}", "ai.gpt Status".cyan().bold()); - println!("Mood: {}", state.current_mood); - println!("Fortune: {}/10", state.fortune_value); - - if state.breakthrough_triggered { - println!("{}", "⚡ Breakthrough triggered!".yellow()); - } - - // Show personality traits - println!("\n{}", "Current Personality".cyan().bold()); - for (trait_name, value) in &state.base_personality { - println!("{}: {:.2}", trait_name.cyan(), value); - } - - // Show specific relationship if requested - if let Some(user_id) = user_id { - if let Some(relationship) = persona.get_relationship(&user_id) { - println!("\n{}: {}", "Relationship with".cyan(), user_id); - println!("Status: {}", relationship.status); - println!("Score: {:.2}", relationship.score); - println!("Total Interactions: {}", relationship.total_interactions); - println!("Transmission Enabled: {}", relationship.transmission_enabled); - - if relationship.is_broken { - println!("{}", "⚠️ This relationship is broken and cannot be repaired.".red()); - } - } else { - println!("\n{}: {}", "No relationship found with".yellow(), user_id); - } - } - - Ok(()) -} \ No newline at end of file diff --git a/src/submodules.rs b/src/submodules.rs deleted file mode 100644 index 4980ee2..0000000 --- a/src/submodules.rs +++ /dev/null @@ -1,480 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use anyhow::{Result, Context}; -use colored::*; -use serde::{Deserialize, Serialize}; - -use crate::config::Config; - -pub async fn handle_submodules( - action: String, - module: Option, - all: bool, - dry_run: bool, - auto_commit: bool, - verbose: bool, - data_dir: Option, -) -> Result<()> { - let config = Config::new(data_dir)?; - let mut submodule_manager = SubmoduleManager::new(config); - - match action.as_str() { - "list" => { - submodule_manager.list_submodules(verbose).await?; - } - "update" => { - submodule_manager.update_submodules(module, all, dry_run, auto_commit, verbose).await?; - } - "status" => { - submodule_manager.show_submodule_status().await?; - } - _ => { - return Err(anyhow::anyhow!("Unknown submodule action: {}", action)); - } - } - - Ok(()) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubmoduleInfo { - pub name: String, - pub path: String, - pub branch: String, - pub current_commit: Option, - pub target_commit: Option, - pub status: String, -} - -impl Default for SubmoduleInfo { - fn default() -> Self { - SubmoduleInfo { - name: String::new(), - path: String::new(), - branch: "main".to_string(), - current_commit: None, - target_commit: None, - status: "unknown".to_string(), - } - } -} - -#[allow(dead_code)] -pub struct SubmoduleManager { - config: Config, - ai_root: PathBuf, - submodules: HashMap, -} - -impl SubmoduleManager { - pub fn new(config: Config) -> Self { - let ai_root = dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("ai") - .join("ai"); - - SubmoduleManager { - config, - ai_root, - submodules: HashMap::new(), - } - } - - pub async fn list_submodules(&mut self, verbose: bool) -> Result<()> { - println!("{}", "📋 Submodules Status".cyan().bold()); - println!(); - - let submodules = self.parse_gitmodules()?; - - if submodules.is_empty() { - println!("{}", "No submodules found".yellow()); - return Ok(()); - } - - // Display submodules in a table format - println!("{:<15} {:<25} {:<15} {}", - "Module".cyan().bold(), - "Path".cyan().bold(), - "Branch".cyan().bold(), - "Status".cyan().bold()); - println!("{}", "-".repeat(80)); - - for (module_name, module_info) in &submodules { - let status_color = match module_info.status.as_str() { - "clean" => module_info.status.green(), - "modified" => module_info.status.yellow(), - "missing" => module_info.status.red(), - "conflicts" => module_info.status.red(), - _ => module_info.status.normal(), - }; - - println!("{:<15} {:<25} {:<15} {}", - module_name.blue(), - module_info.path, - module_info.branch.green(), - status_color); - } - - println!(); - - if verbose { - println!("Total submodules: {}", submodules.len().to_string().cyan()); - println!("Repository root: {}", self.ai_root.display().to_string().blue()); - } - - Ok(()) - } - - pub async fn update_submodules( - &mut self, - module: Option, - all: bool, - dry_run: bool, - auto_commit: bool, - verbose: bool - ) -> Result<()> { - if !module.is_some() && !all { - return Err(anyhow::anyhow!("Either --module or --all is required")); - } - - if module.is_some() && all { - return Err(anyhow::anyhow!("Cannot use both --module and --all")); - } - - let submodules = self.parse_gitmodules()?; - - if submodules.is_empty() { - println!("{}", "No submodules found".yellow()); - return Ok(()); - } - - // Determine which modules to update - let modules_to_update: Vec = if all { - submodules.keys().cloned().collect() - } else if let Some(module_name) = module { - if !submodules.contains_key(&module_name) { - return Err(anyhow::anyhow!( - "Submodule '{}' not found. Available modules: {}", - module_name, - submodules.keys().cloned().collect::>().join(", ") - )); - } - vec![module_name] - } else { - vec![] - }; - - if dry_run { - println!("{}", "🔍 DRY RUN MODE - No changes will be made".yellow().bold()); - } - - println!("{}", format!("🔄 Updating {} submodule(s)...", modules_to_update.len()).cyan().bold()); - - let mut updated_modules = Vec::new(); - - for module_name in modules_to_update { - if let Some(module_info) = submodules.get(&module_name) { - println!("\n{}", format!("📦 Processing: {}", module_name).blue().bold()); - - let module_path = PathBuf::from(&module_info.path); - let full_path = self.ai_root.join(&module_path); - - if !full_path.exists() { - println!("{}", format!("❌ Module directory not found: {}", module_info.path).red()); - continue; - } - - // Get current commit - let current_commit = self.get_current_commit(&full_path)?; - - if dry_run { - println!("{}", format!("🔍 Would update {} to branch {}", module_name, module_info.branch).yellow()); - if let Some(ref commit) = current_commit { - println!("{}", format!("Current: {}", commit).dimmed()); - } - continue; - } - - // Perform update - if let Err(e) = self.update_single_module(&module_name, &module_info, &full_path).await { - println!("{}", format!("❌ Failed to update {}: {}", module_name, e).red()); - continue; - } - - // Get new commit - let new_commit = self.get_current_commit(&full_path)?; - - if current_commit != new_commit { - println!("{}", format!("✅ Updated {} ({:?} → {:?})", - module_name, - current_commit.as_deref().unwrap_or("unknown"), - new_commit.as_deref().unwrap_or("unknown")).green()); - updated_modules.push((module_name.clone(), current_commit, new_commit)); - } else { - println!("{}", "✅ Already up to date".green()); - } - } - } - - // Summary - if !updated_modules.is_empty() { - println!("\n{}", format!("🎉 Successfully updated {} module(s)", updated_modules.len()).green().bold()); - - if verbose { - for (module_name, old_commit, new_commit) in &updated_modules { - println!(" • {}: {:?} → {:?}", - module_name, - old_commit.as_deref().unwrap_or("unknown"), - new_commit.as_deref().unwrap_or("unknown")); - } - } - - if auto_commit && !dry_run { - self.auto_commit_changes(&updated_modules).await?; - } else if !dry_run { - println!("{}", "💾 Changes staged but not committed".yellow()); - println!("Run with --auto-commit to commit automatically"); - } - } else if !dry_run { - println!("{}", "No modules needed updating".yellow()); - } - - Ok(()) - } - - pub async fn show_submodule_status(&self) -> Result<()> { - println!("{}", "📊 Submodule Status Overview".cyan().bold()); - println!(); - - let submodules = self.parse_gitmodules()?; - let mut total_modules = 0; - let mut clean_modules = 0; - let mut modified_modules = 0; - let mut missing_modules = 0; - - for (module_name, module_info) in submodules { - let module_path = self.ai_root.join(&module_info.path); - - if module_path.exists() { - total_modules += 1; - match module_info.status.as_str() { - "clean" => clean_modules += 1, - "modified" => modified_modules += 1, - _ => {} - } - } else { - missing_modules += 1; - } - - println!("{}: {}", - module_name.blue(), - if module_path.exists() { - module_info.status.green() - } else { - "missing".red() - }); - } - - println!(); - println!("Summary: {} total, {} clean, {} modified, {} missing", - total_modules.to_string().cyan(), - clean_modules.to_string().green(), - modified_modules.to_string().yellow(), - missing_modules.to_string().red()); - - Ok(()) - } - - fn parse_gitmodules(&self) -> Result> { - let gitmodules_path = self.ai_root.join(".gitmodules"); - - if !gitmodules_path.exists() { - return Ok(HashMap::new()); - } - - let content = std::fs::read_to_string(&gitmodules_path) - .with_context(|| format!("Failed to read .gitmodules file: {}", gitmodules_path.display()))?; - - let mut submodules = HashMap::new(); - let mut current_name: Option = None; - let mut current_path: Option = None; - - for line in content.lines() { - let line = line.trim(); - - if line.starts_with("[submodule \"") && line.ends_with("\"]") { - // Save previous submodule if complete - if let (Some(name), Some(path)) = (current_name.take(), current_path.take()) { - let mut info = SubmoduleInfo::default(); - info.name = name.clone(); - info.path = path; - info.branch = self.get_target_branch(&name); - info.status = self.get_submodule_status(&name, &info.path)?; - submodules.insert(name, info); - } - - // Extract new submodule name - current_name = Some(line[12..line.len()-2].to_string()); - } else if line.starts_with("path = ") { - current_path = Some(line[7..].to_string()); - } - } - - // Save last submodule - if let (Some(name), Some(path)) = (current_name, current_path) { - let mut info = SubmoduleInfo::default(); - info.name = name.clone(); - info.path = path; - info.branch = self.get_target_branch(&name); - info.status = self.get_submodule_status(&name, &info.path)?; - submodules.insert(name, info); - } - - Ok(submodules) - } - - fn get_target_branch(&self, module_name: &str) -> String { - // Try to get from ai.json configuration - match module_name { - "verse" => "main".to_string(), - "card" => "main".to_string(), - "bot" => "main".to_string(), - _ => "main".to_string(), - } - } - - fn get_submodule_status(&self, _module_name: &str, module_path: &str) -> Result { - let full_path = self.ai_root.join(module_path); - - if !full_path.exists() { - return Ok("missing".to_string()); - } - - // Check git status - let output = std::process::Command::new("git") - .args(&["submodule", "status", module_path]) - .current_dir(&self.ai_root) - .output(); - - match output { - Ok(output) if output.status.success() => { - let stdout = String::from_utf8_lossy(&output.stdout); - if let Some(status_char) = stdout.chars().next() { - match status_char { - ' ' => Ok("clean".to_string()), - '+' => Ok("modified".to_string()), - '-' => Ok("not_initialized".to_string()), - 'U' => Ok("conflicts".to_string()), - _ => Ok("unknown".to_string()), - } - } else { - Ok("unknown".to_string()) - } - } - _ => Ok("unknown".to_string()) - } - } - - fn get_current_commit(&self, module_path: &PathBuf) -> Result> { - let output = std::process::Command::new("git") - .args(&["rev-parse", "HEAD"]) - .current_dir(module_path) - .output(); - - match output { - Ok(output) if output.status.success() => { - let commit = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if commit.len() >= 8 { - Ok(Some(commit[..8].to_string())) - } else { - Ok(Some(commit)) - } - } - _ => Ok(None) - } - } - - async fn update_single_module( - &self, - _module_name: &str, - module_info: &SubmoduleInfo, - module_path: &PathBuf - ) -> Result<()> { - // Fetch latest changes - println!("{}", "Fetching latest changes...".dimmed()); - let fetch_output = std::process::Command::new("git") - .args(&["fetch", "origin"]) - .current_dir(module_path) - .output()?; - - if !fetch_output.status.success() { - return Err(anyhow::anyhow!("Failed to fetch: {}", - String::from_utf8_lossy(&fetch_output.stderr))); - } - - // Switch to target branch - println!("{}", format!("Switching to branch {}...", module_info.branch).dimmed()); - let checkout_output = std::process::Command::new("git") - .args(&["checkout", &module_info.branch]) - .current_dir(module_path) - .output()?; - - if !checkout_output.status.success() { - return Err(anyhow::anyhow!("Failed to checkout {}: {}", - module_info.branch, String::from_utf8_lossy(&checkout_output.stderr))); - } - - // Pull latest changes - let pull_output = std::process::Command::new("git") - .args(&["pull", "origin", &module_info.branch]) - .current_dir(module_path) - .output()?; - - if !pull_output.status.success() { - return Err(anyhow::anyhow!("Failed to pull: {}", - String::from_utf8_lossy(&pull_output.stderr))); - } - - // Stage the submodule update - let add_output = std::process::Command::new("git") - .args(&["add", &module_info.path]) - .current_dir(&self.ai_root) - .output()?; - - if !add_output.status.success() { - return Err(anyhow::anyhow!("Failed to stage submodule: {}", - String::from_utf8_lossy(&add_output.stderr))); - } - - Ok(()) - } - - async fn auto_commit_changes(&self, updated_modules: &[(String, Option, Option)]) -> Result<()> { - println!("{}", "💾 Auto-committing changes...".blue()); - - let mut commit_message = format!("Update submodules\n\n📦 Updated modules: {}\n", updated_modules.len()); - for (module_name, old_commit, new_commit) in updated_modules { - commit_message.push_str(&format!( - "- {}: {} → {}\n", - module_name, - old_commit.as_deref().unwrap_or("unknown"), - new_commit.as_deref().unwrap_or("unknown") - )); - } - commit_message.push_str("\n🤖 Generated with aigpt-rs submodules update"); - - let commit_output = std::process::Command::new("git") - .args(&["commit", "-m", &commit_message]) - .current_dir(&self.ai_root) - .output()?; - - if commit_output.status.success() { - println!("{}", "✅ Changes committed successfully".green()); - } else { - return Err(anyhow::anyhow!("Failed to commit: {}", - String::from_utf8_lossy(&commit_output.stderr))); - } - - Ok(()) - } -} \ No newline at end of file diff --git a/src/tokens.rs b/src/tokens.rs deleted file mode 100644 index 9f97ba1..0000000 --- a/src/tokens.rs +++ /dev/null @@ -1,1099 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::{DateTime, Local}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::File; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; - -use crate::cli::TokenCommands; -use std::process::Command; - -/// Token usage record from Claude Code JSONL files -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TokenRecord { - #[serde(default)] - pub timestamp: Option, - #[serde(default)] - pub r#type: Option, - #[serde(default)] - pub message: Option, - #[serde(default)] - #[serde(rename = "sessionId")] - pub session_id: Option, - #[serde(default)] - #[serde(rename = "costUSD")] - pub cost_usd: Option, - #[serde(default)] - pub uuid: Option, - #[serde(default)] - pub cwd: Option, -} - -/// Token usage details -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TokenUsage { - #[serde(default)] - pub input_tokens: Option, - #[serde(default)] - pub output_tokens: Option, - #[serde(default)] - pub total_tokens: Option, -} - -/// Cost calculation modes -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum CostMode { - /// Use costUSD if available, otherwise calculate from tokens - Auto, - /// Always calculate costs from token counts, ignore costUSD - Calculate, - /// Always use pre-calculated costUSD values, show 0 for missing costs - Display, -} - -impl From<&str> for CostMode { - fn from(mode: &str) -> Self { - match mode { - "calculate" => CostMode::Calculate, - "display" => CostMode::Display, - _ => CostMode::Auto, // default - } - } -} - -/// Cost calculation summary -#[derive(Debug, Clone, Serialize)] -pub struct CostSummary { - pub input_tokens: u64, - pub output_tokens: u64, - pub cache_creation_tokens: u64, - pub cache_read_tokens: u64, - pub total_tokens: u64, - pub input_cost_usd: f64, - pub output_cost_usd: f64, - pub cache_cost_usd: f64, - pub total_cost_usd: f64, - pub total_cost_jpy: f64, - pub record_count: usize, -} - -/// Daily breakdown of token usage -#[derive(Debug, Clone, Serialize)] -pub struct DailyBreakdown { - pub date: String, - pub summary: CostSummary, -} - -/// Project breakdown of token usage -#[derive(Debug, Clone, Serialize)] -pub struct ProjectBreakdown { - pub project_path: String, - pub project_name: String, - pub summary: CostSummary, -} - -/// Configuration for cost calculation -#[derive(Debug, Clone)] -pub struct CostConfig { - pub input_cost_per_1m: f64, // USD per 1M input tokens - pub output_cost_per_1m: f64, // USD per 1M output tokens - pub usd_to_jpy_rate: f64, -} - -impl Default for CostConfig { - fn default() -> Self { - Self { - input_cost_per_1m: 3.0, - output_cost_per_1m: 15.0, - usd_to_jpy_rate: 150.0, - } - } -} - -/// Token analysis functionality -pub struct TokenAnalyzer { - config: CostConfig, -} - -impl TokenAnalyzer { - pub fn new() -> Self { - Self { - config: CostConfig::default(), - } - } - - pub fn with_config(config: CostConfig) -> Self { - Self { config } - } - - /// Find Claude Code data directory - pub fn find_claude_data_dir() -> Option { - let possible_dirs = [ - dirs::home_dir().map(|h| h.join(".claude")), - dirs::config_dir().map(|c| c.join("claude")), - Some(PathBuf::from(".claude")), - ]; - - for dir_opt in possible_dirs.iter() { - if let Some(dir) = dir_opt { - if dir.exists() && dir.is_dir() { - return Some(dir.clone()); - } - } - } - - None - } - - /// Parse JSONL files from Claude data directory (recursive search) - pub fn parse_jsonl_files>(&self, claude_dir: P) -> Result> { - let claude_dir = claude_dir.as_ref(); - let mut records = Vec::new(); - - // Recursively look for JSONL files in the directory and subdirectories - self.parse_jsonl_files_recursive(claude_dir, &mut records)?; - - Ok(records) - } - - /// Recursively parse JSONL files - fn parse_jsonl_files_recursive(&self, dir: &Path, records: &mut Vec) -> Result<()> { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - // Recursively search subdirectories - self.parse_jsonl_files_recursive(&path, records)?; - } else if path.extension().map_or(false, |ext| ext == "jsonl") { - match self.parse_jsonl_file(&path) { - Ok(mut file_records) => records.append(&mut file_records), - Err(e) => { - eprintln!("Warning: Failed to parse {}: {}", path.display(), e); - } - } - } - } - } - Ok(()) - } - - /// Parse a single JSONL file - fn parse_jsonl_file>(&self, file_path: P) -> Result> { - let file = File::open(file_path)?; - let reader = BufReader::new(file); - let mut records = Vec::new(); - - for (line_num, line) in reader.lines().enumerate() { - match line { - Ok(line_content) => { - if line_content.trim().is_empty() { - continue; - } - - match serde_json::from_str::(&line_content) { - Ok(record) => { - // Only include records with usage data in message or costUSD - let has_usage_data = record.cost_usd.is_some() || - record.message.as_ref().and_then(|m| m.get("usage")).is_some(); - if has_usage_data { - records.push(record); - } - } - Err(e) => { - eprintln!("Warning: Failed to parse line {}: {}", line_num + 1, e); - } - } - } - Err(e) => { - eprintln!("Warning: Failed to read line {}: {}", line_num + 1, e); - } - } - } - - Ok(records) - } - - /// Calculate cost summary from records - pub fn calculate_costs(&self, records: &[TokenRecord]) -> CostSummary { - self.calculate_costs_with_mode(records, CostMode::Auto) - } - - /// Calculate cost summary from records with specified cost mode - pub fn calculate_costs_with_mode(&self, records: &[TokenRecord], mode: CostMode) -> CostSummary { - let mut input_tokens = 0u64; - let mut output_tokens = 0u64; - let mut cache_creation_tokens = 0u64; - let mut cache_read_tokens = 0u64; - let mut total_cost_usd = 0.0; - let mut cost_records_count = 0; - - for record in records { - // Extract token usage from message.usage field - if let Some(message) = &record.message { - if let Some(usage) = message.get("usage") { - if let Some(input) = usage.get("input_tokens").and_then(|v| v.as_u64()) { - input_tokens += input; - } - if let Some(output) = usage.get("output_tokens").and_then(|v| v.as_u64()) { - output_tokens += output; - } - // Track cache tokens separately - if let Some(cache_creation) = usage.get("cache_creation_input_tokens").and_then(|v| v.as_u64()) { - cache_creation_tokens += cache_creation; - } - if let Some(cache_read) = usage.get("cache_read_input_tokens").and_then(|v| v.as_u64()) { - cache_read_tokens += cache_read; - } - } - } - - // Calculate cost based on mode - let record_cost = match mode { - CostMode::Display => { - // Always use costUSD, even if undefined (0.0) - record.cost_usd.unwrap_or(0.0) - } - CostMode::Calculate => { - // Always calculate from tokens - if let Some(message) = &record.message { - if let Some(usage) = message.get("usage") { - let input = usage.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - let output = usage.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - let cache_creation = usage.get("cache_creation_input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - let cache_read = usage.get("cache_read_input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - - // Regular tokens at normal price - let regular_cost = (input / 1_000_000.0) * self.config.input_cost_per_1m + - (output / 1_000_000.0) * self.config.output_cost_per_1m; - - // Cache tokens - cache creation at normal price, cache read at reduced price - let cache_cost = (cache_creation / 1_000_000.0) * self.config.input_cost_per_1m + - (cache_read / 1_000_000.0) * (self.config.input_cost_per_1m * 0.1); // 10% of normal price for cache reads - - regular_cost + cache_cost - } else { - 0.0 - } - } else { - 0.0 - } - } - CostMode::Auto => { - // Use costUSD if available, otherwise calculate from tokens - if let Some(cost) = record.cost_usd { - cost - } else if let Some(message) = &record.message { - if let Some(usage) = message.get("usage") { - let input = usage.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - let output = usage.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - let cache_creation = usage.get("cache_creation_input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - let cache_read = usage.get("cache_read_input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as f64; - - // Regular tokens at normal price - let regular_cost = (input / 1_000_000.0) * self.config.input_cost_per_1m + - (output / 1_000_000.0) * self.config.output_cost_per_1m; - - // Cache tokens - cache creation at normal price, cache read at reduced price - let cache_cost = (cache_creation / 1_000_000.0) * self.config.input_cost_per_1m + - (cache_read / 1_000_000.0) * (self.config.input_cost_per_1m * 0.1); // 10% of normal price for cache reads - - regular_cost + cache_cost - } else { - 0.0 - } - } else { - 0.0 - } - } - }; - - total_cost_usd += record_cost; - if record.cost_usd.is_some() { - cost_records_count += 1; - } - } - - // Debug info - match mode { - CostMode::Display => { - if cost_records_count > 0 { - println!("Debug: Display mode - Found {} records with costUSD data, total: ${:.4}", cost_records_count, total_cost_usd); - } else { - println!("Debug: Display mode - No costUSD data found, showing $0.00"); - } - } - CostMode::Calculate => { - println!("Debug: Calculate mode - Using token-based calculation only, total: ${:.4}", total_cost_usd); - } - CostMode::Auto => { - if cost_records_count > 0 { - println!("Debug: Auto mode - Found {} records with costUSD data, total: ${:.4}", cost_records_count, total_cost_usd); - } else { - println!("Debug: Auto mode - No costUSD data found, using token-based calculation"); - } - } - } - - let total_tokens = input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens; - - // Calculate component costs for display purposes - let input_cost_usd = (input_tokens as f64 / 1_000_000.0) * self.config.input_cost_per_1m; - let output_cost_usd = (output_tokens as f64 / 1_000_000.0) * self.config.output_cost_per_1m; - let cache_cost_usd = (cache_creation_tokens as f64 / 1_000_000.0) * self.config.input_cost_per_1m + - (cache_read_tokens as f64 / 1_000_000.0) * (self.config.input_cost_per_1m * 0.1); - let total_cost_jpy = total_cost_usd * self.config.usd_to_jpy_rate; - - CostSummary { - input_tokens, - output_tokens, - cache_creation_tokens, - cache_read_tokens, - total_tokens, - input_cost_usd, - output_cost_usd, - cache_cost_usd, - total_cost_usd, - total_cost_jpy, - record_count: records.len(), - } - } - - /// Group records by date (JST timezone) - pub fn group_by_date(&self, records: &[TokenRecord]) -> Result>> { - self.group_by_date_with_mode(records, CostMode::Auto) - } - - /// Group records by date (JST timezone) with cost mode - pub fn group_by_date_with_mode(&self, records: &[TokenRecord], _mode: CostMode) -> Result>> { - let mut grouped: HashMap> = HashMap::new(); - - for record in records { - if let Some(ref timestamp) = record.timestamp { - if let Ok(date_str) = self.extract_date_jst(timestamp) { - grouped.entry(date_str).or_insert_with(Vec::new).push(record.clone()); - } - } - } - - Ok(grouped) - } - - /// Extract date in JST from timestamp - fn extract_date_jst(&self, timestamp: &str) -> Result { - if timestamp.is_empty() { - return Err(anyhow!("Empty timestamp")); - } - - // Try to parse various timestamp formats and convert to JST - let dt = if let Ok(dt) = DateTime::parse_from_rfc3339(timestamp) { - dt.with_timezone(&chrono::FixedOffset::east_opt(9 * 3600).unwrap()) - } else if let Ok(dt) = DateTime::parse_from_str(timestamp, "%Y-%m-%dT%H:%M:%S%.fZ") { - dt.with_timezone(&chrono::FixedOffset::east_opt(9 * 3600).unwrap()) - } else if let Ok(dt) = chrono::DateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S") { - dt.with_timezone(&chrono::FixedOffset::east_opt(9 * 3600).unwrap()) - } else { - return Err(anyhow!("Failed to parse timestamp: {}", timestamp)); - }; - - Ok(dt.format("%Y-%m-%d").to_string()) - } - - /// Group records by project path - pub fn group_by_project(&self, records: &[TokenRecord]) -> Result>> { - self.group_by_project_with_mode(records, CostMode::Auto) - } - - /// Group records by project path with cost mode - pub fn group_by_project_with_mode(&self, records: &[TokenRecord], _mode: CostMode) -> Result>> { - let mut grouped: HashMap> = HashMap::new(); - - for record in records { - // Extract project path from cwd field (at top level of JSON) - let project_path = record.cwd - .as_ref() - .unwrap_or(&"Unknown Project".to_string()) - .clone(); - - grouped.entry(project_path).or_insert_with(Vec::new).push(record.clone()); - } - - Ok(grouped) - } - - /// Generate project breakdown with cost mode - pub fn project_breakdown_with_mode(&self, records: &[TokenRecord], mode: CostMode) -> Result> { - let grouped = self.group_by_project_with_mode(records, mode)?; - let mut breakdowns: Vec = grouped - .into_iter() - .map(|(project_path, project_records)| { - let project_name = std::path::Path::new(&project_path) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(&project_path) - .to_string(); - - ProjectBreakdown { - project_path: project_path.clone(), - project_name, - summary: self.calculate_costs_with_mode(&project_records, mode), - } - }) - .collect(); - - // Sort by total cost (highest first) - breakdowns.sort_by(|a, b| b.summary.total_cost_usd.partial_cmp(&a.summary.total_cost_usd).unwrap_or(std::cmp::Ordering::Equal)); - - Ok(breakdowns) - } - - /// Generate project breakdown - pub fn project_breakdown(&self, records: &[TokenRecord]) -> Result> { - self.project_breakdown_with_mode(records, CostMode::Auto) - } - - /// Generate daily breakdown - pub fn daily_breakdown(&self, records: &[TokenRecord]) -> Result> { - let grouped = self.group_by_date(records)?; - let mut breakdowns: Vec = grouped - .into_iter() - .map(|(date, date_records)| DailyBreakdown { - date, - summary: self.calculate_costs(&date_records), - }) - .collect(); - - // Sort by date (most recent first) - breakdowns.sort_by(|a, b| b.date.cmp(&a.date)); - - Ok(breakdowns) - } - - /// Filter records by time period - pub fn filter_by_period(&self, records: &[TokenRecord], period: &str) -> Result> { - let now = Local::now(); - let cutoff = match period { - "today" => now.date_naive().and_hms_opt(0, 0, 0).unwrap(), - "week" => (now - chrono::Duration::days(7)).naive_local(), - "month" => (now - chrono::Duration::days(30)).naive_local(), - "all" => return Ok(records.to_vec()), - _ => return Err(anyhow!("Invalid period: {}", period)), - }; - - let filtered: Vec = records - .iter() - .filter(|record| { - if let Some(ref timestamp) = record.timestamp { - if let Ok(date_str) = self.extract_date_jst(timestamp) { - if let Ok(record_date) = chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") { - return record_date.and_hms_opt(0, 0, 0).unwrap() >= cutoff; - } - } - } - false - }) - .cloned() - .collect(); - - Ok(filtered) - } -} - -/// Handle token-related commands -pub async fn handle_tokens(command: TokenCommands) -> Result<()> { - match command { - TokenCommands::Summary { period, claude_dir, details, format, mode } => { - handle_summary( - period.unwrap_or_else(|| "week".to_string()), - claude_dir, - details, - format.unwrap_or_else(|| "table".to_string()), - mode.unwrap_or_else(|| "auto".to_string()) - ).await - } - TokenCommands::Daily { days, claude_dir } => { - handle_daily(days.unwrap_or(7), claude_dir).await - } - TokenCommands::Status { claude_dir } => { - handle_status(claude_dir).await - } - TokenCommands::Analyze { file } => { - handle_analyze_file(file).await - } - TokenCommands::Report { days } => { - handle_duckdb_report(days.unwrap_or(7)).await - } - TokenCommands::Cost { month } => { - handle_duckdb_cost(month).await - } - TokenCommands::Projects { period, claude_dir, mode, details, top } => { - handle_projects( - period.unwrap_or_else(|| "week".to_string()), - claude_dir, - mode.unwrap_or_else(|| "calculate".to_string()), - details, - top.unwrap_or(10) - ).await - } - } -} - -/// Handle summary command -async fn handle_summary( - period: String, - claude_dir: Option, - details: bool, - format: String, - mode: String, -) -> Result<()> { - let analyzer = TokenAnalyzer::new(); - let cost_mode = CostMode::from(mode.as_str()); - - // Find Claude data directory - let data_dir = claude_dir.or_else(|| TokenAnalyzer::find_claude_data_dir()) - .ok_or_else(|| anyhow!("Claude Code data directory not found"))?; - - println!("Loading data from: {}", data_dir.display()); - println!("Cost calculation mode: {:?}", cost_mode); - - // Parse records - let all_records = analyzer.parse_jsonl_files(&data_dir)?; - if all_records.is_empty() { - println!("No token usage data found"); - return Ok(()); - } - - println!("Debug: Found {} total records", all_records.len()); - if let Some(latest) = all_records.iter().filter_map(|r| r.timestamp.as_ref()).max() { - println!("Debug: Latest timestamp: {}", latest); - } - - // Filter by period - let filtered_records = analyzer.filter_by_period(&all_records, &period)?; - if filtered_records.is_empty() { - println!("No data found for period: {}", period); - return Ok(()); - } - - // Calculate summary with specified mode - let summary = analyzer.calculate_costs_with_mode(&filtered_records, cost_mode); - - // Output results - match format.as_str() { - "json" => { - println!("{}", serde_json::to_string_pretty(&summary)?); - } - "table" | _ => { - print_summary_table_with_mode(&summary, &period, details, &cost_mode); - } - } - - Ok(()) -} - -/// Handle daily command -async fn handle_daily(days: u32, claude_dir: Option) -> Result<()> { - let analyzer = TokenAnalyzer::new(); - - // Find Claude data directory - let data_dir = claude_dir.or_else(|| TokenAnalyzer::find_claude_data_dir()) - .ok_or_else(|| anyhow!("Claude Code data directory not found"))?; - - println!("Loading data from: {}", data_dir.display()); - - // Parse records - let records = analyzer.parse_jsonl_files(&data_dir)?; - if records.is_empty() { - println!("No token usage data found"); - return Ok(()); - } - - // Generate daily breakdown - let breakdown = analyzer.daily_breakdown(&records)?; - let limited_breakdown: Vec<_> = breakdown.into_iter().take(days as usize).collect(); - - // Print daily breakdown - print_daily_breakdown(&limited_breakdown); - - Ok(()) -} - -/// Handle status command -async fn handle_status(claude_dir: Option) -> Result<()> { - let analyzer = TokenAnalyzer::new(); - - // Find Claude data directory - let data_dir = claude_dir.or_else(|| TokenAnalyzer::find_claude_data_dir()); - - match data_dir { - Some(dir) => { - println!("Claude Code data directory: {}", dir.display()); - - // Parse records to get basic stats - let records = analyzer.parse_jsonl_files(&dir)?; - let summary = analyzer.calculate_costs(&records); - - println!("Total records: {}", summary.record_count); - println!("Total tokens: {}", summary.total_tokens); - println!("Estimated total cost: ${:.4} USD (¥{:.0} JPY)", - summary.total_cost_usd, summary.total_cost_jpy); - } - None => { - println!("Claude Code data directory not found"); - println!("Checked locations:"); - println!(" - ~/.claude"); - println!(" - ~/.config/claude"); - println!(" - ./.claude"); - } - } - - Ok(()) -} - -/// Handle projects command -async fn handle_projects( - period: String, - claude_dir: Option, - mode: String, - details: bool, - top: u32, -) -> Result<()> { - let analyzer = TokenAnalyzer::new(); - let cost_mode = CostMode::from(mode.as_str()); - - // Find Claude data directory - let data_dir = claude_dir.or_else(|| TokenAnalyzer::find_claude_data_dir()) - .ok_or_else(|| anyhow!("Claude Code data directory not found"))?; - - println!("Loading data from: {}", data_dir.display()); - println!("Cost calculation mode: {:?}", cost_mode); - - // Parse records - let all_records = analyzer.parse_jsonl_files(&data_dir)?; - if all_records.is_empty() { - println!("No token usage data found"); - return Ok(()); - } - - println!("Debug: Found {} total records", all_records.len()); - - // Filter by period - let filtered_records = analyzer.filter_by_period(&all_records, &period)?; - if filtered_records.is_empty() { - println!("No data found for period: {}", period); - return Ok(()); - } - - // Generate project breakdown - let project_breakdown = analyzer.project_breakdown_with_mode(&filtered_records, cost_mode)?; - let limited_breakdown: Vec<_> = project_breakdown.into_iter().take(top as usize).collect(); - - // Print project breakdown - print_project_breakdown(&limited_breakdown, &period, details, &cost_mode); - - Ok(()) -} - -/// Print project breakdown -fn print_project_breakdown(breakdown: &[ProjectBreakdown], period: &str, details: bool, mode: &CostMode) { - println!("\n=== Claude Code Token Usage by Project ({}) ===", period); - println!(); - - for (i, project) in breakdown.iter().enumerate() { - println!("{}. 📁 {} ({})", - i + 1, - project.project_name, - if project.project_path.len() > 50 { - format!("...{}", &project.project_path[project.project_path.len()-47..]) - } else { - project.project_path.clone() - } - ); - - println!(" 📊 Tokens: {} total", format_number(project.summary.total_tokens)); - - if details { - println!(" • Input: {:>12}", format_number(project.summary.input_tokens)); - println!(" • Output: {:>12}", format_number(project.summary.output_tokens)); - println!(" • Cache create: {:>12}", format_number(project.summary.cache_creation_tokens)); - println!(" • Cache read: {:>12}", format_number(project.summary.cache_read_tokens)); - } - - println!(" 💰 Cost: ${:.4} USD (¥{:.0} JPY)", - project.summary.total_cost_usd, - project.summary.total_cost_jpy); - - if details { - println!(" • Records: {}", project.summary.record_count); - } - - println!(); - } - - if breakdown.len() > 1 { - let total_tokens: u64 = breakdown.iter().map(|p| p.summary.total_tokens).sum(); - let total_cost: f64 = breakdown.iter().map(|p| p.summary.total_cost_usd).sum(); - - println!("📈 Summary:"); - println!(" Total tokens: {}", format_number(total_tokens)); - println!(" Total cost: ${:.4} USD (¥{:.0} JPY)", total_cost, total_cost * 150.0); - println!(" Projects shown: {}", breakdown.len()); - println!(); - } - - println!("💡 Cost calculation (Mode: {:?})", mode); -} - -/// Print summary table -fn print_summary_table(summary: &CostSummary, period: &str, details: bool) { - print_summary_table_with_mode(summary, period, details, &CostMode::Auto) -} - -/// Print summary table with cost mode information -fn print_summary_table_with_mode(summary: &CostSummary, period: &str, details: bool, mode: &CostMode) { - println!("\n=== Claude Code Token Usage Summary ({}) ===", period); - println!(); - - println!("📊 Token Usage:"); - println!(" Input tokens: {:>12}", format_number(summary.input_tokens)); - println!(" Output tokens: {:>12}", format_number(summary.output_tokens)); - println!(" Cache creation: {:>12}", format_number(summary.cache_creation_tokens)); - println!(" Cache read: {:>12}", format_number(summary.cache_read_tokens)); - println!(" Total tokens: {:>12}", format_number(summary.total_tokens)); - println!(); - - println!("💰 Cost Estimation:"); - println!(" Input cost: {:>12}", format!("${:.4} USD", summary.input_cost_usd)); - println!(" Output cost: {:>12}", format!("${:.4} USD", summary.output_cost_usd)); - println!(" Cache cost: {:>12}", format!("${:.4} USD", summary.cache_cost_usd)); - println!(" Total cost: {:>12}", format!("${:.4} USD", summary.total_cost_usd)); - println!(" Total cost: {:>12}", format!("¥{:.0} JPY", summary.total_cost_jpy)); - println!(); - - if details { - println!("📈 Additional Details:"); - println!(" Records: {:>12}", format_number(summary.record_count as u64)); - println!(" Avg per record:{:>12}", format!("${:.4} USD", - if summary.record_count > 0 { summary.total_cost_usd / summary.record_count as f64 } else { 0.0 })); - println!(); - } - - println!("💡 Cost calculation (Mode: {:?}):", mode); - match mode { - CostMode::Display => { - println!(" Using pre-calculated costUSD values only"); - println!(" Missing costs shown as $0.00"); - } - CostMode::Calculate => { - println!(" Input: $3.00 per 1M tokens"); - println!(" Output: $15.00 per 1M tokens"); - println!(" Ignoring pre-calculated costUSD values"); - } - CostMode::Auto => { - println!(" Input: $3.00 per 1M tokens"); - println!(" Output: $15.00 per 1M tokens"); - println!(" Using costUSD when available, tokens otherwise"); - } - } - println!(" USD to JPY: 150.0"); -} - -/// Print daily breakdown -fn print_daily_breakdown(breakdown: &[DailyBreakdown]) { - println!("\n=== Daily Token Usage Breakdown ==="); - println!(); - - for daily in breakdown { - println!("📅 {} (Records: {})", daily.date, daily.summary.record_count); - println!(" Tokens: {} input + {} output = {} total", - format_number(daily.summary.input_tokens), - format_number(daily.summary.output_tokens), - format_number(daily.summary.total_tokens)); - println!(" Cost: ${:.4} USD (¥{:.0} JPY)", - daily.summary.total_cost_usd, - daily.summary.total_cost_jpy); - println!(); - } -} - -/// Format large numbers with commas -fn format_number(n: u64) -> String { - let s = n.to_string(); - let mut result = String::new(); - for (i, c) in s.chars().rev().enumerate() { - if i > 0 && i % 3 == 0 { - result.push(','); - } - result.push(c); - } - result.chars().rev().collect() -} - -/// Handle DuckDB-based token report (daily breakdown) -async fn handle_duckdb_report(days: u32) -> Result<()> { - if !check_duckdb_available() { - print_duckdb_help(); - return Err(anyhow!("DuckDB is not available")); - } - - let claude_dir = TokenAnalyzer::find_claude_data_dir() - .ok_or_else(|| anyhow!("Claude Code data directory not found"))?; - - println!("\x1b[1;34m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m"); - println!("\x1b[1;36m Claude Code トークン使用状況レポート \x1b[0m"); - println!("\x1b[1;34m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m"); - println!(); - - let duckdb_query = format!(r#" -SELECT - 日付, - 入力トークン, - 出力トークン, - 合計トークン, - 料金 -FROM ( - SELECT - strftime(DATE(timestamp::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Tokyo'), '%Y年%m月%d日') AS 日付, - LPAD(FORMAT('{{:,}}', SUM(CAST(message -> 'usage' ->> 'input_tokens' AS INTEGER))), 12, ' ') AS 入力トークン, - LPAD(FORMAT('{{:,}}', SUM(CAST(message -> 'usage' ->> 'output_tokens' AS INTEGER))), 12, ' ') AS 出力トークン, - LPAD(FORMAT('{{:,}}', SUM(CAST(message -> 'usage' ->> 'input_tokens' AS INTEGER) + CAST(message -> 'usage' ->> 'output_tokens' AS INTEGER))), 12, ' ') AS 合計トークン, - LPAD(FORMAT('¥{{:,}}', CAST(ROUND(SUM(costUSD) * 150, 0) AS INTEGER)), 10, ' ') AS 料金, - DATE(timestamp::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Tokyo') as sort_date - FROM read_json('{}/**/*.jsonl') - WHERE timestamp IS NOT NULL - GROUP BY DATE(timestamp::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Tokyo') - - UNION ALL - - SELECT - '────────────────' AS 日付, - '────────────' AS 入力トークン, - '────────────' AS 出力トークン, - '────────────' AS 合計トークン, - '──────────' AS 料金, - '9999-12-30' as sort_date - - UNION ALL - - SELECT - '【合計】' AS 日付, - LPAD(FORMAT('{{:,}}', SUM(CAST(message -> 'usage' ->> 'input_tokens' AS INTEGER))), 12, ' ') AS 入力トークン, - LPAD(FORMAT('{{:,}}', SUM(CAST(message -> 'usage' ->> 'output_tokens' AS INTEGER))), 12, ' ') AS 出力トークン, - LPAD(FORMAT('{{:,}}', SUM(CAST(message -> 'usage' ->> 'input_tokens' AS INTEGER) + CAST(message -> 'usage' ->> 'output_tokens' AS INTEGER))), 12, ' ') AS 合計トークン, - LPAD(FORMAT('¥{{:,}}', CAST(ROUND(SUM(costUSD) * 150, 0) AS INTEGER)), 10, ' ') AS 料金, - '9999-12-31' as sort_date - FROM read_json('{}/**/*.jsonl') - WHERE timestamp IS NOT NULL -) -ORDER BY sort_date DESC NULLS LAST -LIMIT {}; -"#, claude_dir.display(), claude_dir.display(), days + 2); - - let output = Command::new("duckdb") - .arg("-c") - .arg(&duckdb_query) - .output()?; - - if output.status.success() { - println!("{}", String::from_utf8_lossy(&output.stdout)); - } else { - println!("Error running DuckDB query:"); - println!("{}", String::from_utf8_lossy(&output.stderr)); - } - - println!("\x1b[1;34m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m"); - - Ok(()) -} - -/// Handle DuckDB-based cost analysis (session breakdown) -async fn handle_duckdb_cost(month: Option) -> Result<()> { - if !check_duckdb_available() { - print_duckdb_help(); - return Err(anyhow!("DuckDB is not available")); - } - - let claude_dir = TokenAnalyzer::find_claude_data_dir() - .ok_or_else(|| anyhow!("Claude Code data directory not found"))?; - - let date_filter = match month.as_deref() { - Some("today") => "CURRENT_DATE".to_string(), - Some(date) if date.contains('-') => format!("'{}'", date), - Some("current") | None => "CURRENT_DATE".to_string(), - Some(month_str) => { - // Try to parse as YYYY-MM format - if month_str.len() == 7 && month_str.contains('-') { - format!("'{}-01'", month_str) - } else { - "CURRENT_DATE".to_string() - } - } - }; - - println!("\x1b[1;34m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m"); - println!("\x1b[1;36m Claude Code 本日のセッション一覧 \x1b[0m"); - println!("\x1b[1;34m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m"); - println!(); - - let duckdb_query = format!(r#" -WITH session_stats AS ( - SELECT - sessionId, - MIN(timestamp)::TIMESTAMP as session_start, - MAX(timestamp)::TIMESTAMP as session_end, - COUNT(DISTINCT CASE WHEN type = 'user' THEN uuid END) as user_messages, - COUNT(DISTINCT CASE WHEN type = 'assistant' THEN uuid END) as assistant_messages, - SUM(CASE WHEN type = 'assistant' AND json_extract(message, '$.usage.input_tokens') IS NOT NULL THEN CAST(json_extract(message, '$.usage.input_tokens') AS INTEGER) ELSE 0 END) as total_input_tokens, - SUM(CASE WHEN type = 'assistant' AND json_extract(message, '$.usage.output_tokens') IS NOT NULL THEN CAST(json_extract(message, '$.usage.output_tokens') AS INTEGER) ELSE 0 END) as total_output_tokens, - SUM(CASE WHEN type = 'assistant' AND costUSD IS NOT NULL THEN costUSD ELSE 0 END) as total_cost - FROM read_json('{}/**/*.jsonl') - WHERE type IN ('user', 'assistant') - AND sessionId IS NOT NULL - GROUP BY sessionId -), -today_sessions AS ( - SELECT * FROM session_stats s - WHERE DATE(s.session_start AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Tokyo') = {} -) -SELECT - ID, - 開始時刻, - 時間, - メッセージ数, - 料金, - 概要 -FROM ( - SELECT - SUBSTR(CAST(s.sessionId AS VARCHAR), 1, 8) || '...' as ID, - STRFTIME((s.session_start AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Tokyo'), '%m/%d %H:%M') as 開始時刻, - LPAD(CAST(ROUND(EXTRACT(EPOCH FROM (s.session_end - s.session_start)) / 60, 0) AS INTEGER) || '分', 5, ' ') as 時間, - LPAD(CAST(s.user_messages AS VARCHAR), 6, ' ') as メッセージ数, - LPAD(FORMAT('¥{{:,}}', CAST(ROUND(s.total_cost * 150, 0) AS INTEGER)), 8, ' ') as 料金, - '本日のセッション' as 概要, - s.session_start as sort_key - FROM today_sessions s - - UNION ALL - - SELECT - '────────────' as ID, - '────────────' as 開始時刻, - '─────' as 時間, - '──────' as メッセージ数, - '────────' as 料金, - '────────────────────────────────────────────────────────────' as 概要, - '9999-12-31'::TIMESTAMP as sort_key - - UNION ALL - - SELECT - '【合計】' as ID, - CAST(COUNT(*) AS VARCHAR) || '件' as 開始時刻, - LPAD(CAST(SUM(ROUND(EXTRACT(EPOCH FROM (session_end - session_start)) / 60, 0)) AS INTEGER) || '分', 5, ' ') as 時間, - LPAD(CAST(SUM(user_messages) AS VARCHAR), 6, ' ') as メッセージ数, - LPAD(FORMAT('¥{{:,}}', CAST(ROUND(SUM(total_cost) * 150, 0) AS INTEGER)), 8, ' ') as 料金, - '本日の合計' as 概要, - '9999-12-31'::TIMESTAMP as sort_key - FROM today_sessions -) -ORDER BY sort_key DESC; -"#, claude_dir.display(), date_filter); - - let output = Command::new("duckdb") - .arg("-c") - .arg(&duckdb_query) - .output()?; - - if output.status.success() { - println!("{}", String::from_utf8_lossy(&output.stdout)); - } else { - println!("Error running DuckDB query:"); - println!("{}", String::from_utf8_lossy(&output.stderr)); - } - - println!("\x1b[1;34m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m"); - - Ok(()) -} - -/// Handle analyze file command -async fn handle_analyze_file(file: PathBuf) -> Result<()> { - let analyzer = TokenAnalyzer::new(); - - if !file.exists() { - return Err(anyhow!("File does not exist: {}", file.display())); - } - - println!("Analyzing file: {}", file.display()); - - let records = analyzer.parse_jsonl_file(&file)?; - if records.is_empty() { - println!("No token usage records found in file"); - return Ok(()); - } - - let summary = analyzer.calculate_costs(&records); - print_summary_table(&summary, &format!("File: {}", file.file_name().unwrap_or_default().to_string_lossy()), true); - - Ok(()) -} - -/// Check if DuckDB is available on the system -fn check_duckdb_available() -> bool { - Command::new("duckdb") - .arg("--version") - .output() - .map(|output| output.status.success()) - .unwrap_or(false) -} - -/// Print DuckDB installation help -pub fn print_duckdb_help() { - println!("\n🦆 DuckDB is required for advanced token analysis features!"); - println!(); - println!("📦 Installation:"); - println!(" macOS: brew install duckdb"); - println!(" Windows: Download from https://duckdb.org/docs/installation/"); - println!(" Linux: apt install duckdb (or download from website)"); - println!(); - println!("🚀 After installation, try:"); - println!(" aigpt tokens report --days 7"); - println!(" aigpt tokens cost --month today"); - println!(); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cost_calculation() { - let analyzer = TokenAnalyzer::new(); - let records = vec![ - TokenRecord { - timestamp: "2024-01-01T10:00:00Z".to_string(), - usage: Some(TokenUsage { - input_tokens: Some(1000), - output_tokens: Some(500), - total_tokens: Some(1500), - }), - model: Some("claude-3".to_string()), - conversation_id: Some("test".to_string()), - }, - ]; - - let summary = analyzer.calculate_costs(&records); - assert_eq!(summary.input_tokens, 1000); - assert_eq!(summary.output_tokens, 500); - assert_eq!(summary.total_tokens, 1500); - assert_eq!(summary.record_count, 1); - } - - #[test] - fn test_date_extraction() { - let analyzer = TokenAnalyzer::new(); - let result = analyzer.extract_date_jst("2024-01-01T10:00:00Z"); - assert!(result.is_ok()); - // Note: The exact date depends on JST conversion - } -} \ No newline at end of file diff --git a/src/transmission.rs b/src/transmission.rs deleted file mode 100644 index 2a7b5c5..0000000 --- a/src/transmission.rs +++ /dev/null @@ -1,423 +0,0 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use anyhow::{Result, Context}; -use chrono::{DateTime, Utc}; - -use crate::config::Config; -use crate::persona::Persona; -use crate::relationship::{Relationship, RelationshipStatus}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TransmissionLog { - pub user_id: String, - pub message: String, - pub timestamp: DateTime, - pub transmission_type: TransmissionType, - pub success: bool, - pub error: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum TransmissionType { - Autonomous, // AI decided to send - Scheduled, // Time-based trigger - Breakthrough, // Fortune breakthrough triggered - Maintenance, // Daily maintenance message -} - -impl std::fmt::Display for TransmissionType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TransmissionType::Autonomous => write!(f, "autonomous"), - TransmissionType::Scheduled => write!(f, "scheduled"), - TransmissionType::Breakthrough => write!(f, "breakthrough"), - TransmissionType::Maintenance => write!(f, "maintenance"), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TransmissionController { - config: Config, - transmission_history: Vec, - last_check: Option>, -} - -impl TransmissionController { - pub fn new(config: Config) -> Result { - let transmission_history = Self::load_transmission_history(&config)?; - - Ok(TransmissionController { - config, - transmission_history, - last_check: None, - }) - } - - pub async fn check_autonomous_transmissions(&mut self, persona: &mut Persona) -> Result> { - let mut transmissions = Vec::new(); - let now = Utc::now(); - - // Get all transmission-eligible relationships - let eligible_user_ids: Vec = { - let relationships = persona.list_all_relationships(); - relationships.iter() - .filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken) - .filter(|(_, rel)| rel.score >= rel.threshold) - .map(|(id, _)| id.clone()) - .collect() - }; - - for user_id in eligible_user_ids { - // Get fresh relationship data for each check - if let Some(relationship) = persona.get_relationship(&user_id) { - // Check if enough time has passed since last transmission - if let Some(last_transmission) = relationship.last_transmission { - let hours_since_last = (now - last_transmission).num_hours(); - if hours_since_last < 24 { - continue; // Skip if transmitted in last 24 hours - } - } - - // Check if conditions are met for autonomous transmission - if self.should_transmit_to_user(&user_id, relationship, persona)? { - let transmission = self.generate_autonomous_transmission(persona, &user_id).await?; - transmissions.push(transmission); - } - } - } - - self.last_check = Some(now); - self.save_transmission_history()?; - - Ok(transmissions) - } - - pub async fn check_breakthrough_transmissions(&mut self, persona: &mut Persona) -> Result> { - let mut transmissions = Vec::new(); - let state = persona.get_current_state()?; - - // Only trigger breakthrough transmissions if fortune is very high - if !state.breakthrough_triggered || state.fortune_value < 9 { - return Ok(transmissions); - } - - // Get close relationships for breakthrough sharing - let relationships = persona.list_all_relationships(); - let close_friends: Vec<_> = relationships.iter() - .filter(|(_, rel)| matches!(rel.status, RelationshipStatus::Friend | RelationshipStatus::CloseFriend)) - .filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken) - .collect(); - - for (user_id, _relationship) in close_friends { - // Check if we haven't sent a breakthrough message today - let today = chrono::Utc::now().date_naive(); - let already_sent_today = self.transmission_history.iter() - .any(|log| { - log.user_id == *user_id && - matches!(log.transmission_type, TransmissionType::Breakthrough) && - log.timestamp.date_naive() == today - }); - - if !already_sent_today { - let transmission = self.generate_breakthrough_transmission(persona, user_id).await?; - transmissions.push(transmission); - } - } - - Ok(transmissions) - } - - pub async fn check_maintenance_transmissions(&mut self, persona: &mut Persona) -> Result> { - let mut transmissions = Vec::new(); - let now = Utc::now(); - - // Only send maintenance messages once per day - let today = now.date_naive(); - let already_sent_today = self.transmission_history.iter() - .any(|log| { - matches!(log.transmission_type, TransmissionType::Maintenance) && - log.timestamp.date_naive() == today - }); - - if already_sent_today { - return Ok(transmissions); - } - - // Apply daily maintenance to persona - persona.daily_maintenance()?; - - // Get relationships that might need a maintenance check-in - let relationships = persona.list_all_relationships(); - let maintenance_candidates: Vec<_> = relationships.iter() - .filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken) - .filter(|(_, rel)| { - // Send maintenance to relationships that haven't been contacted in a while - if let Some(last_interaction) = rel.last_interaction { - let days_since = (now - last_interaction).num_days(); - days_since >= 7 // Haven't talked in a week - } else { - false - } - }) - .take(3) // Limit to 3 maintenance messages per day - .collect(); - - for (user_id, _) in maintenance_candidates { - let transmission = self.generate_maintenance_transmission(persona, user_id).await?; - transmissions.push(transmission); - } - - Ok(transmissions) - } - - fn should_transmit_to_user(&self, user_id: &str, relationship: &Relationship, persona: &Persona) -> Result { - // Basic transmission criteria - if !relationship.transmission_enabled || relationship.is_broken { - return Ok(false); - } - - // Score must be above threshold - if relationship.score < relationship.threshold { - return Ok(false); - } - - // Check transmission cooldown - if let Some(last_transmission) = relationship.last_transmission { - let hours_since = (Utc::now() - last_transmission).num_hours(); - if hours_since < 24 { - return Ok(false); - } - } - - // Calculate transmission probability based on relationship strength - let base_probability = match relationship.status { - RelationshipStatus::New => 0.1, - RelationshipStatus::Acquaintance => 0.2, - RelationshipStatus::Friend => 0.4, - RelationshipStatus::CloseFriend => 0.6, - RelationshipStatus::Broken => 0.0, - }; - - // Modify probability based on fortune - let state = persona.get_current_state()?; - let fortune_modifier = (state.fortune_value as f64 - 5.0) / 10.0; // -0.4 to +0.5 - let final_probability = (base_probability + fortune_modifier).max(0.0).min(1.0); - - // Simple random check (in real implementation, this would be more sophisticated) - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher = DefaultHasher::new(); - user_id.hash(&mut hasher); - Utc::now().timestamp().hash(&mut hasher); - let hash = hasher.finish(); - let random_value = (hash % 100) as f64 / 100.0; - - Ok(random_value < final_probability) - } - - async fn generate_autonomous_transmission(&mut self, persona: &mut Persona, user_id: &str) -> Result { - let now = Utc::now(); - - // Get recent memories for context - let memories = persona.get_memories(user_id, 3); - let context = if !memories.is_empty() { - format!("Based on our recent conversations: {}", memories.join(", ")) - } else { - "Starting a spontaneous conversation".to_string() - }; - - // Generate message using AI if available - let message = match self.generate_ai_message(persona, user_id, &context, TransmissionType::Autonomous).await { - Ok(msg) => msg, - Err(_) => { - // Fallback to simple messages - let fallback_messages = [ - "Hey! How have you been?", - "Just thinking about our last conversation...", - "Hope you're having a good day!", - "Something interesting happened today and it reminded me of you.", - ]; - let index = (now.timestamp() as usize) % fallback_messages.len(); - fallback_messages[index].to_string() - } - }; - - let log = TransmissionLog { - user_id: user_id.to_string(), - message, - timestamp: now, - transmission_type: TransmissionType::Autonomous, - success: true, // For now, assume success - error: None, - }; - - self.transmission_history.push(log.clone()); - Ok(log) - } - - async fn generate_breakthrough_transmission(&mut self, persona: &mut Persona, user_id: &str) -> Result { - let now = Utc::now(); - let state = persona.get_current_state()?; - - let message = match self.generate_ai_message(persona, user_id, "Breakthrough moment - feeling inspired!", TransmissionType::Breakthrough).await { - Ok(msg) => msg, - Err(_) => { - format!("Amazing day today! ⚡ Fortune is at {}/10 and I'm feeling incredibly inspired. Had to share this energy with you!", state.fortune_value) - } - }; - - let log = TransmissionLog { - user_id: user_id.to_string(), - message, - timestamp: now, - transmission_type: TransmissionType::Breakthrough, - success: true, - error: None, - }; - - self.transmission_history.push(log.clone()); - Ok(log) - } - - async fn generate_maintenance_transmission(&mut self, persona: &mut Persona, user_id: &str) -> Result { - let now = Utc::now(); - - let message = match self.generate_ai_message(persona, user_id, "Maintenance check-in", TransmissionType::Maintenance).await { - Ok(msg) => msg, - Err(_) => { - "Hey! It's been a while since we last talked. Just checking in to see how you're doing!".to_string() - } - }; - - let log = TransmissionLog { - user_id: user_id.to_string(), - message, - timestamp: now, - transmission_type: TransmissionType::Maintenance, - success: true, - error: None, - }; - - self.transmission_history.push(log.clone()); - Ok(log) - } - - async fn generate_ai_message(&self, _persona: &mut Persona, _user_id: &str, context: &str, transmission_type: TransmissionType) -> Result { - // Try to use AI for message generation - let _system_prompt = format!( - "You are initiating a {} conversation. Context: {}. Keep the message casual, personal, and under 100 characters. Show genuine interest in the person.", - transmission_type, context - ); - - // This is a simplified version - in a real implementation, we'd use the AI provider - // For now, return an error to trigger fallback - Err(anyhow::anyhow!("AI provider not available for transmission generation")) - } - - fn get_eligible_relationships(&self, persona: &Persona) -> Vec { - persona.list_all_relationships().iter() - .filter(|(_, rel)| rel.transmission_enabled && !rel.is_broken) - .filter(|(_, rel)| rel.score >= rel.threshold) - .map(|(id, _)| id.clone()) - .collect() - } - - pub fn get_transmission_stats(&self) -> TransmissionStats { - let total_transmissions = self.transmission_history.len(); - let successful_transmissions = self.transmission_history.iter() - .filter(|log| log.success) - .count(); - - let today = Utc::now().date_naive(); - let today_transmissions = self.transmission_history.iter() - .filter(|log| log.timestamp.date_naive() == today) - .count(); - - let by_type = { - let mut counts = HashMap::new(); - for log in &self.transmission_history { - *counts.entry(log.transmission_type.to_string()).or_insert(0) += 1; - } - counts - }; - - TransmissionStats { - total_transmissions, - successful_transmissions, - today_transmissions, - success_rate: if total_transmissions > 0 { - successful_transmissions as f64 / total_transmissions as f64 - } else { - 0.0 - }, - by_type, - } - } - - pub fn get_recent_transmissions(&self, limit: usize) -> Vec<&TransmissionLog> { - let mut logs: Vec<_> = self.transmission_history.iter().collect(); - logs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - logs.into_iter().take(limit).collect() - } - - fn load_transmission_history(config: &Config) -> Result> { - let file_path = config.transmission_file(); - if !file_path.exists() { - return Ok(Vec::new()); - } - - let content = std::fs::read_to_string(file_path) - .context("Failed to read transmission history file")?; - - let history: Vec = serde_json::from_str(&content) - .context("Failed to parse transmission history file")?; - - Ok(history) - } - - fn save_transmission_history(&self) -> Result<()> { - let content = serde_json::to_string_pretty(&self.transmission_history) - .context("Failed to serialize transmission history")?; - - std::fs::write(&self.config.transmission_file(), content) - .context("Failed to write transmission history file")?; - - Ok(()) - } - - pub async fn check_and_send(&mut self) -> Result> { - let config = self.config.clone(); - let mut persona = Persona::new(&config)?; - - let mut results = Vec::new(); - - // Check autonomous transmissions - let autonomous = self.check_autonomous_transmissions(&mut persona).await?; - for log in autonomous { - if log.success { - results.push((log.user_id, log.message)); - } - } - - // Check breakthrough transmissions - let breakthrough = self.check_breakthrough_transmissions(&mut persona).await?; - for log in breakthrough { - if log.success { - results.push((log.user_id, log.message)); - } - } - - Ok(results) - } -} - -#[derive(Debug, Clone)] -pub struct TransmissionStats { - pub total_transmissions: usize, - pub successful_transmissions: usize, - pub today_transmissions: usize, - pub success_rate: f64, - pub by_type: HashMap, -} \ No newline at end of file