fix cli
This commit is contained in:
parent
582b983a32
commit
ed6d6e0d47
@ -1,10 +1,18 @@
|
||||
[package]
|
||||
name = "aigpt-rs"
|
||||
name = "aigpt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "AI.GPT - Autonomous transmission AI with unique personality (Rust implementation)"
|
||||
authors = ["syui"]
|
||||
|
||||
[[bin]]
|
||||
name = "aigpt"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "test-config"
|
||||
path = "src/bin/test_config.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@ -18,3 +26,8 @@ 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"
|
108
TOML_MIGRATION.md
Normal file
108
TOML_MIGRATION.md
Normal file
@ -0,0 +1,108 @@
|
||||
# TOML Configuration Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The ai.gpt Rust implementation (`aigpt-rs`) now uses TOML format for configuration instead of JSON. This provides better readability and is more idiomatic for Rust applications.
|
||||
|
||||
## Configuration Location
|
||||
|
||||
The configuration file is stored at:
|
||||
- **macOS**: `~/Library/Application Support/syui/ai/gpt/config.toml`
|
||||
- **Linux**: `~/.config/syui/ai/gpt/config.toml`
|
||||
- **Windows**: `%APPDATA%\syui\ai\gpt\config.toml`
|
||||
|
||||
## Automatic Migration
|
||||
|
||||
When you run the Rust implementation for the first time, it will automatically:
|
||||
|
||||
1. Check if `config.toml` exists
|
||||
2. If not, look for `config.json` in various locations:
|
||||
- `../config.json` (relative to aigpt-rs directory)
|
||||
- `config.json` (current directory)
|
||||
- `gpt/config.json` (from project root)
|
||||
- `/Users/syui/ai/ai/gpt/config.json` (absolute path)
|
||||
3. If found, automatically convert the JSON to TOML format
|
||||
4. Save the converted configuration to the appropriate location
|
||||
|
||||
## TOML Configuration Structure
|
||||
|
||||
```toml
|
||||
# Default AI provider
|
||||
default_provider = "openai"
|
||||
|
||||
# Provider configurations
|
||||
[providers.openai]
|
||||
default_model = "gpt-4o-mini"
|
||||
api_key = "your-api-key-here" # Optional, can use OPENAI_API_KEY env var
|
||||
system_prompt = """
|
||||
Multi-line system prompt
|
||||
goes here
|
||||
"""
|
||||
|
||||
[providers.ollama]
|
||||
default_model = "qwen3"
|
||||
host = "http://127.0.0.1:11434"
|
||||
|
||||
# AT Protocol configuration (optional)
|
||||
[atproto]
|
||||
host = "https://bsky.social"
|
||||
handle = "your-handle.bsky.social" # Optional
|
||||
password = "your-app-password" # Optional
|
||||
|
||||
# MCP (Model Context Protocol) configuration
|
||||
[mcp]
|
||||
enabled = true
|
||||
auto_detect = true
|
||||
|
||||
# MCP Server definitions
|
||||
[mcp.servers.ai_gpt]
|
||||
base_url = "http://localhost:8001"
|
||||
name = "ai.gpt MCP Server"
|
||||
timeout = 10.0
|
||||
|
||||
# MCP endpoints
|
||||
[mcp.servers.ai_gpt.endpoints]
|
||||
get_memories = "/get_memories"
|
||||
search_memories = "/search_memories"
|
||||
# ... other endpoints ...
|
||||
```
|
||||
|
||||
## Manual Migration
|
||||
|
||||
If automatic migration doesn't work, you can manually convert your `config.json`:
|
||||
|
||||
1. Copy the example configuration from `gpt/config.toml.example`
|
||||
2. Fill in your specific values from `config.json`
|
||||
3. Save it to the configuration location mentioned above
|
||||
|
||||
## Testing Configuration
|
||||
|
||||
To test if your configuration is working:
|
||||
|
||||
```bash
|
||||
cd gpt/aigpt-rs
|
||||
cargo run --bin test-config
|
||||
```
|
||||
|
||||
This will show:
|
||||
- Loaded configuration values
|
||||
- Available providers
|
||||
- MCP and ATProto settings
|
||||
- Configuration file path
|
||||
|
||||
## Differences from JSON
|
||||
|
||||
Key differences in TOML format:
|
||||
- Multi-line strings use triple quotes (`"""`)
|
||||
- Comments start with `#`
|
||||
- Tables (objects) use `[table.name]` syntax
|
||||
- Arrays of tables use `[[array.name]]` syntax
|
||||
- More readable for configuration files
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The Python implementation still uses JSON format. Both implementations can coexist:
|
||||
- Python: Uses `config.json`
|
||||
- Rust: Uses `config.toml` (with automatic migration from JSON)
|
||||
|
||||
The Rust implementation will only perform the migration once. After `config.toml` is created, it will use that file exclusively.
|
@ -1,428 +0,0 @@
|
||||
# AI.GPT Rust Implementation
|
||||
|
||||
**自律送信AI(Rust版)** - Autonomous transmission AI with unique personality
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 概要
|
||||
|
||||
ai.gptは、ユニークな人格を持つ自律送信AIシステムのRust実装です。Python版から完全移行され、パフォーマンスと型安全性が向上しました。
|
||||
|
||||
### 主要機能
|
||||
|
||||
- **自律人格システム**: 関係性、記憶、感情状態を管理
|
||||
- **MCP統合**: Model Context Protocolによる高度なツール統合
|
||||
- **継続的会話**: リアルタイム対話とコンテキスト管理
|
||||
- **サービス連携**: ai.card、ai.log、ai.botとの自動連携
|
||||
- **トークン分析**: Claude Codeの使用量とコスト計算
|
||||
- **スケジューラー**: 自動実行タスクとメンテナンス
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
```
|
||||
ai.gpt (Rust)
|
||||
├── 人格システム (Persona)
|
||||
│ ├── 関係性管理 (Relationships)
|
||||
│ ├── 記憶システム (Memory)
|
||||
│ └── 感情状態 (Fortune/Mood)
|
||||
├── 自律送信 (Transmission)
|
||||
│ ├── 自動送信判定
|
||||
│ ├── ブレイクスルー検出
|
||||
│ └── メンテナンス通知
|
||||
├── MCPサーバー (16+ tools)
|
||||
│ ├── 記憶管理ツール
|
||||
│ ├── シェル統合ツール
|
||||
│ └── サービス連携ツール
|
||||
├── HTTPクライアント
|
||||
│ ├── ai.card連携
|
||||
│ ├── ai.log連携
|
||||
│ └── ai.bot連携
|
||||
└── CLI (16 commands)
|
||||
├── 会話モード
|
||||
├── スケジューラー
|
||||
└── トークン分析
|
||||
```
|
||||
|
||||
## インストール
|
||||
|
||||
### 前提条件
|
||||
|
||||
- Rust 1.70+
|
||||
- SQLite または PostgreSQL
|
||||
- OpenAI API または Ollama (オプション)
|
||||
|
||||
### ビルド
|
||||
|
||||
```bash
|
||||
# リポジトリクローン
|
||||
git clone https://git.syui.ai/ai/gpt
|
||||
cd gpt/aigpt-rs
|
||||
|
||||
# リリースビルド
|
||||
cargo build --release
|
||||
|
||||
# インストール(オプション)
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
## 設定
|
||||
|
||||
設定ファイルは `~/.config/syui/ai/gpt/` に保存されます:
|
||||
|
||||
```
|
||||
~/.config/syui/ai/gpt/
|
||||
├── config.toml # メイン設定
|
||||
├── persona.json # 人格データ
|
||||
├── relationships.json # 関係性データ
|
||||
├── memories.db # 記憶データベース
|
||||
└── transmissions.json # 送信履歴
|
||||
```
|
||||
|
||||
### 基本設定例
|
||||
|
||||
```toml
|
||||
# ~/.config/syui/ai/gpt/config.toml
|
||||
[ai]
|
||||
provider = "ollama" # または "openai"
|
||||
model = "llama3"
|
||||
api_key = "your-api-key" # OpenAI使用時
|
||||
|
||||
[database]
|
||||
type = "sqlite" # または "postgresql"
|
||||
url = "memories.db"
|
||||
|
||||
[transmission]
|
||||
enabled = true
|
||||
check_interval_hours = 6
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本コマンド
|
||||
|
||||
```bash
|
||||
# AI状態確認
|
||||
aigpt-rs status
|
||||
|
||||
# 1回の対話
|
||||
aigpt-rs chat "user_did" "Hello!"
|
||||
|
||||
# 継続的会話モード(推奨)
|
||||
aigpt-rs conversation "user_did"
|
||||
aigpt-rs conv "user_did" # エイリアス
|
||||
|
||||
# 運勢確認
|
||||
aigpt-rs fortune
|
||||
|
||||
# 関係性一覧
|
||||
aigpt-rs relationships
|
||||
|
||||
# 自律送信チェック
|
||||
aigpt-rs transmit
|
||||
|
||||
# スケジューラー実行
|
||||
aigpt-rs schedule
|
||||
|
||||
# MCPサーバー起動
|
||||
aigpt-rs server --port 8080
|
||||
```
|
||||
|
||||
### 会話モード
|
||||
|
||||
継続的会話モードでは、MCPコマンドが使用できます:
|
||||
|
||||
```bash
|
||||
# 会話モード開始
|
||||
$ aigpt-rs conv did:plc:your_user_id
|
||||
|
||||
# MCPコマンド例
|
||||
/memories # 記憶を表示
|
||||
/search <query> # 記憶を検索
|
||||
/context # コンテキスト要約
|
||||
/relationship # 関係性状況
|
||||
/cards # カードコレクション
|
||||
/help # ヘルプ表示
|
||||
```
|
||||
|
||||
### トークン分析
|
||||
|
||||
Claude Codeの使用量とコスト分析:
|
||||
|
||||
```bash
|
||||
# 今日の使用量サマリー
|
||||
aigpt-rs tokens summary
|
||||
|
||||
# 過去7日間の詳細
|
||||
aigpt-rs tokens daily --days 7
|
||||
|
||||
# データ状況確認
|
||||
aigpt-rs tokens status
|
||||
```
|
||||
|
||||
## MCP統合
|
||||
|
||||
### 利用可能なツール(16+ tools)
|
||||
|
||||
#### コア機能
|
||||
- `get_status` - AI状態と関係性
|
||||
- `chat_with_ai` - AI対話
|
||||
- `get_relationships` - 関係性一覧
|
||||
- `get_memories` - 記憶取得
|
||||
|
||||
#### 高度な記憶管理
|
||||
- `get_contextual_memories` - コンテキスト記憶
|
||||
- `search_memories` - 記憶検索
|
||||
- `create_summary` - 要約作成
|
||||
- `create_core_memory` - 重要記憶作成
|
||||
|
||||
#### システム統合
|
||||
- `execute_command` - シェルコマンド実行
|
||||
- `analyze_file` - ファイル解析
|
||||
- `write_file` - ファイル書き込み
|
||||
- `list_files` - ファイル一覧
|
||||
|
||||
#### 自律機能
|
||||
- `check_transmissions` - 送信チェック
|
||||
- `run_maintenance` - メンテナンス実行
|
||||
- `run_scheduler` - スケジューラー実行
|
||||
- `get_scheduler_status` - スケジューラー状況
|
||||
|
||||
## サービス連携
|
||||
|
||||
### ai.card統合
|
||||
|
||||
```bash
|
||||
# カード統計取得
|
||||
curl http://localhost:8000/api/v1/cards/gacha-stats
|
||||
|
||||
# カード引き(会話モード内)
|
||||
/cards
|
||||
> y # カードを引く
|
||||
```
|
||||
|
||||
### ai.log統合
|
||||
|
||||
ブログ生成とドキュメント管理:
|
||||
|
||||
```bash
|
||||
# ドキュメント生成
|
||||
aigpt-rs docs generate --project ai.gpt
|
||||
|
||||
# 同期
|
||||
aigpt-rs docs sync --ai-integration
|
||||
```
|
||||
|
||||
### ai.bot統合
|
||||
|
||||
分散SNS連携(atproto):
|
||||
|
||||
```bash
|
||||
# サブモジュール管理
|
||||
aigpt-rs submodules update --all --auto-commit
|
||||
```
|
||||
|
||||
## 開発
|
||||
|
||||
### プロジェクト構造
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # エントリーポイント
|
||||
├── cli.rs # CLIハンドラー
|
||||
├── config.rs # 設定管理
|
||||
├── persona.rs # 人格システム
|
||||
├── memory.rs # 記憶管理
|
||||
├── relationship.rs # 関係性管理
|
||||
├── transmission.rs # 自律送信
|
||||
├── scheduler.rs # スケジューラー
|
||||
├── mcp_server.rs # MCPサーバー
|
||||
├── http_client.rs # HTTP通信
|
||||
├── conversation.rs # 会話モード
|
||||
├── tokens.rs # トークン分析
|
||||
├── ai_provider.rs # AI プロバイダー
|
||||
├── import.rs # データインポート
|
||||
├── docs.rs # ドキュメント管理
|
||||
├── submodules.rs # サブモジュール管理
|
||||
├── shell.rs # シェルモード
|
||||
└── status.rs # ステータス表示
|
||||
```
|
||||
|
||||
### 依存関係
|
||||
|
||||
主要な依存関係:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
colored = "2.0"
|
||||
```
|
||||
|
||||
### テスト実行
|
||||
|
||||
```bash
|
||||
# 単体テスト
|
||||
cargo test
|
||||
|
||||
# 統合テスト
|
||||
cargo test --test integration
|
||||
|
||||
# ベンチマーク
|
||||
cargo bench
|
||||
```
|
||||
|
||||
## パフォーマンス
|
||||
|
||||
### Python版との比較
|
||||
|
||||
| 機能 | Python版 | Rust版 | 改善率 |
|
||||
|------|----------|--------|--------|
|
||||
| 起動時間 | 2.1s | 0.3s | **7x faster** |
|
||||
| メモリ使用量 | 45MB | 12MB | **73% reduction** |
|
||||
| 会話応答 | 850ms | 280ms | **3x faster** |
|
||||
| MCP処理 | 1.2s | 420ms | **3x faster** |
|
||||
|
||||
### ベンチマーク結果
|
||||
|
||||
```
|
||||
Conversation Mode:
|
||||
- Cold start: 287ms
|
||||
- Warm response: 156ms
|
||||
- Memory search: 23ms
|
||||
- Context switch: 89ms
|
||||
|
||||
MCP Server:
|
||||
- Tool execution: 45ms
|
||||
- Memory retrieval: 12ms
|
||||
- Service detection: 78ms
|
||||
```
|
||||
|
||||
## セキュリティ
|
||||
|
||||
### 実装されたセキュリティ機能
|
||||
|
||||
- **コマンド実行制限**: 危険なコマンドのブラックリスト
|
||||
- **ファイルアクセス制御**: 安全なパス検証
|
||||
- **API認証**: トークンベース認証
|
||||
- **入力検証**: 全入力の厳密な検証
|
||||
|
||||
### セキュリティベストプラクティス
|
||||
|
||||
1. API キーを環境変数で管理
|
||||
2. データベース接続の暗号化
|
||||
3. ログの機密情報マスキング
|
||||
4. 定期的な依存関係更新
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
### よくある問題
|
||||
|
||||
#### 設定ファイルが見つからない
|
||||
|
||||
```bash
|
||||
# 設定ディレクトリ作成
|
||||
mkdir -p ~/.config/syui/ai/gpt
|
||||
|
||||
# 基本設定ファイル作成
|
||||
echo '[ai]
|
||||
provider = "ollama"
|
||||
model = "llama3"' > ~/.config/syui/ai/gpt/config.toml
|
||||
```
|
||||
|
||||
#### データベース接続エラー
|
||||
|
||||
```bash
|
||||
# SQLite の場合
|
||||
chmod 644 ~/.config/syui/ai/gpt/memories.db
|
||||
|
||||
# PostgreSQL の場合
|
||||
export DATABASE_URL="postgresql://user:pass@localhost/aigpt"
|
||||
```
|
||||
|
||||
#### MCPサーバー接続失敗
|
||||
|
||||
```bash
|
||||
# ポート確認
|
||||
netstat -tulpn | grep 8080
|
||||
|
||||
# ファイアウォール確認
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
### ログ分析
|
||||
|
||||
```bash
|
||||
# 詳細ログ有効化
|
||||
export RUST_LOG=debug
|
||||
aigpt-rs conversation user_id
|
||||
|
||||
# エラーログ確認
|
||||
tail -f ~/.config/syui/ai/gpt/error.log
|
||||
```
|
||||
|
||||
## ロードマップ
|
||||
|
||||
### Phase 1: Core Enhancement ✅
|
||||
- [x] Python → Rust 完全移行
|
||||
- [x] MCP サーバー統合
|
||||
- [x] パフォーマンス最適化
|
||||
|
||||
### Phase 2: Advanced Features 🚧
|
||||
- [ ] WebUI実装
|
||||
- [ ] リアルタイムストリーミング
|
||||
- [ ] 高度なRAG統合
|
||||
- [ ] マルチモーダル対応
|
||||
|
||||
### Phase 3: Ecosystem Integration 📋
|
||||
- [ ] ai.verse統合
|
||||
- [ ] ai.os統合
|
||||
- [ ] 分散アーキテクチャ
|
||||
|
||||
## コントリビューション
|
||||
|
||||
### 開発への参加
|
||||
|
||||
1. Forkしてクローン
|
||||
2. フィーチャーブランチ作成
|
||||
3. 変更をコミット
|
||||
4. プルリクエスト作成
|
||||
|
||||
### コーディング規約
|
||||
|
||||
- `cargo fmt` でフォーマット
|
||||
- `cargo clippy` でリント
|
||||
- 変更にはテストを追加
|
||||
- ドキュメントを更新
|
||||
|
||||
## ライセンス
|
||||
|
||||
MIT License - 詳細は [LICENSE](LICENSE) ファイルを参照
|
||||
|
||||
## 関連プロジェクト
|
||||
|
||||
- [ai.card](https://git.syui.ai/ai/card) - カードゲーム統合
|
||||
- [ai.log](https://git.syui.ai/ai/log) - ブログ生成システム
|
||||
- [ai.bot](https://git.syui.ai/ai/bot) - 分散SNS Bot
|
||||
- [ai.shell](https://git.syui.ai/ai/shell) - AI Shell環境
|
||||
- [ai.verse](https://git.syui.ai/ai/verse) - メタバース統合
|
||||
|
||||
## サポート
|
||||
|
||||
- **Issues**: [GitHub Issues](https://git.syui.ai/ai/gpt/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://git.syui.ai/ai/gpt/discussions)
|
||||
- **Wiki**: [Project Wiki](https://git.syui.ai/ai/gpt/wiki)
|
||||
|
||||
---
|
||||
|
||||
**ai.gpt** は [syui.ai](https://syui.ai) エコシステムの一部です。
|
||||
|
||||
生成日時: 2025-06-07 04:40:21 UTC
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
@ -1,367 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::persona::Persona;
|
||||
use crate::transmission::TransmissionController;
|
||||
use crate::scheduler::AIScheduler;
|
||||
use crate::mcp_server::MCPServer;
|
||||
|
||||
pub async fn handle_chat(
|
||||
user_id: String,
|
||||
message: String,
|
||||
data_dir: Option<PathBuf>,
|
||||
model: Option<String>,
|
||||
provider: Option<String>,
|
||||
) -> Result<()> {
|
||||
let config = Config::new(data_dir)?;
|
||||
let mut persona = Persona::new(&config)?;
|
||||
|
||||
// Try AI-powered response first, fallback to simple response
|
||||
let (response, relationship_delta) = if provider.is_some() || model.is_some() {
|
||||
// Use AI provider
|
||||
persona.process_ai_interaction(&user_id, &message, provider, model).await?
|
||||
} else {
|
||||
// Use simple response (backward compatibility)
|
||||
persona.process_interaction(&user_id, &message)?
|
||||
};
|
||||
|
||||
// Display conversation
|
||||
println!("{}: {}", "User".cyan(), message);
|
||||
println!("{}: {}", "AI".green(), 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());
|
||||
}
|
||||
}
|
||||
|
||||
// Show current relationship status
|
||||
if let Some(relationship) = persona.get_relationship(&user_id) {
|
||||
println!("\n{}: {}", "Relationship Status".cyan(), relationship.status);
|
||||
println!("Score: {:.2} / {}", relationship.score, relationship.threshold);
|
||||
println!("Transmission: {}", if relationship.transmission_enabled { "✓ Enabled".green() } else { "✗ Disabled".yellow() });
|
||||
|
||||
if relationship.is_broken {
|
||||
println!("{}", "⚠️ This relationship is broken and cannot be repaired.".red());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_fortune(data_dir: Option<PathBuf>) -> Result<()> {
|
||||
let config = Config::new(data_dir)?;
|
||||
let persona = Persona::new(&config)?;
|
||||
let state = persona.get_current_state()?;
|
||||
|
||||
// Fortune display
|
||||
let fortune_stars = "🌟".repeat(state.fortune_value as usize);
|
||||
let empty_stars = "☆".repeat((10 - state.fortune_value) as usize);
|
||||
|
||||
println!("{}", "AI Fortune".yellow().bold());
|
||||
println!("{}{}", fortune_stars, empty_stars);
|
||||
println!("Today's Fortune: {}/10", state.fortune_value);
|
||||
println!("Date: {}", chrono::Utc::now().format("%Y-%m-%d"));
|
||||
|
||||
if state.breakthrough_triggered {
|
||||
println!("\n{}", "⚡ BREAKTHROUGH! Special fortune activated!".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_relationships(data_dir: Option<PathBuf>) -> 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 yet".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", "All Relationships".cyan().bold());
|
||||
println!();
|
||||
|
||||
for (user_id, rel) in relationships {
|
||||
let transmission = if rel.is_broken {
|
||||
"💔"
|
||||
} else if rel.transmission_enabled {
|
||||
"✓"
|
||||
} else {
|
||||
"✗"
|
||||
};
|
||||
|
||||
let last_interaction = rel.last_interaction
|
||||
.map(|dt| dt.format("%Y-%m-%d").to_string())
|
||||
.unwrap_or_else(|| "Never".to_string());
|
||||
|
||||
let user_display = if user_id.len() > 16 {
|
||||
format!("{}...", &user_id[..16])
|
||||
} else {
|
||||
user_id
|
||||
};
|
||||
|
||||
println!("{:<20} {:<12} {:<8} {:<5} {}",
|
||||
user_display.cyan(),
|
||||
rel.status,
|
||||
format!("{:.2}", rel.score),
|
||||
transmission,
|
||||
last_interaction.dimmed());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_transmit(data_dir: Option<PathBuf>) -> Result<()> {
|
||||
let config = Config::new(data_dir)?;
|
||||
let mut persona = Persona::new(&config)?;
|
||||
let mut transmission_controller = TransmissionController::new(&config)?;
|
||||
|
||||
println!("{}", "🚀 Checking for autonomous transmissions...".cyan().bold());
|
||||
|
||||
// Check all types of transmissions
|
||||
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_transmissions = autonomous.len() + breakthrough.len() + maintenance.len();
|
||||
|
||||
if total_transmissions == 0 {
|
||||
println!("{}", "No transmissions needed at this time.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("\n{}", "📨 Transmission Results:".green().bold());
|
||||
|
||||
// Display autonomous transmissions
|
||||
if !autonomous.is_empty() {
|
||||
println!("\n{}", "🤖 Autonomous Transmissions:".blue());
|
||||
for transmission in autonomous {
|
||||
println!(" {} → {}", transmission.user_id.cyan(), transmission.message);
|
||||
println!(" {} {}", "Type:".dimmed(), transmission.transmission_type);
|
||||
println!(" {} {}", "Time:".dimmed(), transmission.timestamp.format("%H:%M:%S"));
|
||||
}
|
||||
}
|
||||
|
||||
// Display breakthrough transmissions
|
||||
if !breakthrough.is_empty() {
|
||||
println!("\n{}", "⚡ Breakthrough Transmissions:".yellow());
|
||||
for transmission in breakthrough {
|
||||
println!(" {} → {}", transmission.user_id.cyan(), transmission.message);
|
||||
println!(" {} {}", "Time:".dimmed(), transmission.timestamp.format("%H:%M:%S"));
|
||||
}
|
||||
}
|
||||
|
||||
// Display maintenance transmissions
|
||||
if !maintenance.is_empty() {
|
||||
println!("\n{}", "🔧 Maintenance Transmissions:".green());
|
||||
for transmission in maintenance {
|
||||
println!(" {} → {}", transmission.user_id.cyan(), transmission.message);
|
||||
println!(" {} {}", "Time:".dimmed(), transmission.timestamp.format("%H:%M:%S"));
|
||||
}
|
||||
}
|
||||
|
||||
// Show transmission stats
|
||||
let stats = transmission_controller.get_transmission_stats();
|
||||
println!("\n{}", "📊 Transmission Stats:".magenta().bold());
|
||||
println!("Total: {} | Today: {} | Success Rate: {:.1}%",
|
||||
stats.total_transmissions,
|
||||
stats.today_transmissions,
|
||||
stats.success_rate * 100.0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_maintenance(data_dir: Option<PathBuf>) -> Result<()> {
|
||||
let config = Config::new(data_dir)?;
|
||||
let mut persona = Persona::new(&config)?;
|
||||
let mut transmission_controller = TransmissionController::new(&config)?;
|
||||
|
||||
println!("{}", "🔧 Running daily maintenance...".cyan().bold());
|
||||
|
||||
// Run daily maintenance on persona (time decay, etc.)
|
||||
persona.daily_maintenance()?;
|
||||
println!("✓ {}", "Applied relationship time decay".green());
|
||||
|
||||
// Check for maintenance transmissions
|
||||
let maintenance_transmissions = transmission_controller.check_maintenance_transmissions(&mut persona).await?;
|
||||
|
||||
if maintenance_transmissions.is_empty() {
|
||||
println!("✓ {}", "No maintenance transmissions needed".green());
|
||||
} else {
|
||||
println!("📨 {}", format!("Sent {} maintenance messages:", maintenance_transmissions.len()).green());
|
||||
for transmission in maintenance_transmissions {
|
||||
println!(" {} → {}", transmission.user_id.cyan(), transmission.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show relationship stats after maintenance
|
||||
if let Some(rel_stats) = persona.get_relationship_stats() {
|
||||
println!("\n{}", "📊 Relationship Statistics:".magenta().bold());
|
||||
println!("Total: {} | Active: {} | Transmission Enabled: {} | Broken: {}",
|
||||
rel_stats.total_relationships,
|
||||
rel_stats.active_relationships,
|
||||
rel_stats.transmission_enabled,
|
||||
rel_stats.broken_relationships);
|
||||
println!("Average Score: {:.2}", rel_stats.avg_score);
|
||||
}
|
||||
|
||||
// Show transmission history
|
||||
let recent_transmissions = transmission_controller.get_recent_transmissions(5);
|
||||
if !recent_transmissions.is_empty() {
|
||||
println!("\n{}", "📝 Recent Transmissions:".blue().bold());
|
||||
for transmission in recent_transmissions {
|
||||
println!(" {} {} → {} ({})",
|
||||
transmission.timestamp.format("%m-%d %H:%M").to_string().dimmed(),
|
||||
transmission.user_id.cyan(),
|
||||
transmission.message,
|
||||
transmission.transmission_type.to_string().yellow());
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "✅ Daily maintenance completed!".green().bold());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_schedule(data_dir: Option<PathBuf>) -> Result<()> {
|
||||
let config = Config::new(data_dir)?;
|
||||
let mut persona = Persona::new(&config)?;
|
||||
let mut transmission_controller = TransmissionController::new(&config)?;
|
||||
let mut scheduler = AIScheduler::new(&config)?;
|
||||
|
||||
println!("{}", "⏰ Running scheduled tasks...".cyan().bold());
|
||||
|
||||
// Run all due scheduled tasks
|
||||
let executions = scheduler.run_scheduled_tasks(&mut persona, &mut transmission_controller).await?;
|
||||
|
||||
if executions.is_empty() {
|
||||
println!("{}", "No scheduled tasks due at this time.".yellow());
|
||||
} else {
|
||||
println!("\n{}", "📋 Task Execution Results:".green().bold());
|
||||
|
||||
for execution in &executions {
|
||||
let status_icon = if execution.success { "✅" } else { "❌" };
|
||||
let _status_color = if execution.success { "green" } else { "red" };
|
||||
|
||||
println!(" {} {} ({:.0}ms)",
|
||||
status_icon,
|
||||
execution.task_id.cyan(),
|
||||
execution.duration_ms);
|
||||
|
||||
if let Some(result) = &execution.result {
|
||||
println!(" {}", result);
|
||||
}
|
||||
|
||||
if let Some(error) = &execution.error {
|
||||
println!(" {} {}", "Error:".red(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show scheduler statistics
|
||||
let stats = scheduler.get_scheduler_stats();
|
||||
println!("\n{}", "📊 Scheduler Statistics:".magenta().bold());
|
||||
println!("Total Tasks: {} | Enabled: {} | Due: {}",
|
||||
stats.total_tasks,
|
||||
stats.enabled_tasks,
|
||||
stats.due_tasks);
|
||||
println!("Executions: {} | Today: {} | Success Rate: {:.1}%",
|
||||
stats.total_executions,
|
||||
stats.today_executions,
|
||||
stats.success_rate * 100.0);
|
||||
println!("Average Duration: {:.1}ms", stats.avg_duration_ms);
|
||||
|
||||
// Show upcoming tasks
|
||||
let tasks = scheduler.list_tasks();
|
||||
if !tasks.is_empty() {
|
||||
println!("\n{}", "📅 Upcoming Tasks:".blue().bold());
|
||||
|
||||
let mut upcoming_tasks: Vec<_> = tasks.values()
|
||||
.filter(|task| task.enabled)
|
||||
.collect();
|
||||
upcoming_tasks.sort_by_key(|task| task.next_run);
|
||||
|
||||
for task in upcoming_tasks.iter().take(5) {
|
||||
let time_until = (task.next_run - chrono::Utc::now()).num_minutes();
|
||||
let time_display = if time_until > 60 {
|
||||
format!("{}h {}m", time_until / 60, time_until % 60)
|
||||
} else if time_until > 0 {
|
||||
format!("{}m", time_until)
|
||||
} else {
|
||||
"overdue".to_string()
|
||||
};
|
||||
|
||||
println!(" {} {} ({})",
|
||||
task.next_run.format("%m-%d %H:%M").to_string().dimmed(),
|
||||
task.task_type.to_string().cyan(),
|
||||
time_display.yellow());
|
||||
}
|
||||
}
|
||||
|
||||
// Show recent execution history
|
||||
let recent_executions = scheduler.get_execution_history(Some(5));
|
||||
if !recent_executions.is_empty() {
|
||||
println!("\n{}", "📝 Recent Executions:".blue().bold());
|
||||
for execution in recent_executions {
|
||||
let status_icon = if execution.success { "✅" } else { "❌" };
|
||||
println!(" {} {} {} ({:.0}ms)",
|
||||
execution.execution_time.format("%m-%d %H:%M").to_string().dimmed(),
|
||||
status_icon,
|
||||
execution.task_id.cyan(),
|
||||
execution.duration_ms);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "⏰ Scheduler check completed!".green().bold());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_server(port: Option<u16>, data_dir: Option<PathBuf>) -> Result<()> {
|
||||
let config = Config::new(data_dir)?;
|
||||
let mut mcp_server = MCPServer::new(config)?;
|
||||
let port = port.unwrap_or(8080);
|
||||
|
||||
println!("{}", "🚀 Starting ai.gpt MCP Server...".cyan().bold());
|
||||
|
||||
// Start the MCP server
|
||||
mcp_server.start_server(port).await?;
|
||||
|
||||
// Show server info
|
||||
let tools = mcp_server.get_tools();
|
||||
println!("\n{}", "📋 Available MCP Tools:".green().bold());
|
||||
|
||||
for (i, tool) in tools.iter().enumerate() {
|
||||
println!("{}. {} - {}",
|
||||
(i + 1).to_string().cyan(),
|
||||
tool.name.green(),
|
||||
tool.description);
|
||||
}
|
||||
|
||||
println!("\n{}", "💡 Usage Examples:".blue().bold());
|
||||
println!(" • {}: Get AI status and mood", "get_status".green());
|
||||
println!(" • {}: Chat with the AI", "chat_with_ai".green());
|
||||
println!(" • {}: View all relationships", "get_relationships".green());
|
||||
println!(" • {}: Run autonomous transmissions", "check_transmissions".green());
|
||||
println!(" • {}: Execute scheduled tasks", "run_scheduler".green());
|
||||
|
||||
println!("\n{}", "🔧 Server Configuration:".magenta().bold());
|
||||
println!("Port: {}", port.to_string().yellow());
|
||||
println!("Tools: {}", tools.len().to_string().yellow());
|
||||
println!("Protocol: MCP (Model Context Protocol)");
|
||||
|
||||
println!("\n{}", "✅ MCP Server is ready to accept requests".green().bold());
|
||||
|
||||
// In a real implementation, the server would keep running here
|
||||
// For now, we just show the configuration and exit
|
||||
println!("\n{}", "ℹ️ Server simulation complete. In production, this would run continuously.".blue());
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,103 +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 {
|
||||
pub data_dir: PathBuf,
|
||||
pub default_provider: String,
|
||||
pub providers: HashMap<String, ProviderConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProviderConfig {
|
||||
pub default_model: String,
|
||||
pub host: Option<String>,
|
||||
pub api_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(data_dir: Option<PathBuf>) -> Result<Self> {
|
||||
let data_dir = data_dir.unwrap_or_else(|| {
|
||||
dirs::config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("syui")
|
||||
.join("ai")
|
||||
.join("gpt")
|
||||
});
|
||||
|
||||
// Ensure data directory exists
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.context("Failed to create data directory")?;
|
||||
|
||||
// Create default providers
|
||||
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,
|
||||
});
|
||||
|
||||
providers.insert("openai".to_string(), ProviderConfig {
|
||||
default_model: "gpt-4o-mini".to_string(),
|
||||
host: None,
|
||||
api_key: std::env::var("OPENAI_API_KEY").ok(),
|
||||
});
|
||||
|
||||
Ok(Config {
|
||||
data_dir,
|
||||
default_provider: "ollama".to_string(),
|
||||
providers,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_provider(&self, provider_name: &str) -> Option<&ProviderConfig> {
|
||||
self.providers.get(provider_name)
|
||||
}
|
||||
|
||||
pub fn get_ai_config(&self, provider: Option<String>, model: Option<String>) -> Result<AIConfig> {
|
||||
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")
|
||||
}
|
||||
}
|
2
card
2
card
@ -1 +1 @@
|
||||
Subproject commit 13723cf3d74e3d22c514b60413f790ef28ccf2aa
|
||||
Subproject commit e7948bf4cf31a9c3f02c86d1e2c548deb21523a8
|
64
config.toml.example
Normal file
64
config.toml.example
Normal file
@ -0,0 +1,64 @@
|
||||
# AI.GPT Configuration File
|
||||
# This is an example configuration file showing all available options
|
||||
|
||||
default_provider = "openai"
|
||||
|
||||
[providers.openai]
|
||||
api_key = ""
|
||||
default_model = "gpt-4o-mini"
|
||||
system_prompt = """
|
||||
あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。
|
||||
|
||||
重要:カード、コレクション、ガチャなどカード関連の質問を受けたら、必ずcard_get_user_cards、card_analyze_collection、card_draw_cardなどの適切なツールを使用してください。didパラメータには会話相手のユーザーID(例:'syui')を使用してください。
|
||||
|
||||
ブログ、記事、日記、思考などの話題が出たら、log_create_post、log_list_posts、log_build_blog、log_ai_contentなどのai.logツールを使用してください。AI記憶システムと連携して、思い出や学習内容をブログ記事として自動生成できます。
|
||||
|
||||
翻訳や多言語対応について聞かれたら、log_translate_documentツールを使用してOllama AIで翻訳ができることを教えてください。日本語から英語、英語から日本語などの翻訳が可能で、マークダウン構造も保持します。ドキュメント生成についてはlog_generate_docsツールでREADME、API、構造、変更履歴の自動生成ができます。
|
||||
"""
|
||||
|
||||
[providers.ollama]
|
||||
host = "http://127.0.0.1:11434"
|
||||
default_model = "qwen3"
|
||||
|
||||
[atproto]
|
||||
host = "https://bsky.social"
|
||||
# handle = "your-handle.bsky.social"
|
||||
# password = "your-app-password"
|
||||
|
||||
[mcp]
|
||||
enabled = true
|
||||
auto_detect = true
|
||||
|
||||
[mcp.servers.ai_gpt]
|
||||
base_url = "http://localhost:8001"
|
||||
name = "ai.gpt MCP Server"
|
||||
timeout = 10.0
|
||||
|
||||
[mcp.servers.ai_gpt.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"
|
||||
log_create_post = "/log_create_post"
|
||||
log_list_posts = "/log_list_posts"
|
||||
log_build_blog = "/log_build_blog"
|
||||
log_get_post = "/log_get_post"
|
||||
log_system_status = "/log_system_status"
|
||||
log_ai_content = "/log_ai_content"
|
||||
log_translate_document = "/log_translate_document"
|
||||
log_generate_docs = "/log_generate_docs"
|
54
src/bin/test_config.rs
Normal file
54
src/bin/test_config.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use aigpt::config::Config;
|
||||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
println!("Testing configuration loading...");
|
||||
|
||||
// Debug: check which JSON files exist
|
||||
let possible_paths = vec![
|
||||
"../config.json",
|
||||
"config.json",
|
||||
"gpt/config.json",
|
||||
"/Users/syui/ai/ai/gpt/config.json",
|
||||
];
|
||||
|
||||
println!("Checking for config.json files:");
|
||||
for path in &possible_paths {
|
||||
let path_buf = std::path::PathBuf::from(path);
|
||||
if path_buf.exists() {
|
||||
println!(" ✓ Found: {}", path);
|
||||
} else {
|
||||
println!(" ✗ Not found: {}", path);
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
let config = Config::new(None)?;
|
||||
|
||||
println!("Configuration loaded successfully!");
|
||||
println!("Default provider: {}", config.default_provider);
|
||||
println!("Available providers:");
|
||||
for (name, provider) in &config.providers {
|
||||
println!(" - {}: model={}, host={:?}",
|
||||
name,
|
||||
provider.default_model,
|
||||
provider.host);
|
||||
}
|
||||
|
||||
if let Some(mcp) = &config.mcp {
|
||||
println!("\nMCP Configuration:");
|
||||
println!(" Enabled: {}", mcp.enabled);
|
||||
println!(" Auto-detect: {}", mcp.auto_detect);
|
||||
println!(" Servers: {}", mcp.servers.len());
|
||||
}
|
||||
|
||||
if let Some(atproto) = &config.atproto {
|
||||
println!("\nATProto Configuration:");
|
||||
println!(" Host: {}", atproto.host);
|
||||
println!(" Handle: {:?}", atproto.handle);
|
||||
}
|
||||
|
||||
println!("\nConfig file path: {}", config.data_dir.join("config.json").display());
|
||||
|
||||
Ok(())
|
||||
}
|
36
src/cli/commands.rs
Normal file
36
src/cli/commands.rs
Normal file
@ -0,0 +1,36 @@
|
||||
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 = "today")]
|
||||
period: String,
|
||||
/// Claude Code data directory path
|
||||
#[arg(long)]
|
||||
claude_dir: Option<PathBuf>,
|
||||
/// Show detailed breakdown
|
||||
#[arg(long)]
|
||||
details: bool,
|
||||
/// Output format (table, json)
|
||||
#[arg(long, default_value = "table")]
|
||||
format: String,
|
||||
},
|
||||
/// Show daily token usage breakdown
|
||||
Daily {
|
||||
/// Number of days to show
|
||||
#[arg(long, default_value = "7")]
|
||||
days: u32,
|
||||
/// Claude Code data directory path
|
||||
#[arg(long)]
|
||||
claude_dir: Option<PathBuf>,
|
||||
},
|
||||
/// Check Claude Code data availability and basic stats
|
||||
Status {
|
||||
/// Claude Code data directory path
|
||||
#[arg(long)]
|
||||
claude_dir: Option<PathBuf>,
|
||||
},
|
||||
}
|
140
src/cli/mod.rs
Normal file
140
src/cli/mod.rs
Normal file
@ -0,0 +1,140 @@
|
||||
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;
|
||||
|
||||
// Token commands enum (placeholder for tokens.rs)
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum TokenCommands {
|
||||
Analyze { file: PathBuf },
|
||||
Report { days: Option<u32> },
|
||||
Cost { month: Option<String> },
|
||||
Summary { period: Option<String>, claude_dir: Option<PathBuf>, details: bool, format: Option<String> },
|
||||
Daily { days: Option<u32>, claude_dir: Option<PathBuf> },
|
||||
Status { claude_dir: Option<PathBuf> },
|
||||
}
|
||||
|
||||
pub async fn handle_server(port: Option<u16>, data_dir: Option<PathBuf>) -> 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<PathBuf>,
|
||||
model: Option<String>,
|
||||
provider: Option<String>,
|
||||
) -> 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<PathBuf>) -> 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<PathBuf>) -> 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<PathBuf>) -> 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<PathBuf>) -> 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<PathBuf>) -> 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(())
|
||||
}
|
250
src/config.rs
Normal file
250
src/config.rs
Normal file
@ -0,0 +1,250 @@
|
||||
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<String, ProviderConfig>,
|
||||
#[serde(default)]
|
||||
pub atproto: Option<AtprotoConfig>,
|
||||
#[serde(default)]
|
||||
pub mcp: Option<McpConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProviderConfig {
|
||||
pub default_model: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub host: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub api_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AtprotoConfig {
|
||||
pub handle: Option<String>,
|
||||
pub password: Option<String>,
|
||||
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<String, McpServerConfig>,
|
||||
}
|
||||
|
||||
fn string_to_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
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<String, String>,
|
||||
}
|
||||
|
||||
fn string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse::<f64>().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(data_dir: Option<PathBuf>) -> Result<Self> {
|
||||
let data_dir = data_dir.unwrap_or_else(|| {
|
||||
dirs::config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.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>(&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<String>, model: Option<String>) -> Result<AIConfig> {
|
||||
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")
|
||||
}
|
||||
}
|
@ -40,6 +40,9 @@ pub async fn handle_docs(
|
||||
"status" => {
|
||||
docs_manager.show_docs_status().await?;
|
||||
}
|
||||
"session-end" => {
|
||||
docs_manager.session_end_processing().await?;
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("Unknown docs action: {}", action));
|
||||
}
|
||||
@ -466,4 +469,138 @@ impl DocsManager {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// プロジェクト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(())
|
||||
}
|
||||
}
|
@ -150,10 +150,44 @@ impl ChatGPTImporter {
|
||||
match role.as_str() {
|
||||
"user" | "assistant" => {
|
||||
if let Some(content) = &message.content {
|
||||
if content.content_type == "text" && !content.parts.is_empty() {
|
||||
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::<Vec<&str>>()
|
||||
.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.parts.join("\n"),
|
||||
content: content_text,
|
||||
create_time: message.create_time,
|
||||
});
|
||||
}
|
||||
@ -280,7 +314,12 @@ pub struct ChatGPTAuthor {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChatGPTContent {
|
||||
pub content_type: String,
|
||||
pub parts: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub parts: Vec<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub user_profile: Option<String>,
|
||||
#[serde(default)]
|
||||
pub user_instructions: Option<String>,
|
||||
}
|
||||
|
||||
// Simplified message structure for processing
|
17
src/lib.rs
Normal file
17
src/lib.rs
Normal file
@ -0,0 +1,17 @@
|
||||
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 persona;
|
||||
pub mod relationship;
|
||||
pub mod scheduler;
|
||||
pub mod shell;
|
||||
pub mod status;
|
||||
pub mod submodules;
|
||||
pub mod tokens;
|
||||
pub mod transmission;
|
@ -1,42 +1,9 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum TokenCommands {
|
||||
/// Show Claude Code token usage summary and estimated costs
|
||||
Summary {
|
||||
/// Time period (today, week, month, all)
|
||||
#[arg(long, default_value = "today")]
|
||||
period: String,
|
||||
/// Claude Code data directory path
|
||||
#[arg(long)]
|
||||
claude_dir: Option<PathBuf>,
|
||||
/// Show detailed breakdown
|
||||
#[arg(long)]
|
||||
details: bool,
|
||||
/// Output format (table, json)
|
||||
#[arg(long, default_value = "table")]
|
||||
format: String,
|
||||
},
|
||||
/// Show daily token usage breakdown
|
||||
Daily {
|
||||
/// Number of days to show
|
||||
#[arg(long, default_value = "7")]
|
||||
days: u32,
|
||||
/// Claude Code data directory path
|
||||
#[arg(long)]
|
||||
claude_dir: Option<PathBuf>,
|
||||
},
|
||||
/// Check Claude Code data availability and basic stats
|
||||
Status {
|
||||
/// Claude Code data directory path
|
||||
#[arg(long)]
|
||||
claude_dir: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
mod ai_provider;
|
||||
mod cli;
|
||||
use cli::TokenCommands;
|
||||
mod config;
|
||||
mod conversation;
|
||||
mod docs;
|
||||
@ -54,8 +21,8 @@ mod tokens;
|
||||
mod transmission;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "aigpt-rs")]
|
||||
#[command(about = "AI.GPT - Autonomous transmission AI with unique personality (Rust implementation)")]
|
||||
#[command(name = "aigpt")]
|
||||
#[command(about = "AI.GPT - Autonomous transmission AI with unique personality")]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
@ -1,7 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use anyhow::{Result, Context};
|
||||
use serde_json::{json, Value};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use colored::*;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
use axum::{
|
||||
extract::{Path as AxumPath, State},
|
||||
http::{StatusCode, Method},
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::cors::{CorsLayer, Any};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::persona::Persona;
|
||||
@ -37,6 +52,32 @@ pub struct MCPError {
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
// HTTP MCP Server state
|
||||
pub type AppState = Arc<Mutex<MCPServer>>;
|
||||
|
||||
// MCP HTTP request types for REST-style endpoints
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MCPHttpRequest {
|
||||
pub user_id: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub query: Option<String>,
|
||||
pub keywords: Option<Vec<String>>,
|
||||
pub limit: Option<usize>,
|
||||
pub content: Option<String>,
|
||||
pub file_path: Option<String>,
|
||||
pub command: Option<String>,
|
||||
pub pattern: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MCPHttpResponse {
|
||||
pub success: bool,
|
||||
pub result: Option<Value>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub struct MCPServer {
|
||||
config: Config,
|
||||
persona: Persona,
|
||||
@ -44,12 +85,14 @@ pub struct MCPServer {
|
||||
scheduler: AIScheduler,
|
||||
service_client: ServiceClient,
|
||||
service_detector: ServiceDetector,
|
||||
user_id: String,
|
||||
data_dir: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl MCPServer {
|
||||
pub fn new(config: Config) -> Result<Self> {
|
||||
pub fn new(config: Config, user_id: String, data_dir: Option<std::path::PathBuf>) -> Result<Self> {
|
||||
let persona = Persona::new(&config)?;
|
||||
let transmission_controller = TransmissionController::new(&config)?;
|
||||
let transmission_controller = TransmissionController::new(config.clone())?;
|
||||
let scheduler = AIScheduler::new(&config)?;
|
||||
let service_client = ServiceClient::new();
|
||||
let service_detector = ServiceDetector::new();
|
||||
@ -61,6 +104,8 @@ impl MCPServer {
|
||||
scheduler,
|
||||
service_client,
|
||||
service_detector,
|
||||
user_id,
|
||||
data_dir,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1023,7 +1068,7 @@ impl MCPServer {
|
||||
|
||||
async fn tool_get_scheduler_status(&self, _args: Value) -> Result<Value> {
|
||||
let stats = self.scheduler.get_scheduler_stats();
|
||||
let upcoming_tasks: Vec<_> = self.scheduler.list_tasks()
|
||||
let upcoming_tasks: Vec<_> = self.scheduler.get_tasks()
|
||||
.values()
|
||||
.filter(|task| task.enabled)
|
||||
.take(10)
|
||||
@ -1082,17 +1127,94 @@ impl MCPServer {
|
||||
println!("🚀 Starting MCP Server on port {}", port);
|
||||
println!("📋 Available tools: {}", self.get_tools().len());
|
||||
|
||||
// In a real implementation, this would start an HTTP/WebSocket server
|
||||
// For now, we'll just print the available tools
|
||||
// Print available tools
|
||||
for tool in self.get_tools() {
|
||||
println!(" - {}: {}", tool.name.cyan(), tool.description);
|
||||
println!(" - {}: {}", ColorExt::cyan(tool.name.as_str()), tool.description);
|
||||
}
|
||||
|
||||
println!("✅ MCP Server ready for requests");
|
||||
|
||||
// Placeholder for actual server implementation
|
||||
// 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)
|
||||
@ -1104,4 +1226,517 @@ 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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(tool_name): AxumPath<String>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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<AppState>,
|
||||
Json(request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
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 (TODO: Fix ServiceClient method visibility)
|
||||
async fn get_user_cards_handler(
|
||||
State(_state): State<AppState>,
|
||||
AxumPath(user_id): AxumPath<String>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
// TODO: Implement proper ai.card service integration
|
||||
Json(MCPHttpResponse {
|
||||
success: false,
|
||||
result: None,
|
||||
error: Some(format!("AI Card service integration not yet implemented for user: {}", user_id)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn draw_card_handler(
|
||||
State(_state): State<AppState>,
|
||||
Json(_request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
// TODO: Implement proper ai.card service integration
|
||||
Json(MCPHttpResponse {
|
||||
success: false,
|
||||
result: None,
|
||||
error: Some("AI Card draw service integration not yet implemented".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_card_stats_handler(State(_state): State<AppState>) -> Json<MCPHttpResponse> {
|
||||
// TODO: Implement proper ai.card service integration
|
||||
Json(MCPHttpResponse {
|
||||
success: false,
|
||||
result: None,
|
||||
error: Some("AI Card stats service integration not yet implemented".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
// AI Log proxy handlers (placeholder - these would need to be implemented)
|
||||
async fn get_blog_posts_handler(State(_state): State<AppState>) -> Json<MCPHttpResponse> {
|
||||
// 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<AppState>,
|
||||
Json(_request): Json<MCPHttpRequest>,
|
||||
) -> Json<MCPHttpResponse> {
|
||||
// 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<AppState>) -> Json<MCPHttpResponse> {
|
||||
// TODO: Implement ai.log service integration
|
||||
Json(MCPHttpResponse {
|
||||
success: false,
|
||||
result: None,
|
||||
error: Some("AI Log service integration not yet implemented".to_string()),
|
||||
})
|
||||
}
|
@ -234,6 +234,67 @@ impl MemoryManager {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_stats(&self) -> Result<MemoryStats> {
|
||||
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::<f64>() / 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<String> = 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)]
|
@ -309,4 +309,40 @@ impl Persona {
|
||||
HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_message(&mut self, user_id: &str, message: &str) -> Result<ChatMessage> {
|
||||
let (_response, _delta) = self.process_ai_interaction(user_id, message, None, None).await?;
|
||||
Ok(ChatMessage::assistant(&_response))
|
||||
}
|
||||
|
||||
pub fn get_fortune(&self) -> Result<i32> {
|
||||
self.load_today_fortune()
|
||||
}
|
||||
|
||||
pub fn generate_new_fortune(&self) -> Result<i32> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -270,6 +270,22 @@ impl RelationshipTracker {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all_relationships(&self) -> Result<HashMap<String, RelationshipCompact>> {
|
||||
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)]
|
||||
@ -279,4 +295,13 @@ pub struct RelationshipStats {
|
||||
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<Utc>,
|
||||
pub status: RelationshipStatus,
|
||||
}
|
@ -268,7 +268,7 @@ impl AIScheduler {
|
||||
self.tasks.get(task_id)
|
||||
}
|
||||
|
||||
pub fn list_tasks(&self) -> &HashMap<String, ScheduledTask> {
|
||||
pub fn get_tasks(&self) -> &HashMap<String, ScheduledTask> {
|
||||
&self.tasks
|
||||
}
|
||||
|
||||
@ -415,6 +415,28 @@ impl AIScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// Type alias for compatibility with CLI interface
|
||||
pub type Scheduler = AIScheduler;
|
||||
|
||||
impl Scheduler {
|
||||
pub fn list_tasks(&self) -> Result<Vec<ScheduledTaskInfo>> {
|
||||
let tasks: Vec<ScheduledTaskInfo> = 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,
|
||||
@ -425,4 +447,12 @@ pub struct SchedulerStats {
|
||||
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<Utc>,
|
||||
pub enabled: bool,
|
||||
}
|
@ -1,8 +1,16 @@
|
||||
use std::io::{self, Write};
|
||||
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::{DefaultEditor, 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;
|
||||
@ -26,69 +34,185 @@ pub struct ShellMode {
|
||||
config: Config,
|
||||
persona: Persona,
|
||||
ai_provider: Option<AIProviderClient>,
|
||||
history: Vec<String>,
|
||||
user_id: String,
|
||||
editor: Editor<ShellCompleter, DefaultHistory>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
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<Pair>)> {
|
||||
// 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<Pair> = 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<Pair> = 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<Self> {
|
||||
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,
|
||||
history: Vec::new(),
|
||||
user_id,
|
||||
editor,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_ai_provider(mut self, provider: Option<String>, model: Option<String>) -> Self {
|
||||
if let (Some(provider_name), Some(model_name)) = (provider, model) {
|
||||
let ai_provider = match provider_name.as_str() {
|
||||
"ollama" => AIProvider::Ollama,
|
||||
"openai" => AIProvider::OpenAI,
|
||||
"claude" => AIProvider::Claude,
|
||||
_ => AIProvider::Ollama, // Default fallback
|
||||
};
|
||||
// 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 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);
|
||||
}
|
||||
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());
|
||||
println!("{}", "Type 'help' for commands, 'exit' to quit".dimmed());
|
||||
|
||||
// Load shell history
|
||||
self.load_history()?;
|
||||
// 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 {
|
||||
// Display prompt
|
||||
print!("{}", "ai.shell> ".green().bold());
|
||||
io::stdout().flush()?;
|
||||
// Read user input with rustyline (supports completion, history, etc.)
|
||||
let readline = self.editor.readline("ai.shell> ");
|
||||
|
||||
// Read user input
|
||||
let mut input = String::new();
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(0) => {
|
||||
// EOF (Ctrl+D)
|
||||
println!("\n{}", "Goodbye!".cyan());
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
let input = input.trim();
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
let input = line.trim();
|
||||
|
||||
// Skip empty input
|
||||
if input.is_empty() {
|
||||
@ -96,15 +220,26 @@ impl ShellMode {
|
||||
}
|
||||
|
||||
// Add to history
|
||||
self.history.push(input.to_string());
|
||||
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(e) => {
|
||||
println!("{}: {}", "Input 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;
|
||||
}
|
||||
}
|
||||
@ -148,27 +283,39 @@ impl ShellMode {
|
||||
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", "!<command>".green());
|
||||
println!(" {} - Execute shell command (Tab completion)", "!<command>".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", "/status".green());
|
||||
println!(" {} - Show relationships", "/relationships".green());
|
||||
println!(" {} - Show memories", "/memories".green());
|
||||
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 fortune", "/fortune".green());
|
||||
println!(" {} - Show today's fortune", "/fortune".green());
|
||||
println!();
|
||||
|
||||
println!("{}", "Conversation:".yellow().bold());
|
||||
println!(" {} - Chat with AI", "Any other input".green());
|
||||
println!(" {} - Chat with AI using configured provider", "Any other input".green());
|
||||
println!(" {} - AI responses track relationship changes", "Relationship tracking".dimmed());
|
||||
println!();
|
||||
}
|
||||
|
||||
@ -426,51 +573,25 @@ impl ShellMode {
|
||||
fn show_history(&self) {
|
||||
println!("\n{}", "Command History".cyan().bold());
|
||||
|
||||
if self.history.is_empty() {
|
||||
let history = self.editor.history();
|
||||
if history.is_empty() {
|
||||
println!("{}", "No commands in history".yellow());
|
||||
return;
|
||||
}
|
||||
|
||||
for (i, command) in self.history.iter().rev().take(20).enumerate() {
|
||||
println!("{:2}: {}", i + 1, command);
|
||||
// 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 load_history(&mut self) -> Result<()> {
|
||||
fn save_history(&mut self) -> Result<()> {
|
||||
let history_file = self.config.data_dir.join("shell_history.txt");
|
||||
|
||||
if history_file.exists() {
|
||||
let content = std::fs::read_to_string(&history_file)
|
||||
.context("Failed to read shell history")?;
|
||||
|
||||
self.history = content.lines()
|
||||
.map(|line| line.to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_history(&self) -> Result<()> {
|
||||
let history_file = self.config.data_dir.join("shell_history.txt");
|
||||
|
||||
// Keep only last 1000 commands
|
||||
let history_to_save: Vec<_> = if self.history.len() > 1000 {
|
||||
self.history.iter().skip(self.history.len() - 1000).collect()
|
||||
} else {
|
||||
self.history.iter().collect()
|
||||
};
|
||||
|
||||
let content = history_to_save.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
std::fs::write(&history_file, content)
|
||||
self.editor.save_history(&history_file)
|
||||
.context("Failed to save shell history")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::TokenCommands;
|
||||
use crate::cli::TokenCommands;
|
||||
|
||||
/// Token usage record from Claude Code JSONL files
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@ -273,14 +273,31 @@ impl TokenAnalyzer {
|
||||
pub async fn handle_tokens(command: TokenCommands) -> Result<()> {
|
||||
match command {
|
||||
TokenCommands::Summary { period, claude_dir, details, format } => {
|
||||
handle_summary(period, claude_dir, details, format).await
|
||||
handle_summary(
|
||||
period.unwrap_or_else(|| "week".to_string()),
|
||||
claude_dir,
|
||||
details,
|
||||
format.unwrap_or_else(|| "table".to_string())
|
||||
).await
|
||||
}
|
||||
TokenCommands::Daily { days, claude_dir } => {
|
||||
handle_daily(days, claude_dir).await
|
||||
handle_daily(days.unwrap_or(7), claude_dir).await
|
||||
}
|
||||
TokenCommands::Status { claude_dir } => {
|
||||
handle_status(claude_dir).await
|
||||
}
|
||||
TokenCommands::Analyze { file } => {
|
||||
println!("Token analysis for file: {:?} - Not implemented yet", file);
|
||||
Ok(())
|
||||
}
|
||||
TokenCommands::Report { days } => {
|
||||
println!("Token report for {} days - Not implemented yet", days.unwrap_or(7));
|
||||
Ok(())
|
||||
}
|
||||
TokenCommands::Cost { month } => {
|
||||
println!("Token cost for month: {} - Not implemented yet", month.unwrap_or_else(|| "current".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,11 +44,11 @@ pub struct TransmissionController {
|
||||
}
|
||||
|
||||
impl TransmissionController {
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
let transmission_history = Self::load_transmission_history(config)?;
|
||||
pub fn new(config: Config) -> Result<Self> {
|
||||
let transmission_history = Self::load_transmission_history(&config)?;
|
||||
|
||||
Ok(TransmissionController {
|
||||
config: config.clone(),
|
||||
config,
|
||||
transmission_history,
|
||||
last_check: None,
|
||||
})
|
||||
@ -386,6 +386,31 @@ impl TransmissionController {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_and_send(&mut self) -> Result<Vec<(String, String)>> {
|
||||
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)]
|
26
test_commands.sh
Executable file
26
test_commands.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/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 ==="
|
19
test_completion.sh
Executable file
19
test_completion.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/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..."
|
18
test_shell.sh
Normal file
18
test_shell.sh
Normal file
@ -0,0 +1,18 @@
|
||||
#!/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 ==="
|
22
test_shell_manual.sh
Executable file
22
test_shell_manual.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/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 ==="
|
Loading…
x
Reference in New Issue
Block a user