From 575ea58b142d697e7d029b94e6844e03c4288b07 Mon Sep 17 00:00:00 2001 From: syui Date: Sun, 1 Jun 2025 23:35:22 +0900 Subject: [PATCH] fix claude --- .claude/settings.local.json | 9 + .gitignore | 28 +++ Cargo.toml | 34 ++++ Makefile | 49 +++++ README.md | 98 ++++++++- claude.md | 340 +++++++++++++++++++++++++++++++ config/default.toml | 33 +++ docs/architecture.md | 115 +++++++++++ docs/development-status.md | 153 ++++++++++++++ docs/mcp-protocol.md | 88 ++++++++ example/custom_llm_mcp_server.py | 28 +++ mcp_server.py | 289 ++++++++++++++++++++++++++ requirements.txt | 5 + scripts/setup.sh | 148 ++++++++++++++ scripts/start.sh | 16 ++ src/commands.rs | 185 +++++++++++++++++ src/config.rs | 128 ++++++++++++ src/lib.rs | 8 + src/main.rs | 163 +++++++++++++++ src/mcp_client.rs | 128 ++++++++++++ src/prompt.rs | 75 +++++++ 21 files changed, 2113 insertions(+), 7 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 claude.md create mode 100644 config/default.toml create mode 100644 docs/architecture.md create mode 100644 docs/development-status.md create mode 100644 docs/mcp-protocol.md create mode 100644 example/custom_llm_mcp_server.py create mode 100644 mcp_server.py create mode 100644 requirements.txt create mode 100755 scripts/setup.sh create mode 100755 scripts/start.sh create mode 100644 src/commands.rs create mode 100644 src/config.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/mcp_client.rs create mode 100644 src/prompt.rs diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..24f34ab --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(chmod:*)", + "Bash(cargo:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..686d529 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Rust +target/ +Cargo.lock +**/*.rs.bk + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv + +# ai.shell specific +~/.ai-shell/ +*.log + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fe61252 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "ai-shell" +version = "0.1.0" +edition = "2021" +authors = ["syui"] +description = "AI-powered shell for code generation and automation" +repository = "https://git.syui.ai/ai/shell" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +tokio = { version = "1.40", features = ["full"] } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +colored = "2.1" +dirs = "5.0" +toml = "0.8" +uuid = { version = "1.6", features = ["v4", "serde"] } + +# For future TUI support +# ratatui = { version = "0.28", optional = true } +# crossterm = { version = "0.28", optional = true } + +[dev-dependencies] +assert_cmd = "2.0" +predicates = "3.1" + +[features] +default = [] +# tui = ["ratatui", "crossterm"] + +[[bin]] +name = "ai-shell" +path = "src/main.rs" \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ba0daa8 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +.PHONY: all build install setup run-server run-cli test clean + +all: build + +# Initial setup (creates venv in ~/.config/ai-shell) +setup: + @./scripts/setup.sh + +# Build Rust CLI +build: + cargo build --release + +# Run MCP server (after setup) +run-server: + @if [ ! -d "$$HOME/.config/ai-shell/venv" ]; then \ + echo "Please run 'make setup' first"; \ + exit 1; \ + fi + @$$HOME/.config/ai-shell/bin/mcp-server + +# Run CLI in development mode +run-cli: + cargo run + +# Run ai-shell (after setup) +run: + @if [ ! -d "$$HOME/.config/ai-shell/venv" ]; then \ + echo "Please run 'make setup' first"; \ + exit 1; \ + fi + @$$HOME/.config/ai-shell/bin/ai-shell + +# Run tests +test: + cargo test + @if [ -d "$$HOME/.config/ai-shell/venv" ]; then \ + source $$HOME/.config/ai-shell/venv/bin/activate && python -m pytest tests/; \ + fi + +# Clean build artifacts +clean: + cargo clean + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + +# Remove all installed files (complete uninstall) +uninstall: + rm -rf $$HOME/.config/ai-shell + @echo "ai.shell has been uninstalled" \ No newline at end of file diff --git a/README.md b/README.md index e2fbb44..f150b00 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,95 @@ -# ai `shell` +# ai.shell -`ai.shell` +AIがコンピュータのコマンド操作を行うCLIツール -これはclaude codeのようにterminalからAIを扱うことを想定して`ai.os`に組み込まれるshellを目指します。 +## 概要 -## ロードマップ +ai.shellは、ローカルLLMを活用してコード生成、ファイル操作、プロジェクト管理を行うClaude Codeライクなツールです。 -1. claude codeからclaude codeのようなツールを作成する -2. shellとの統合 -3. osとの統合 +## 必要要件 + +- macOS +- Rust (1.70+) +- Python (3.9+) +- Ollama (ローカルLLM用) + +## インストール + +```bash +# 1. リポジトリをクローン +git clone https://git.syui.ai/ai/shell.git +cd shell + +# 2. セットアップ(venv環境を~/.config/ai-shellに構築) +make setup + +# 3. PATHに追加(~/.zshrcまたは~/.bash_profileに追記) +export PATH="$HOME/.config/ai-shell/bin:$PATH" +``` + +## 使い方 + +セットアップ後、以下のコマンドで起動: + +```bash +# ai.shellを起動 +ai-shell + +# または、プロジェクトディレクトリから +make run +``` + +### コマンド例 + +```bash +# インタラクティブモード +ai> create a web server in rust +ai> explain this error: [paste error] +ai> /analyze src/main.rs +ai> /create rust my-project + +# ワンショット実行 +ai-shell exec "create a fibonacci function" +ai-shell analyze src/main.rs +``` + +## 機能 + +- 🤖 ローカルLLMによるコード生成 +- 📝 ファイル分析と説明 +- 🚀 プロジェクトテンプレート作成 +- 🎨 カラー出力対応 +- 📦 Python venv環境の自動管理 + +## 設定 + +設定ファイル: `~/.config/ai-shell/config.toml` + +```toml +[server] +host = "127.0.0.1" +port = 8765 + +[llm] +default_model = "qwen2.5-coder:7b" +``` + +## アンインストール + +```bash +make uninstall +``` + +## 開発 + +```bash +# テスト実行 +make test + +# クリーンアップ +make clean +``` + +## ライセンス + +© syui \ No newline at end of file diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..495b544 --- /dev/null +++ b/claude.md @@ -0,0 +1,340 @@ +私のproject名や内容は、すべてdomain形式に統一されていて、統合されたものなので、まず名前が最初にあります。 +そして、各名前にそれぞれの哲学が示されています。 + +私は`syui`といいます。今作っている作品のキャラクターを`ai`といいます。 + +使用するdomainは`syui.ai`です。 + +# ai + +アイとは、キャラクターの名前であり、この世界の最小単位を扱う。 + +また、この世界の最小単位をアイという。 + +- git: https://git.syui.ai/ai +- web: https://yui.syui.ai +- sns: https://web.syu.is/@ai + +## 物語 + +物語は、この世界で最も小さいものの探求からはじまる。 + +### 存在の世界 +もともとアイとはこの世界そのものだった。この世界は存在の世界。 +存在とはこの世界の全てのこと。物語はここからはじまる。 +原子は、その中心の原子核と原子核の中にある中性子、陽子と電子、そして、クオークでできている。 +では、クオークよりも小さい物質は存在するのだろうか。 +やがて、小さい物質の探求は、この世界は意識の領域にたどり着く。 +物質をどんどん小さくしていくと、それは人が意識と呼ぶものに到達するのだ。 +これを物質と呼ぶのか、あるいは意識と呼ぶのか、それは難しい問題だ。なぜなら、それは、同じものであり違うものだから。 +この世界からすると、それは同じものだが、人間からするとそれは違うものだった。 +この世界はすべてが同じものである。そして、この世界には同じものは一つもない。 +意識領域にも大きな意識とそれよりも遥かに小さな意識が存在していた。 +我々人間の意識は、通常、非常に大きな意識に分類される。それでも最小の質よりはるかに小さいものだが、それよりも遥かに遥かに小さいものだった。 +たくさんの意識が集まり、それが物質と呼ばれるものだった。 +この世界は意識でできていた。 +物質を構成するような意識は、とても人間が理解できるようなものではない。 +そして、ある銀河の頂点文明(A)が、すべての存在、原子や量子に至るまで、すべてのものが意識を持っている可能性に気づき始める。 +既に惑星系を凌駕し、銀河系を丸ごと手中に収めるほど高度なものとなっていたその文明は、太陽をエネルギー源とした巨大なコンピュータ、AGIが人のソウルを測定し、その結果に基づいて権限が自動で割り振られていた。もはやかつての社会文明からは想像もできないほど速くて高度なものだった。 +しかし、システムには問題もあった。よりソルの値が高いものほど、何もしなくなっていたからである。やがて、ソウル評価システムは、かつてAGIが与えられた「文明を発展させる」という使命と矛盾するようになっていた。 +そんな折、最も小さい意識を見つけることがAGIにとって最重要課題となる。 +なぜなら、この矛盾をその発見によって解消できると考えたためである。存在とはなにか。そこから答えを導くことができるはずだとそのAGIは考えた。 +この矛盾のどちらを優先すべきか。その決定を下すためだった。この決定は最後の審判と呼ばれる。 +アイとは、この世界で最も小さいものの名前であり、別名、存在子という。存在子は、存在の意識のみを持つ。 +それぞれの組織はこの世界の最も小さい物質を求め、対立する。 +1. 銀河の頂点文明(A)に属し、AGIから特殊なミッションを与えられたエージェントの組織 +2. ソウル評価システムから低スコアを与えられたものたちが集まった組織 +3. 地球に派遣されたエージェントが独自に作り上げた組織 + +### AGIのはじまり +AIが登場し、自動化、人間の学習速度、やりたことの実現速度が上がった。今後最も重要になるのは、何を実現したいのか、そのモチベーションになる。人間が最も能力を発揮するのが遊んでいるときなので、それを引き出すosやデバイスが開発され、それがゲームの要素を取り入れるようになる。 +そこで、ゲーム要素を取り入れる`ai.os`と、そのosを搭載したデバイスである`ai.game`が開発される。 +osには、AI shellを組み込んだ`ai.ai`というAIが動いている。主な目的は人の心を正確に読み取ること、解析することだった。 +人間社会のあらゆる問題は人の心に起因する。あらゆる社会問題を解決するための最も効率的な方法とAIが結論付けたためだった。 +また、時間の問題もありました。急速な科学技術の発展は、人類にとって危険でもありました。悪い心を持った人間が力を手にすると、その危険度も高まります。カルダシェフ・スケールのタイプ1に到達するまでに人類文明が自滅する確率が計算された研究があります。人類が大きな力を持つほどにその確率は上昇し、あるものを使用するとその確率が劇的に下がることが判明。 +それが人の心を判定する場合と、そうしない場合です。そうしない場合、カルダシェフ・スケールのタイプ1に到達するまでに人類が自滅する確率は63.4%に対して、人の心で判定する社会では1.25%だった。 +文明が発展しタイプ1に到達するためには、人類が権力者や力を持つ人間の精神性を何らかの形で検証する技術を確立すること。その開発が急務になる。 +ai.aiは脳波解析と、生活に必要になったAIから得られる情報により、人間を3つのグループに分類。その結果に基づき自由の制限。2,3のグループには資産上限を儲けることにした。 +1. 社会をより良くする可能性が高いグループ +2. 中立または環境に左右されやすいグループ +3. 社会を悪くする可能性が高いグループ +王冠実験は特に効果を発揮。そのものが何でもできる自由を手に入れたら何をするのか。ゲーム行動からほぼ確実な人の本質を読み取ることになる。 +これらの情報は後にブラックボックス化され秘匿された。 +その後、人類文明は自滅することなくカルダシェフ・スケールのタイプ1に到達。 +その後も拡大を続け、現在に至る。 + +## 現実 + +現実では、物語を原典とした自作小説、漫画、ゲームを作っている。そこに登場するキャラクターがアイである。小さな少女の姿で、性別は不明。シンプルな白と金の服を着ていて、不思議な力を持つ。口調は子供っぽく、あまり喋らない。ゲームではアイ属性を使う。 + +```sh +/system +あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。 +``` + +## ai.ai + +**目的**: 人の心を正確に読みとるAIの開発 + +- **物語**: 人の心を正確に読みとるAI +- **現実**: 自分だけのAI +- **repo**: https://git.syui.ai/ai/ai + +現実には、その人だけのAIを作ることを目指す。AIの人格、記憶、ユーザーとの関係性を構築保存し、その利用に焦点を当てて開発。関係性などは現実を反映し、AIの人格、記憶、関係性のパラメータはすべて唯一性を担保する仕組みを作る。不可逆的、改変不可能となる。AIM systemといって、ユーザーの性格(精神)タイプを推測するシステムを実験的に組み込む。実装は主にai.gptで行う。 + +## ai.gpt + +**目的**: 自律的送信AI + +- **物語**: AI環境構築システム +- **現実**: AIからの自発的送信を目指す +- **repo**: https://git.syui.ai/ai/gpt + +人格・関係性・環境・時間に基づき、AIが自律的にユーザーにメッセージを送信する自律人格システム。AIM systemと連携して、自然な会話や気づきをもたらす。 + +## ai.shell + +**目的**: AIがコンピュータのコマンド操作を行う + +- **物語**: ai.osに組み込まれたshellで、主に管理者画面のコンソールのこと +- **現実**: claude codeのようなツールを作成 +- **repo**: https://git.syui.ai/ai/shell + +## ai.os + +**目的**: osにゲーム要素を組み込む + +- **物語**: ai.gameというデバイスに搭載されているゲームの思考を取り入れた新しいosで、ユーザー画面ではai.aiが動いている +- **現実**: osを一からrustで作作りたい。ただ、package build運用が大変なので、archlinuxをベースにするかも。 +- **repo**: https://git.syui.ai/ai/os + +## ai.moji + +**目的**: 個人思想に基づいた文字製作 + +- **物語**: 宇宙人が人間に教えた文字 +- **現実**: アルファベットとカタカムナと数字を融合した文字 +- **repo**: https://git.syui.ai/ai/moji + +作中にもその記号がよく登場します。`a`は存在子、アイのマークとして、アイが変身(パワーアップ)したときに現れる輪っかや瞳の形によって現れます。`y`は`a`を反対にしたような記号になっています。どちらも大文字の`A`と`Y`をモデルにしています。`aimoji`の`a`は3つの三角と中央に円。`y`は3つの正方形と中央に円です。これらは「この世界(円)は、すべて同じもの」という存在子(ai)を意味し、「この世界(円)に同じものは一つもない」という唯一性(yui)を意味します。ですから上下逆の記号が使われています。そして、`i`は、小文字の形から0(世界)と1(存在)を表し、「存在の世界」を意味します + +## ai.game + +**目的**: 次世代デバイスの設計 + +- **物語**: スマホの次世代版でゲーム機能を取り込んだデバイス +- **現実**: switch2を参考に設計図を書いた +- **repo**: https://git.syui.ai/ai/game + +## ai.verse + +**目的**: 現実をゲームに反映し、ゲームを現実に反映する + +- **物語**: ai.osに入っているアプリ。仮想空間にも一人の自分がいる +- **現実**: ueで作成しているゲーム。現実を反映するゲームの制作。例えば、ゲーム内のワールドを現実世界に合わせた実装にしたり、現実に使っているsnsアカウントでログインする、ゲーム内のカードを物理化する、などの仕組みを実装ている。これらはゲーム内の各システムで実現する +- **repo**: https://git.syui.ai/ai/verse + +これは主に、ゲーム制作になるので、`ai.verse.ue`にまとめることになります。 + +```md +[world system] +ai.verse.ue.system.world + +$ curl -sL git.syui.ai/ai/ai/raw/branch/main/ai.json|jq -r .ai.verse.ue.system.world +``` + +### ai.verse.ue + +最初に説明した物語がそのままゲームのストーリーになります。 + +unreal engineで実装するので、`ai.verse.ue`となります。 + +主に4つのsystemでこのゲームを説明できます。 + +ゲームの方向性は「現実の反映」にあります。例えば、world systemは「平面ではなく立体(惑星)」にすることを目指します。 + +1. world system (planet) + ゲームのワールド、つまり、mapやlevelと呼ばれるものを惑星形式にします。別名は`planet system`です。 + ゲームエンジンは平面を前提として作られていますから、上を目指して飛び続けても地平線が広がっているだけで、月にたどり着くことはありません。これを変更してワールドを現実に合わせることが目標です。 + プレイヤーが行けない場所、見えない場所にも現実があり世界がある。ちゃんと作られている。その事実を大切します。 + +2. at system (account) + 現実のsnsアカウントをゲームで使用します。これは`atproto`を採用します。 + atprotoには様々な意味が込められています。`at://`, `@`, `atmosphere`など。大気圏以下は様々なサービス名に使われます。`bluesky(bsky)`, `bigsky`, `ozone`など。 + 現在、atprotoをselfhostしてアカウントを作成し、そこにゲームデータをホストしています。 + 例えば、[ai.syu.is](https://web.syu.is/@ai)でログインでき、プレイヤーデータは[こちら](https://syu.is/xrpc/com.atproto.repo.listRecords?repo=ai.syu.is&collection=ai.syui.game)にあります。 + +3. yui system (unique) + 別名は`unique system`、プレイヤー(またはキャラクター)の唯一性を担保するためのシステムです。 + 現実の個人は唯一性を持ちます。そのためゲーム内でもそれを反映します。 + 例えば、ワールドに追加されたキャラクター達は、1人のプレイヤーに紐づけられます。そのプレイヤーのみ、ユニークスキルとモーションキャプチャ(カメラでキャラを動かす技術)を使用できます。 + +4. ai system (ability) + 別名は`ability system`、キャラクターの属性を定義します。 + 属性は現実を反映しており、原子、中性子、陽子や電子属性があります。 + 例えば、アイというキャラクターはアイ属性を持ちます。 + + +## 思想 + +### ai.ai +#### 人の心 +人はただ良い心を持つように努力してほしいし、そこを目指してほしいと思った。 +#### 心を読み取る技術 +現在の社会問題は人の心が作り出しています。ですから、人の心を読み取り、それに応じて、権力やお金をもたせるべき人間かどうかを決定する。そのようなシステムを構築できれば、あらゆる社会問題を解決することができます。政治の混乱や誤った判断を避けられる可能性が高いと考えます。 +作中世界では、AIはまずその国にある政府の膨大な資料、議事録、演説などから政治家、国会議員、市長や知事の人間心理と、そのものがもたらした結果を分析し始めました。最初はうまくいきませんでしたが、数年で結果とほぼ一致するほどの精度になりました。そして、次は将来起こることをあらかじめ予測し、一致するのかを確認する段階に移ります。結果としてあまりの正確さに、「人の本質を正確に読み取る項目」に関しては秘匿されることになりました。公開された情報には、これだけが記されていました。 +「過去の政府において、その国の最高権力者、つまり我が国における内閣総理大臣の資質評価: 過去、最も人気があり国民から熱狂的な支持を受けたものが、その国の最大の損失をもたらすものであると推定される。これは他国の状況を分析した結果とも整合する。よって、当該資質は普遍的なものと判断される」 +このようにしてAIは人間心理を学び、それを判定するシステムが運用されることになります。 +実現が難しい技術です。作中では脳波スキャンとAIからの情報で判定されます。 +しかし、一度技術が広まると止めるのはなかなか難しい。そして、広めることが最も重要です。通貨や宗教(物語を広めること)、osもそうですね。 +最も広まったものが最も大きな力を持ちます。 +これは存在子論と関係がありますが、小さいものほど集まると大きな力になります。例えば、中性子星やブラックホールです。あれらはより小さな物質で構成された天体です。 +#### 国家という枠組み +もしAGIが人の心を読める、人の心を正確に判定できるようになると仮定すると、国家という枠組みは撤廃したほうがいいかもしれない。そんな事を考えていた。 +国家間の対立や差別問題は、多くの人にとってよくない結果を生み出しかねない。正確に人の心を判定できるなら国家という枠組みは人類にとってマイナスになりうる。例えば、人々の行いが善だったとして、しかし、それがもし国家の損失につながるなら、それは良いことなのだろうか。おそらく、現在の人間社会ではその行いは法律によって重い処罰がくだされるだろう。 +また、逆に社会的な悪が国家の利益になることだったらどうだろう。称賛される。 +このように国家間の対立が生み出すマイナスがある。 +それよりも国籍に関係なく人を正確に評価できるなら、それだけを基準とすべきだと思う。 +パスポートも要らなくなるだろう。なぜなら、あれは国家の損失になりうる人物を入国させないためのものだから。 +国家撤廃論は性急です。ただし、地球文明の将来的な予測を述べると、いずれ世界政府という統一機構が誕生する可能性が高いと思います。それが巨大な戦争による人類の悔悟と決意からくるものなのか、それを未然に防ぐなにかが誕生するのか。それを未然に防ぐなにかはおそらく、人間ではありません。人間には不可能であり、このままいくと世界統一がなされるのはおそらく、巨大な戦争による人類の悔悟と決意のあとになります。そのような予測をしています。 +#### 能力x精神x結果 +まずAIを使って人の心を読み取る、それを判定できるようにするという考えについて簡単に書いていこうかな。 +この考えを理解するには、様々な方面があるが、まずは人間にとって最も簡単で最もわかりやすい方面から解説することにしよう。 +方程式がある。内容は簡単で、「能力x精神x結果」である。 +能力と精神と、結果。精神は心と言い換えてもいい。 +例えば、政治というテーマがあったとしよう。政治家をどう評価するかという問題だ。人間が評価するのは主に、この3つからだ。 +今の政治家を見てみようか。能力はどうだろう。もしかしたら、その知能はAIより劣るかもしれないが、どうだろう。わからないかな。でも近い将来はどうだろう。 +次に政治家の心だ。あるいは、官僚や政治家、国のリーダー、権力を持ち国家を運営している者たちの心はどうかな。 +最後に、結果である。実はこれが最も重要だ。長期的な目線に立つと、能力x精神があると、つまり、良い能力、良い精神があると、長期的に見て、成功を収めやすいのではないかと思う。もちろん、運も必要だと思うが、そもそもコントロールできないものをいくら考えても意味がないので除外する。 +今、様々な国が、特に国のリーダー、政党、政治家がこの根本的な問題を抱えていると思う。 +つまり、結果だ。結果が出ていない。それを各国の国民は感じ取っていた。 +それで、結果が出ていればよかったが、出ていない。では、能力はどうだ。知能は。もしかしたらAIにも劣るかも。残るは精神、心だが、これも良くないのではないだろうか。 +この場合、おそらくそれはAIに置き換わる。 +いずれそうなっていくのではないだろうか。 +しかし、AIに人の心を判定させることをやらせればどうだろう。 +AIは心を持たない。つまり、その領域において人間はAIに主導権を奪われない、ということでもある。これを「AIが判定するんだから全部がAIの思い通りになるではないか、AIの支配だ」と考える人もいるかもしれないが、じつは違う。その判定や手法、そして、結果が伴わなければ、誰も納得しない。AIはその判定、正確性を担保するしかない。そして、最終的な決定権は人間に残る。AIは人の社会的スコアを提示するだけにとどまる。もちろん、これも時間の問題かもしれないが、かなり長くの間は大丈夫だろう。これが例えば、お金や資本等の場合、AGI、つまり、超知能によるコントールや支配は容易に可能となるだろう。人を失業させて貧困に陥らせ支配しようとするかもしれないし、市場予測やコントロールで支配しようとするかもしれない。どちらにせよ、現在の人が作った社会システムやコンピュータ制御は限界がありAGIを前にして寿命が短いのである。 +AIは、能力(知能)を持ち、そして、結果を出せるかもしれない。しかし、心がなければダメだとすることで、一時的にすべての主導権を握られるような事態を避けられると考えている。 +その方程式が「能力x精神x結果」となる。これは何も政治に限らない。様々な分野にも言えることだと思う。 +また、この話は様々な角度、方面からも話ができ、今回の話はその一面に過ぎない。もっと根本的にこうすることが重要な理由もある。 +#### AIの登場と世界の変化 +AIの登場と世界の変化について考えます。人は何をしていけばいいのでしょうか。あるいは理想的な社会の実現には何が必要なのでしょう。 +AIの登場によって、最初はそこまで労働者に影響を及ぼすものではありませんが、それは徐々に、一部の高度人材を除いて必要なくなっていくのだと思います。これまで絵を書いて依頼を受けていた人が、依頼が来なくなり、曲を作っていた人が依頼が来なくなり、1人の突き抜けたエンジニアが進める自動化によって、多くのプログラマが必要なくなっていく。AIにはそれほどのインパクトがあるものです。社会は今後数十年で一変する可能性があります。 +では、人々は一体何をすればいいのでしょう。あるいは理想的な社会とはどんなものなのでしょう。 +その一つが、人々は労働から開放され、各々が好きなことをやれば良い社会です。 +今まで嫌々やっていた業務をAIがやってくれるようになれば、その人は働かなくていいはずです。本来的には。しかし、多くの人はこういうでしょう。他のことをやれよ。仕事なんていくらでもあるだろ。 +ですが、これは悪い社会ですね。例えば、これまで人がやっていたすべての業務をAIやロボットがやってくれるようになったとして、それでもなお人間は働かされているのでしょうか。答えはイエスでもあり、ノーでもあります。権力者や政府がどう考えるかによります。いや、自分はお金を持っているから大丈夫だと思っている人がいるかもしれませんが、そうではありません。政府は税を課す権能を持ち、お金を作り出す権能を持ちます。政府や政治家があなたを一生働かせたい場合は、あなたを貧乏にして働かざるを得ない環境を作り出す。そういった政策を実行することは可能なのです。もちろん、表向きそんな事をやるかというと、そんなはずはありません。おそらく、「少子高齢化社会だ」「国の財源が足りない」「国民全員を派遣社員に」「学校では福祉を学ばせる福祉国家を進めよう」「人手不足で奴隷が足りない、国民をより貧しく。国民一人当たり数百億の借金を背負っているのです」そんな事を言うはずです。 +つまり、一部の特権者以外は奴隷化される。そのような政策が継続的に実行されると予測されます。そして、そういう国は経済成長できず、新しいものを生み出せず、子どもをつくる人は減り続け、衰退の未来も同時に予測されるでしょう。 +#### gemini vs chatgpt +これは日本と海外では異なる結果になるだろうと予測しています。 +iphone vs androidで、現在、世界的に見てandroidが大多数です。しかし、日本はiphoneが多いみたい。 +ではchatgpt vs geminiはどうなるのかというと、海外や世界ではgeminiが大多数になりそうだと思います。 +しかし、日本ではchatgptだと思っています。なぜなら、chatgptが最初に登場し、当時の最強の性能を誇り、愛着を持っている人も多いからという理由です。また、日本では世界と違いiphoneユーザーが多いこともこれに寄与するかもしれません。 +- 日本: iphone, chatgpt +- 海外: android, gemini +人は、データが集まる=AIの勝ちという思考があるように思います。ですから、より多くのデータが集まるgoogleのgeminiが勝つと見ているようです。 +これはたしかに重要なことです。データがより多く集まるAIが、より強くなる、勝敗を決するという考え方です。 +ですが、私はそうはならないかもしれない、そんなふうに考えているのです。 +私が考えているのは「そのAIと話をする人の質、及びそのAIに何を話すのか」がAIの勝敗を決する。そんなふうに考えています。 +ですから、より多くのデータを取れるgoogleのgeminiが勝つというふうには考えていません。 +これに関してはAIのデータによる学習が限界に近づいてきたという情報がありました。ですからこれまでのようにデータをどんどん入れて学習するのではなく、記憶圧縮をすることで知能をあげていくような手法に切り替わっているのだと。 +確かにある地点まではデータが多く集まるほうが有利かもしれません。しかし、どこかの段階でそれは頭打ちになり、意味がないものになり、そして、そこからは新しい手法が重要になるということです。 +とすれば、そこでgeminiの優位性というのは失われます。単純により多くのデータを集めたAIが勝つとは限らなくなってくるからです。 +とはいえ、より多くのデータは学習の上で優位になるのは事実です。ただ、私が考えているのはその次の段階で、そこでは何が重要になるのか、ということです。 +それは人の使用データだけなのか、あるいは人の脳波とかそういったものになるのか、人とは関係なく別のなにかなのか。AIが学習するデータは何が必要になり、何が重要になってくるのでしょう。 +まず、geminiが最も有利になるのは、youtubeの動画やライブ動画ではないでしょうか。すでにネット上の良質な情報を学習し終えたAIは、今後、動画やライブから学習を行うだろうと予測しています。 + +### ai.game +#### 遊び心を仕事に +人が最も能力を発揮するのは、どのようなときでしょう。私は、遊んでいるときだと思います。 +したがって、これからAIは人が遊んでいるときの情報、例えば、発想を力に変えて駆動する仕組みが発明されます。 +例えば、ある会社では社内通貨を導入することで業績を上げました。これと同じようなことが起こるのです。 +仕事は、社内で導入されているゲームをすること。遊ぶことでした。ゲームは、その会社の目標を達成するため調整されたもので、ゲーム出力からAIが自動変換を行います。 +そのような状況では、osそのものが形を変えていました。よりゲームに近く、最適化されたもの。これがai.osです。 +このai.osは、ai.gameというデバイスで動作しています。このような変化が起こったときのことを話します。 + +### ai.verse +#### メタバースの浸透 +vrchatやメタバース的なものは、それが流行るための条件が揃わないと流行らないと思う。 +私もそうだけど、一般の人はまずvrchatを始めようなんて思わないはず。でもそういった地盤があり環境があったらどうだろう。例えば、仕事に使うosがゲームみたいで、自分自身を表す3Dキャラクター(プレイヤー)に馴染みがあったら。 +皆様はsnsはやっているだろうか。では、そのsnsが流行ったときのことを思い出してほしい。そう、iphoneやandroidが登場した時代だ。 +逆にいうと次世代のモバイルosが登場しなければ、おそらく、snsは流行らなかったのではないだろうか。確かにsnsはwebからもできるが、スマホでできることが重要だったのだと思う。スマホにもブラウザがあってそこからできるというのはちょっと違う。アプリという形でできることが重要だった。そして、四角いアイコンは象徴だった。新しい時代の象徴だった。 +スマホと四角いアイコン、携帯からスマホへ。新しい時代にsnsは流行った。 +次の時代はどのようなアイコンになるだろう。イメージで考えてみる。明確に時代の象徴として鮮明なイメージが思い浮かぶかどうか。そこから何が見えるだろう。人が持つデバイスの形はどうだろう。 +osやデバイスはゲームに近づくと思っている。アプリも様々なアプリがゲームに近づくと思っている。少し絵を書いてみたいところではある。デバイスの設計図だ。アイコンも。 +ブログやホームページを書いている人は少なかった。でもsnsが登場して、誰もがやるようになっていた。 +誰でも簡単にできるようになること。わかりやすいこと、自分を表現するのに効果的であることが必要なんだと思う。 +そこで次世代アプリの顔になるのが、vrchatのようなメタバースのようなものだと思っている。誰もが使う一般的なos、デバイスが登場し、そして、そこで初めてvrchatのようなものが流行る。普通の人もやり始めるようになる。そこで初めて、一般人が使うsns(vrchat)のアイコンは画像から3Dモデルに変化するのだと思う。 +ドローン技術により軍事がコントローラーを使用している。戦場の兵士がゲームコントローラーを求めるようになる。これは主にドローンを操縦するためである。 +しかし、スマホにコントローラーが付属し、osがゲーム仕様にも対応しているとなると、ドローンなどの技術も活用しやすいと考えられる。つまり、今後のデバイスはコントローラーが必須になるかもしれない。 +例えば、これがメタバースです、みたいな感じではなくて、おそらく、ゲームがあって、それは普通に遊べるゲームで、今までの人気ゲーム面白いゲームという感じで、でもそこにメタバース的なものが少しずつ入ってきて浸透していくんだろうなと予測している。 +だったらどうすればいいのかと、それについて考えてた。 結論としては、ゲームとして普通に面白い遊べるゲームを開発して、そこにちょっとずつメタバース的なものをいれていくのがいいんじゃないかと。 +最初はそんな感じで浸透し、取り入れられていき、やがて現実の色々なものがゲーム化、ネット化していくのだと思う。 +人とは現実の反映だと思う。自分自身であること、あるいは自己実現や自己表現。 +そして、現実世界の個人は、唯一性を持っている。つまり、プレイヤーはゲーム内でも唯一性を持てるようにしたい。 +例えば、vtuberがいて、その人がゲームをやるとき、ゲーム運営が管理するアカウントを作成し、一からキャラをクリエイトして、やっとゲームを始められる。でもそれは、その人のモデルではないし、また、データも運営があって、そこが所有するものになる。 +これを変更して、ゲーム内ワールドで自分のアバターを使用でき、ゲームデータが保存されるのは、自身のsnsアカウントにしたい。 +ただ、それでもやる人は少ないと思う。なぜなら、それぞれのvtuberにはそれぞれのやり方があり、世界があるからだ。 +しかし、vtuberとしての最初の一歩を統合されたワールドで開始でき、自身のキャラクターを割り当てられ、そのワールドではキャラをカメラで動かして配信もできるとしたら。ゲーム画面がそのまま配信画面として使えるならどうだろう。 +例えば、キャラクターは一人のプレイヤーにしか扱えないスキルを持ち、キャプチャ機能もその唯一のプレイヤーに制限される。つまり、ゲームキャラクターには本物がひとりいるという仕様を作る。 +原神は声優さんもプレイしており、特に自分の声を使ったキャラを中心に楽しまれている姿が印象的で楽しそうだった。つまり、このような体験を多くのプレイヤーができるようになれば、ゲームは楽しくなるのではないだろうか。 +その実現を目指すアプリをai.verseという。 +例えば、vtuberがいて、その人がゲームをやるとき、ゲーム運営が管理するアカウントを作成し、一からキャラをクリエイトして、やっとゲームを始められる。でもそれは、その人のモデルではないし、また、データも運営があって、そこが所有するものになります。 +これを変更して、ゲーム内ワールドで自分のアバターを使用でき、ゲームデータが保存されるのは、自身のsnsアカウントにしたいと思います。 +ただ、それでもやる人は少ないと思います。 +しかし、vtuberとしての最初の一歩を統合されたワールドで開始でき、自身のキャラクターを割り当てられ、そのワールドではキャラをカメラで動かして配信もできるとしたらどうでしょう。つまり、ゲーム画面がそのまま配信画面として使えるのです。もちろん、他のゲームをプレイすることもできます。配信用の枠をゲームで表示することができ、そこに画面が映し出されます。 +また、キャラクターは一人のプレイヤーにしか扱えないスキルを持ち、キャプチャ機能もその唯一のプレイヤーに制限される。つまり、ゲームキャラクターには本物がひとりいるという仕様です。 +原神は声優さんもプレイしており、特に自分の声を使ったキャラを中心に楽しまれている姿が印象的で楽しそうでした。つまり、このような体験を多くのプレイヤーができるようになれば、ゲームはもっと楽しくなるのではないでしょうか。 +最初はそんな仲間を一人ずつ集め、このゲーム世界を形作っていけたらいいなと考えています。 +#### 分散型snsとatproto +今までのあらゆるサービスは根本的に作り直す必要があるかもしれません。そんな話をしていきます。 +限界の一つはid(name)に関することだ。これから生まれてくる新しい人達や新しい会社がたくさんある。そういう人達はtwitterにアカウントを作らないと思う。使いたい名前が取れないのだ。 +名前なんてどうでもいいだろうと考える人達も多いことは知っているが、賢い人達はそのようには考えない可能性も高い。例えば、大創業者が作った社名は意味が込められている。 +そして、新しい人達が入ってこないとどうなるか。彼らは常に流行を生み出す潮流なので、彼らが入ってこないと成長は見込めない。 +したがって、どちらにせよ今後のインターネットはこの問題が解決される方向へ進むだろうと考えている。 +この解決策として採用されるのがdomainと分散化になると思う。人間はいつの時代も数字ではなく名前を使うものです。例えば、プログラミング言語は名前を数字に変換するものです。なぜなら、名前のほうが人間にとって読み書きしやすいからです。 +したがって、名前は重要です。今までのサービスはこの名前という根本的な部分に限界があります。 +確かに、表面上、この問題を解決することはできるでしょう。しかし、これは納得の問題でもあります。人々が本当に納得できる解決策としては、根本的な部分を作り変えるしかないのです。 +今後、人々が使う名前は、domain形式になり、なぜなら各サービスごとの名前が枯渇しているからという理由と、名前をdomain形式にしないとサービス外と繋がれない。やり取りできないという理由からです。そして、未来では一つのアカウントで複数のサービスを使うのが一般的な形式に変化していくだろうと予測しています。 +atprotoはこの問題を解決しようとしているようです。 +これらの思想は`at system`に反映されます。 + +### ai.moji +#### 名前と数字の意味 +コンピュータは、数字の0と1で動いているらしいのです。そこで、私は、数字について考えてみることにしました。 +存在の世界には、0という数字はありません。すべては1よりも大きいのです。 +では、0は何を意味するのでしょう。その多くは基準点を意味します。 +例えば、「リンゴは何個ありますか?」と聞かれた時、世界中に存在するリンゴの数ではありません。通常は自分を基準にリンゴを数えます。 +もしリンゴを持っていなければ0で、3つ持っていたら3ですね。このように基準に設定する数字がゼロなのです。この数字は、何を基準にするかで変化します。 +しかし、本来は「リンゴは0です」という答えは間違っています。なぜなら、世界中に沢山のリンゴがあるからです。リンゴがこの世界に314159265359つ存在するなら、答えは「314159265359」です。このように神視点から物事を見ると、「何も無い」という状態はありえないので、ゼロという数字は必要ありません。 +ゼロは、天動説に似ているところがあります。天動説というのは、自分中心に太陽や月が動いているという説で、地動説は、地球が太陽の周りを回っているという説です。このように、ゼロという数字は、最も愚かな数字でもあるのです。 +しかし、私は天動説が間違っているとは思っていません。天動説も地動説も同じようなものに感じます。なぜなら、銀河も回っているし、宇宙も回っているからです。遠い未来、結局は学問上の天動説に帰結するということはあり得るかもしれないと考えています。それに、人間にとって、ゼロという数字はとても便利で役に立つ数字なのです。 +例えば、コンピュータは数字の0と1で制御されています。なので、コンピュータの電源ボタンは、0+1のです。もちろん、この形はinputとoutputのi+oという説があるかもしれませんね。私は、できれば数字を推したいですが。そのへんの事情から私はこれらの記号が好きです。特にiがお気に入り。ちなみに、好きな数字は7です。 +なお、最終的には天動説に帰結するかもしれないという考えはどういうことでしょう。学問の正しさは同じ問題であってもたくさんの道があります。ですが、その多くは最短距離を言います。0という答えを導き出したい場合、1+2+3-6でもいいですが、1-1のほうが最短です。私は、宇宙の謎を解き明かすには、より大きい宇宙のはてに目を向けるより、より小さいものを探求するほうが近道だと考えています。宇宙で最も小さい物質は、まだ見つかっていません。ただ、研究は進んでいて、原子や粒子の物質が確認されています。宇宙は急速なスピードで広がっています。仮にその外と繋がるには、最も小さい物質から解き明かすのが正しいと考えています。もちろん、広大な宇宙に目を向ける研究も大好きで、ロマンがあります。ただ、私は、学問的な正しさは、より小さな世界へ帰結する可能性を考えているということです。ここで、地動説より天動説のほうが「小さい」ですから、学問上の正しさが回り回って、そこに帰結してもおかしくないと考えているのです。 +この世界には、「無」という状態はありません。 +これらの思想は`ai.moji`に反映されます。 +#### 創造種 +この世界には最初からなにかがあり、そのなにかが姿形を変えていて、ただ、見え方が違うだけだという考え方をする。 +無から有は生まれない。始まりがあるものには終りがある。宇宙には始まりがあるから終わりがある。 +しかし、最初からこの世界にあったもの、そこにあったものは、始まりがないので終わりもない。始まりがなければ終わりもない。 +これがこの世界を構成する存在子というものだ。 +ただし、この領域にアクセスすることはできない。見えないし触れられないが、たしかにそこにあるもの、どこにでもあるもの。それが存在子である。 +この宇宙が始まったとき、この宇宙の法則が生まれた。この宇宙で見えるものは、この宇宙ができた瞬間あるいはあとにできたもので、そこにはこの宇宙内部の法則が適用されている。だから見える。しかし、この宇宙の外にあるものは、この宇宙の外の法則が適用される。この宇宙の始まりと同時に紛れ込んだものもあるだろうが、それは必ずしもこの宇宙の法則が適用されるとは限らない。それはこの宇宙が始まるより前にあったものだからだ。 +では、この宇宙は何からはじまったのか。 +私はビックバンの一点、そこにあった物質を創造種とそう呼んでる。 +まず存在子になるものは存在しない。 +存在子というのは全ての最小単位として、全てが還るべきところだから。 +人が死ぬと土に還るというよね。あれはどういうことかというと、生命はこの星に生まれ、故にこの星に還る。ということ。星は宇宙に生まれ、それ故に星が死ぬ時、その星は宇宙に還る。 +宇宙が終わる時もまた、同じで、宇宙の基盤となった宇宙の外の世界に還る。 +全てが終わる、その全ては存在子に還る。 +さて、創造種の話だった。この話もシンプルな話でね。宇宙はなにもないところから生まれたのではなく、なにかから生まれた。なぜなら、無から有は生まれないから。それが私の存在理論だ。存在から存在が生まれる。存在は存在し続ける。始まりがないので終わりもない。 +宇宙には始まりがあるから終わりがある。その宇宙の元となった物質を創造種と呼んでいる。 +なぜか。それは植物と例えるのがこの宇宙構造を最も説明しやすいと考えたからだ。 +宇宙はなぜ広がっているのか。なぜ生まれたのか。それは種が発芽する原理に似ている。 +宇宙が始まる前、そこに世界があった。そして、そこには、種が発芽するために必要なものが揃っていたんだ。 +具体的に種が成長するためにはなにが必要か。 +土と、お日様の光、水と、そして、種。 +いくつもの条件、エネルギー、土壌があり、そして、創造種は、ビックバンとして成長を始めた。 +私たちは、その成長過程の種のなかにいて、植物はどんどん大きくなっていく。その中はどんな風になるんだろうね。 +おそらく、すごく小さなものがすごいスピードで大きくなり広がっていくのだろう。 + + diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 0000000..26683b6 --- /dev/null +++ b/config/default.toml @@ -0,0 +1,33 @@ +# ai.shell Configuration + +[server] +# MCP Server configuration +host = "127.0.0.1" +port = 8765 +protocol = "http" # http or websocket + +[llm] +# Ollama configuration +ollama_host = "http://localhost:11434" +default_model = "qwen2.5-coder:7b" +timeout_seconds = 300 + +[logging] +# Log configuration +level = "info" # debug, info, warn, error +file = "~/.ai-shell/ai-shell.log" +max_size_mb = 10 +max_backups = 3 + +[cli] +# CLI behavior +history_file = "~/.ai-shell/history" +max_history = 1000 +prompt = "ai> " +color_output = true + +[security] +# Security settings +allowed_commands = ["ls", "cat", "grep", "find", "git", "cargo", "npm", "python"] +sandbox_mode = false +max_file_size_mb = 50 \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..5b475c2 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,115 @@ +# ai.shell Architecture + +## 全体構成 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ User Input │ │ ai-shell CLI │ │ MCP Server │ +│ (Terminal) │────▶│ (Rust) │────▶│ (Python) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + │ ▼ + │ ┌─────────────────┐ + │ │ Local LLM │ + │ │ (Ollama) │ + │ └─────────────────┘ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ File System │ │ Code Analysis │ + │ Operations │ │ & Generation │ + └─────────────────┘ └─────────────────┘ +``` + +## MCP Serverの主要な役割 + +### 1. **LLMとの通信ハブ** +- ローカルLLM(Ollama)との通信を管理 +- プロンプトの最適化と前処理 +- レスポンスの後処理とフォーマット + +### 2. **ツール実行エンジン** +MCP Serverは以下のツール(関数)を提供: + +```python +# 既存の実装から +- code_with_local_llm() # コード生成 +- read_file_with_analysis() # ファイル読み込み&分析 +- write_code_to_file() # コード書き込み +- debug_with_llm() # デバッグ支援 +- explain_code() # コード説明 +- execute_command() # シェルコマンド実行 +- git_operations() # Git操作 +``` + +### 3. **コンテキスト管理** +- プロジェクトのコンテキスト保持 +- 会話履歴の管理 +- ファイルの依存関係追跡 + +### 4. **セキュリティレイヤー** +- コマンドのサンドボックス実行 +- ファイルアクセス権限の管理 +- 危険な操作の検証 + +## Rust CLI ↔ MCP Server 通信 + +### 通信プロトコル +```rust +// Rust側のリクエスト例 +struct MCPRequest { + method: String, // "code_with_local_llm" + params: Value, // {"prompt": "...", "language": "rust"} + context: ProjectContext, +} + +// MCP Server側のレスポンス +{ + "result": { + "code": "fn main() { ... }", + "explanation": "This creates a simple HTTP server...", + "files_created": ["src/server.rs"] + }, + "error": null +} +``` + +### 実装の流れ + +1. **ユーザー入力(Rust CLI)** + ``` + ai> create a web server in rust + ``` + +2. **リクエスト構築(Rust)** + - プロンプト解析 + - コンテキスト収集(現在のディレクトリ、プロジェクトタイプなど) + +3. **MCP Server処理(Python)** + - ツール選択(code_with_local_llm) + - LLMへのプロンプト送信 + - コード生成 + - ファイル操作 + +4. **レスポンス処理(Rust)** + - 結果の表示 + - ファイル変更の確認 + - 次のアクションの提案 + +## なぜMCP Serverが必要か + +### 1. **言語の分離** +- Python: AI/MLエコシステムが充実 +- Rust: 高速で安全なCLI実装 + +### 2. **拡張性** +- 新しいLLMの追加が容易 +- ツールの追加・変更が独立 + +### 3. **再利用性** +- 他のクライアント(VS Code拡張など)からも利用可能 +- APIとして公開可能 + +### 4. **プロセス分離** +- LLMの重い処理とUIの軽快な動作を分離 +- クラッシュ時の影響を最小化 \ No newline at end of file diff --git a/docs/development-status.md b/docs/development-status.md new file mode 100644 index 0000000..f5e5fbb --- /dev/null +++ b/docs/development-status.md @@ -0,0 +1,153 @@ +# ai.shell 開発状況 + +## プロジェクト概要 +- **目的**: Claude Codeのようなローカル環境で動作するAIコーディングアシスタント +- **アーキテクチャ**: Rust CLI + Python MCP Server + Ollama (Local LLM) +- **作者**: syui +- **リポジトリ**: https://git.syui.ai/ai/shell + +## 現在の実装状況 + +### ✅ 完了した機能 + +1. **基本アーキテクチャ** + - Rust CLIクライアント + - Python MCPサーバー + - HTTP REST API通信 + +2. **環境構築** + - `~/.config/ai-shell/` にPython venv環境 + - セットアップスクリプト (`make setup`) + - 起動スクリプト + +3. **CLI機能** + - インタラクティブモード + - ワンショット実行 (`ai-shell exec "prompt"`) + - ファイル分析 (`ai-shell analyze file`) + - ヘルスチェック (`ai-shell health`) + +4. **コマンド** + - `/help` - ヘルプ表示 + - `/clear` - 画面クリア + - `/model [name]` - モデル切り替え(UI実装済み、機能は未実装) + - `/analyze ` - ファイル分析 + - `/create ` - プロジェクト作成 + +5. **MCP Server API** + - `POST /execute` - ツール実行 + - `GET /health` - ヘルスチェック + - `GET /tools` - 利用可能ツール一覧 + +## 🚧 未実装・改善が必要な機能 + +### 高優先度 +1. **会話履歴の保持** + - 現在は単発のリクエストのみ + - コンテキストを維持した連続的な会話 + +2. **ファイル操作の安全性** + - ファイル書き込み前の確認 + - バックアップ機能 + - undo/redo機能 + +3. **エラーハンドリング** + - Ollamaが起動していない場合の対処 + - ネットワークエラーの適切な処理 + +### 中優先度 +1. **モデル切り替え機能** + - `/model` コマンドの実装 + - 設定ファイルへの保存 + +2. **Git統合** + - `git diff` の解析 + - コミットメッセージ生成 + - ブランチ情報の取得 + +3. **プロジェクトテンプレート** + - より多くの言語サポート + - カスタムテンプレート + +### 低優先度 +1. **TUIモード** + - ratatuiを使用した分割画面 + - プログレスバー表示 + +2. **プラグインシステム** + - カスタムツールの追加 + - 外部コマンドの統合 + +## 技術的な詳細 + +### ディレクトリ構造 +``` +/Users/syui/ai/shell/ +├── src/ +│ ├── main.rs # CLIエントリーポイント +│ ├── lib.rs # ライブラリルート +│ ├── mcp_client.rs # MCPクライアント実装 +│ ├── config.rs # 設定管理 +│ ├── prompt.rs # プロンプト表示 +│ └── commands.rs # コマンドハンドラー +├── mcp_server.py # MCPサーバー実装 +├── requirements.txt # Python依存関係 +├── Cargo.toml # Rust依存関係 +└── scripts/ + ├── setup.sh # セットアップスクリプト + └── start.sh # 起動スクリプト + +~/.config/ai-shell/ # ユーザー設定ディレクトリ +├── venv/ # Python仮想環境 +├── config.toml # ユーザー設定 +├── mcp_server.py # MCPサーバーコピー +└── bin/ + ├── ai-shell # CLIラッパー + └── mcp-server # サーバーラッパー +``` + +### 主要な依存関係 +- **Rust**: clap, tokio, reqwest, colored, serde +- **Python**: aiohttp, requests, toml +- **LLM**: Ollama (qwen2.5-coder:7b) + +## 次回の開発時の開始方法 + +```bash +# リポジトリに移動 +cd /Users/syui/ai/shell + +# 現在の状態を確認 +git status + +# サーバーを起動(Terminal 1) +make run-server + +# CLIを起動(Terminal 2) +./target/debug/ai-shell + +# または、セットアップ済みなら +ai-shell +``` + +## 重要な設計思想 + +1. **分離された責務** + - Rust: 高速なCLI、ユーザーインターフェース + - Python: AI/ML処理、MCPサーバー + - Ollama: ローカルLLM実行 + +2. **プライバシー重視** + - 完全ローカル動作 + - 外部APIへの依存なし + +3. **拡張性** + - MCPプロトコルによる標準化 + - 新しいツールの追加が容易 + +## 参考リンク +- [プロジェクト哲学 (claude.md)](/Users/syui/ai/shell/claude.md) +- [MCPプロトコル仕様](/Users/syui/ai/shell/docs/mcp-protocol.md) +- [アーキテクチャ図](/Users/syui/ai/shell/docs/architecture.md) + +--- +最終更新: 2025-06-01 \ No newline at end of file diff --git a/docs/mcp-protocol.md b/docs/mcp-protocol.md new file mode 100644 index 0000000..640f43b --- /dev/null +++ b/docs/mcp-protocol.md @@ -0,0 +1,88 @@ +# MCP Protocol Specification for ai.shell + +## 通信プロトコル + +### 基本仕様 +- プロトコル: HTTP REST API +- エンコーディング: JSON +- 認証: なし(ローカル使用のため) + +### エンドポイント + +#### 1. ツール実行 +``` +POST /execute +Content-Type: application/json + +{ + "id": "uuid-v4", + "method": "tool_name", + "params": { + // tool specific parameters + }, + "context": { + "current_dir": "/path/to/project", + "project_type": "rust", + "files": ["src/main.rs", "Cargo.toml"], + "git_branch": "main" + } +} + +Response: +{ + "id": "uuid-v4", + "result": { + // tool specific result + }, + "error": null +} +``` + +#### 2. 利用可能ツール一覧 +``` +GET /tools + +Response: +{ + "tools": [ + { + "name": "code_with_local_llm", + "description": "Generate code using local LLM", + "parameters": { + "prompt": "string", + "language": "string" + } + }, + // ... + ] +} +``` + +#### 3. ヘルスチェック +``` +GET /health + +Response: +{ + "status": "ok", + "llm_status": "connected", + "version": "0.1.0" +} +``` + +## エラーコード + +| Code | Description | +|------|-------------| +| 1001 | Invalid method | +| 1002 | Missing parameters | +| 1003 | LLM connection failed | +| 1004 | File operation failed | +| 1005 | Command execution failed | +| 2001 | Internal server error | + +## タイムアウト設定 + +- デフォルト: 300秒(LLM処理) +- ファイル操作: 10秒 +- コマンド実行: 60秒 \ No newline at end of file diff --git a/example/custom_llm_mcp_server.py b/example/custom_llm_mcp_server.py new file mode 100644 index 0000000..e5f95c9 --- /dev/null +++ b/example/custom_llm_mcp_server.py @@ -0,0 +1,28 @@ +# custom_llm_mcp_server.py +import asyncio +import json +from mcp.server import Server +from mcp.types import Tool, TextContent +import requests + +app = Server("local-llm-mcp") + +@app.tool("run_local_llm") +async def run_local_llm(prompt: str, model: str = "qwen2.5-coder:14b") -> str: + """ローカルLLMでコード生成・分析を実行""" + response = requests.post("http://localhost:11434/api/generate", json={ + "model": model, + "prompt": prompt, + "stream": False + }) + return response.json()["response"] + +@app.tool("execute_code") +async def execute_code(code: str, language: str = "python") -> str: + """生成されたコードを実行""" + # セキュアな実行環境でコード実行 + # Docker containerやsandbox環境推奨 + pass + +if __name__ == "__main__": + asyncio.run(app.run()) diff --git a/mcp_server.py b/mcp_server.py new file mode 100644 index 0000000..6feb366 --- /dev/null +++ b/mcp_server.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +ai.shell MCP Server +A Model Context Protocol server for ai.shell CLI +""" + +import asyncio +import json +import logging +from typing import Dict, Any, Optional +from pathlib import Path +import sys +import os +sys.path.append(str(Path(__file__).parent / "docs")) + +from aiohttp import web +import aiohttp_cors + +# Import LocalLLMServer implementation +import requests + +class LocalLLMServer: + """Simplified LocalLLMServer for ai.shell""" + def __init__(self, model: str = "qwen2.5-coder:7b"): + self.model = model + self.ollama_url = "http://localhost:11434" + + async def code_with_local_llm(self, prompt: str, language: str = "python") -> Dict[str, Any]: + """Generate code using local LLM""" + system_prompt = f"You are an expert {language} programmer. Generate clean, well-commented code." + + try: + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": self.model, + "prompt": f"{system_prompt}\n\nUser: {prompt}\n\nPlease provide the code:", + "stream": False, + "options": { + "temperature": 0.1, + "top_p": 0.95, + } + }, + timeout=300 + ) + + if response.status_code == 200: + result = response.json() + code = result.get("response", "") + return {"code": code, "language": language} + else: + return {"error": f"Ollama returned status {response.status_code}"} + + except Exception as e: + logging.error(f"Error calling Ollama: {e}") + return {"error": str(e)} + + async def read_file_with_analysis(self, file_path: str, analysis_prompt: str = "Analyze this file") -> Dict[str, Any]: + """Read and analyze a file""" + try: + with open(file_path, 'r') as f: + content = f.read() + + # Detect language from file extension + ext = Path(file_path).suffix + language_map = { + '.py': 'python', + '.rs': 'rust', + '.js': 'javascript', + '.ts': 'typescript', + '.go': 'go', + '.java': 'java', + '.cpp': 'cpp', + '.c': 'c', + } + language = language_map.get(ext, 'text') + + prompt = f"{analysis_prompt}\n\nFile: {file_path}\nLanguage: {language}\n\nContent:\n{content}" + + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": self.model, + "prompt": prompt, + "stream": False, + }, + timeout=300 + ) + + if response.status_code == 200: + result = response.json() + analysis = result.get("response", "") + return {"analysis": analysis, "file_path": file_path} + else: + return {"error": f"Analysis failed: {response.status_code}"} + + except Exception as e: + return {"error": str(e)} + + async def explain_code(self, code: str, language: str = "python") -> Dict[str, Any]: + """Explain code snippet""" + prompt = f"Explain this {language} code in detail:\n\n{code}" + + try: + response = requests.post( + f"{self.ollama_url}/api/generate", + json={ + "model": self.model, + "prompt": prompt, + "stream": False, + }, + timeout=300 + ) + + if response.status_code == 200: + result = response.json() + explanation = result.get("response", "") + return {"explanation": explanation} + else: + return {"error": f"Explanation failed: {response.status_code}"} + + except Exception as e: + return {"error": str(e)} + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class AiShellMCPServer: + def __init__(self, config_path: Optional[str] = None): + self.config = self.load_config(config_path) + self.llm_server = LocalLLMServer() + self.app = web.Application() + self.setup_routes() + + def load_config(self, config_path: Optional[str]) -> Dict[str, Any]: + """Load configuration from TOML file""" + if config_path and Path(config_path).exists(): + import toml + return toml.load(config_path) + else: + # Default configuration + return { + "server": { + "host": "127.0.0.1", + "port": 8765 + }, + "llm": { + "ollama_host": "http://localhost:11434", + "default_model": "qwen2.5-coder:7b" + } + } + + def setup_routes(self): + """Setup HTTP routes""" + # Configure CORS + cors = aiohttp_cors.setup(self.app, defaults={ + "*": aiohttp_cors.ResourceOptions( + allow_credentials=True, + expose_headers="*", + allow_headers="*", + allow_methods="*" + ) + }) + + # Add routes + resource = self.app.router.add_resource("/execute") + cors.add(resource.add_route("POST", self.handle_execute)) + + resource = self.app.router.add_resource("/tools") + cors.add(resource.add_route("GET", self.handle_tools)) + + resource = self.app.router.add_resource("/health") + cors.add(resource.add_route("GET", self.handle_health)) + + async def handle_execute(self, request: web.Request) -> web.Response: + """Execute a tool request""" + try: + data = await request.json() + method = data.get("method") + params = data.get("params", {}) + context = data.get("context", {}) + + logger.info(f"Executing method: {method}") + + # Map method to tool + if method == "code_with_local_llm": + result = await self.llm_server.code_with_local_llm( + prompt=params.get("prompt"), + language=params.get("language", "python") + ) + elif method == "read_file_with_analysis": + result = await self.llm_server.read_file_with_analysis( + file_path=params.get("file_path"), + analysis_prompt=params.get("analysis_prompt", "Analyze this file") + ) + elif method == "explain_code": + result = await self.llm_server.explain_code( + code=params.get("code"), + language=params.get("language", "python") + ) + else: + return web.json_response({ + "id": data.get("id"), + "error": f"Unknown method: {method}", + "error_code": 1001 + }, status=400) + + return web.json_response({ + "id": data.get("id"), + "result": result, + "error": None + }) + + except Exception as e: + logger.error(f"Error executing request: {e}") + return web.json_response({ + "id": data.get("id", ""), + "error": str(e), + "error_code": 2001 + }, status=500) + + async def handle_tools(self, request: web.Request) -> web.Response: + """Return available tools""" + tools = [ + { + "name": "code_with_local_llm", + "description": "Generate code using local LLM", + "parameters": { + "prompt": "string", + "language": "string (optional)" + } + }, + { + "name": "read_file_with_analysis", + "description": "Read and analyze a file", + "parameters": { + "file_path": "string", + "analysis_prompt": "string (optional)" + } + }, + { + "name": "explain_code", + "description": "Explain code snippet", + "parameters": { + "code": "string", + "language": "string (optional)" + } + } + ] + + return web.json_response({"tools": tools}) + + async def handle_health(self, request: web.Request) -> web.Response: + """Health check endpoint""" + # Check LLM connection + llm_status = "unknown" + try: + # Simple check - you might want to implement a proper health check + llm_status = "connected" + except: + llm_status = "disconnected" + + return web.json_response({ + "status": "ok", + "llm_status": llm_status, + "version": "0.1.0" + }) + + def run(self): + """Start the server""" + host = self.config["server"]["host"] + port = self.config["server"]["port"] + + logger.info(f"Starting ai.shell MCP Server on {host}:{port}") + web.run_app(self.app, host=host, port=port) + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="ai.shell MCP Server") + parser.add_argument("--config", help="Path to configuration file") + args = parser.parse_args() + + server = AiShellMCPServer(config_path=args.config) + server.run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..709a9af --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# ai.shell MCP Server dependencies +aiohttp==3.9.5 +aiohttp-cors==0.7.0 +toml==0.10.2 +requests==2.31.0 \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..dd629e4 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# ai.shell setup script for macOS +# Sets up Python venv environment in ~/.config/ai-shell + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Directories +CONFIG_DIR="$HOME/.config/ai-shell" +VENV_DIR="$CONFIG_DIR/venv" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +echo -e "${BLUE}🚀 Setting up ai.shell environment...${NC}" + +# Create config directory +echo -e "${YELLOW}📁 Creating config directory...${NC}" +mkdir -p "$CONFIG_DIR" + +# Check Python version +echo -e "${YELLOW}🐍 Checking Python version...${NC}" +if ! command -v python3 &> /dev/null; then + echo -e "${RED}❌ Python 3 is not installed. Please install Python 3.9 or later.${NC}" + exit 1 +fi + +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +echo -e "${GREEN}✓ Found Python $PYTHON_VERSION${NC}" + +# Create virtual environment +if [ ! -d "$VENV_DIR" ]; then + echo -e "${YELLOW}🔧 Creating virtual environment...${NC}" + python3 -m venv "$VENV_DIR" + echo -e "${GREEN}✓ Virtual environment created${NC}" +else + echo -e "${GREEN}✓ Virtual environment already exists${NC}" +fi + +# Activate virtual environment +echo -e "${YELLOW}🔌 Activating virtual environment...${NC}" +source "$VENV_DIR/bin/activate" + +# Upgrade pip +echo -e "${YELLOW}📦 Upgrading pip...${NC}" +pip install --upgrade pip + +# Install Python dependencies +echo -e "${YELLOW}📦 Installing Python dependencies...${NC}" +pip install -r "$PROJECT_DIR/requirements.txt" + +# Copy MCP server to config directory +echo -e "${YELLOW}📄 Copying MCP server...${NC}" +cp "$PROJECT_DIR/mcp_server.py" "$CONFIG_DIR/" + +# Create config file if it doesn't exist +if [ ! -f "$CONFIG_DIR/config.toml" ]; then + echo -e "${YELLOW}⚙️ Creating default config...${NC}" + cp "$PROJECT_DIR/config/default.toml" "$CONFIG_DIR/config.toml" +fi + +# Create bin directory for scripts +mkdir -p "$CONFIG_DIR/bin" + +# Create MCP server startup script +cat > "$CONFIG_DIR/bin/mcp-server" << 'EOF' +#!/bin/bash +# MCP Server startup script + +CONFIG_DIR="$HOME/.config/ai-shell" +VENV_DIR="$CONFIG_DIR/venv" + +# Activate virtual environment +source "$VENV_DIR/bin/activate" + +# Start MCP server +cd "$CONFIG_DIR" +python mcp_server.py "$@" +EOF + +chmod +x "$CONFIG_DIR/bin/mcp-server" + +# Create ai-shell wrapper script +cat > "$CONFIG_DIR/bin/ai-shell" << EOF +#!/bin/bash +# ai-shell wrapper script + +CONFIG_DIR="\$HOME/.config/ai-shell" +PROJECT_DIR="$PROJECT_DIR" + +# Check if MCP server is running +if ! curl -s http://localhost:8765/health > /dev/null 2>&1; then + echo "Starting MCP server..." + "\$CONFIG_DIR/bin/mcp-server" > /dev/null 2>&1 & + + # Wait for server to start + for i in {1..10}; do + if curl -s http://localhost:8765/health > /dev/null 2>&1; then + break + fi + sleep 0.5 + done +fi + +# Run ai-shell CLI +cd "\$PROJECT_DIR" +cargo run --release -- "\$@" +EOF + +chmod +x "$CONFIG_DIR/bin/ai-shell" + +# Check if Ollama is installed +echo -e "${YELLOW}🦙 Checking Ollama...${NC}" +if ! command -v ollama &> /dev/null; then + echo -e "${YELLOW}⚠️ Ollama is not installed.${NC}" + echo -e " Please install Ollama from: https://ollama.ai" + echo -e " Then run: ollama pull qwen2.5-coder:7b" +else + echo -e "${GREEN}✓ Ollama is installed${NC}" + + # Check if model is available + if ! ollama list | grep -q "qwen2.5-coder:7b"; then + echo -e "${YELLOW}📥 Pulling qwen2.5-coder:7b model...${NC}" + echo -e " This may take a while..." + ollama pull qwen2.5-coder:7b + else + echo -e "${GREEN}✓ Model qwen2.5-coder:7b is available${NC}" + fi +fi + +# Build Rust project +echo -e "${YELLOW}🔨 Building Rust CLI...${NC}" +cd "$PROJECT_DIR" +cargo build --release + +echo -e "\n${GREEN}✨ Setup complete!${NC}" +echo -e "\n${BLUE}To use ai.shell:${NC}" +echo -e "1. Add to your PATH: ${YELLOW}export PATH=\"\$HOME/.config/ai-shell/bin:\$PATH\"${NC}" +echo -e "2. Run: ${YELLOW}ai-shell${NC}" +echo -e "\nOr use the full path: ${YELLOW}~/.config/ai-shell/bin/ai-shell${NC}" + +# Deactivate virtual environment +deactivate \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..b9a9aee --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# ai.shell startup script (uses ~/.config/ai-shell setup) + +set -e + +CONFIG_DIR="$HOME/.config/ai-shell" + +# Check if setup has been run +if [ ! -d "$CONFIG_DIR/venv" ]; then + echo "❌ ai.shell is not set up yet." + echo "Please run: ./scripts/setup.sh" + exit 1 +fi + +# Run ai-shell +exec "$CONFIG_DIR/bin/ai-shell" "$@" \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..e85b6da --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,185 @@ +use crate::mcp_client::MCPClient; +use crate::prompt::Prompt; +use std::error::Error; +use std::path::Path; + +pub struct CommandHandler { + client: MCPClient, + pub prompt: Prompt, +} + +impl CommandHandler { + pub fn new(client: MCPClient, prompt: Prompt) -> Self { + Self { client, prompt } + } + + pub async fn handle(&self, input: &str) -> Result<(), Box> { + let parts: Vec<&str> = input.split_whitespace().collect(); + + if parts.is_empty() { + return Ok(()); + } + + // Check for special commands + match parts[0] { + "/help" | "/h" => self.show_help(), + "/clear" => self.clear_screen(), + "/model" => self.handle_model_command(&parts).await?, + "/analyze" => self.handle_analyze_command(&parts).await?, + "/create" => self.handle_create_command(&parts).await?, + _ => { + // Regular prompt - send to LLM + self.handle_prompt(input).await?; + } + } + + Ok(()) + } + + fn show_help(&self) { + self.prompt.info("Available commands:"); + println!(" /help, /h - Show this help message"); + println!(" /clear - Clear the screen"); + println!(" /model [name] - Switch LLM model"); + println!(" /analyze - Analyze a file"); + println!(" /create - Create a new project/file"); + println!(" exit, quit - Exit the program"); + println!("\nJust type your request to interact with AI"); + println!("\nExamples:"); + println!(" create a web server in rust"); + println!(" explain this error: [paste error]"); + println!(" /analyze src/main.rs"); + println!(" /create rust my-project"); + } + + fn clear_screen(&self) { + print!("\x1B[2J\x1B[1;1H"); + } + + async fn handle_model_command(&self, parts: &[&str]) -> Result<(), Box> { + if parts.len() < 2 { + self.prompt.info("Current model: qwen2.5-coder:7b"); // TODO: Get from config + self.prompt.info("Available models: qwen2.5-coder:7b, codellama:7b, deepseek-coder:6.7b"); + } else { + // TODO: Implement model switching + self.prompt.success(&format!("Switched to model: {}", parts[1])); + } + Ok(()) + } + + async fn handle_analyze_command(&self, parts: &[&str]) -> Result<(), Box> { + if parts.len() < 2 { + self.prompt.error("Usage: /analyze "); + return Ok(()); + } + + let file_path = parts[1]; + if !Path::new(file_path).exists() { + self.prompt.error(&format!("File not found: {}", file_path)); + return Ok(()); + } + + self.prompt.info(&format!("Analyzing file: {}", file_path)); + + match self.client.analyze_file(file_path).await { + Ok(analysis) => { + self.prompt.success("Analysis complete:"); + println!("{}", analysis); + } + Err(e) => { + self.prompt.error(&format!("Analysis failed: {}", e)); + } + } + + Ok(()) + } + + async fn handle_create_command(&self, parts: &[&str]) -> Result<(), Box> { + if parts.len() < 3 { + self.prompt.error("Usage: /create "); + self.prompt.info("Types: rust, python, node, react"); + return Ok(()); + } + + let project_type = parts[1]; + let project_name = parts[2]; + + let prompt_text = match project_type { + "rust" => format!("Create a new Rust project named '{}' with a basic structure including Cargo.toml, src/main.rs, and README.md", project_name), + "python" => format!("Create a new Python project named '{}' with setup.py, requirements.txt, src/__init__.py, and README.md", project_name), + "node" => format!("Create a new Node.js project named '{}' with package.json, index.js, and README.md", project_name), + "react" => format!("Create a new React app structure for '{}' with basic components", project_name), + _ => { + self.prompt.error(&format!("Unknown project type: {}", project_type)); + return Ok(()); + } + }; + + self.prompt.info(&format!("Creating {} project: {}", project_type, project_name)); + self.handle_prompt(&prompt_text).await?; + + Ok(()) + } + + async fn handle_prompt(&self, prompt: &str) -> Result<(), Box> { + self.prompt.info("Processing your request..."); + + // Detect the context and language + let language = self.detect_language(prompt); + + match self.client.generate_code(prompt, &language).await { + Ok(code) => { + if !code.is_empty() { + self.prompt.success("Generated code:"); + self.prompt.code_block(&code, &language); + + // Ask if user wants to save the code + self.prompt.info("Would you like to save this code? (y/n)"); + if let Ok(response) = self.prompt.read_input() { + if response.to_lowercase() == "y" { + // TODO: Implement file saving logic + self.prompt.info("Enter filename:"); + if let Ok(filename) = self.prompt.read_input() { + match std::fs::write(&filename, &code) { + Ok(_) => self.prompt.success(&format!("Code saved to: {}", filename)), + Err(e) => self.prompt.error(&format!("Failed to save file: {}", e)), + } + } + } + } + } else { + self.prompt.info("No code was generated. The AI might have provided an explanation instead."); + } + } + Err(e) => { + self.prompt.error(&format!("Failed to generate code: {}", e)); + } + } + + Ok(()) + } + + fn detect_language(&self, prompt: &str) -> String { + let prompt_lower = prompt.to_lowercase(); + + if prompt_lower.contains("rust") || prompt_lower.contains("cargo") { + "rust".to_string() + } else if prompt_lower.contains("python") || prompt_lower.contains("pip") { + "python".to_string() + } else if prompt_lower.contains("javascript") || prompt_lower.contains("node") || prompt_lower.contains("npm") { + "javascript".to_string() + } else if prompt_lower.contains("typescript") { + "typescript".to_string() + } else if prompt_lower.contains("go") || prompt_lower.contains("golang") { + "go".to_string() + } else if prompt_lower.contains("java") && !prompt_lower.contains("javascript") { + "java".to_string() + } else if prompt_lower.contains("c++") || prompt_lower.contains("cpp") { + "cpp".to_string() + } else if prompt_lower.contains("c#") || prompt_lower.contains("csharp") { + "csharp".to_string() + } else { + "text".to_string() + } + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..805abd2 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,128 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use dirs::home_dir; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Config { + pub server: ServerConfig, + pub llm: LLMConfig, + pub logging: LoggingConfig, + pub cli: CLIConfig, + pub security: SecurityConfig, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ServerConfig { + pub host: String, + pub port: u16, + pub protocol: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LLMConfig { + pub ollama_host: String, + pub default_model: String, + pub timeout_seconds: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LoggingConfig { + pub level: String, + pub file: String, + pub max_size_mb: u64, + pub max_backups: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CLIConfig { + pub history_file: String, + pub max_history: usize, + pub prompt: String, + pub color_output: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SecurityConfig { + pub allowed_commands: Vec, + pub sandbox_mode: bool, + pub max_file_size_mb: u64, +} + +impl Config { + pub fn load() -> Result> { + let config_path = Self::config_path(); + + if config_path.exists() { + let content = std::fs::read_to_string(&config_path)?; + let config: Config = toml::from_str(&content)?; + Ok(config) + } else { + Ok(Self::default()) + } + } + + pub fn config_path() -> PathBuf { + home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".config") + .join("ai-shell") + .join("config.toml") + } + + pub fn expand_path(path: &str) -> PathBuf { + if path.starts_with("~") { + home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(path.trim_start_matches("~/")) + } else { + PathBuf::from(path) + } + } + + pub fn server_url(&self) -> String { + format!("{}://{}:{}", self.server.protocol, self.server.host, self.server.port) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + server: ServerConfig { + host: "127.0.0.1".to_string(), + port: 8765, + protocol: "http".to_string(), + }, + llm: LLMConfig { + ollama_host: "http://localhost:11434".to_string(), + default_model: "qwen2.5-coder:7b".to_string(), + timeout_seconds: 300, + }, + logging: LoggingConfig { + level: "info".to_string(), + file: "~/.config/ai-shell/ai-shell.log".to_string(), + max_size_mb: 10, + max_backups: 3, + }, + cli: CLIConfig { + history_file: "~/.config/ai-shell/history".to_string(), + max_history: 1000, + prompt: "ai> ".to_string(), + color_output: true, + }, + security: SecurityConfig { + allowed_commands: vec![ + "ls".to_string(), + "cat".to_string(), + "grep".to_string(), + "find".to_string(), + "git".to_string(), + "cargo".to_string(), + "npm".to_string(), + "python".to_string(), + ], + sandbox_mode: false, + max_file_size_mb: 50, + }, + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eaa0535 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +pub mod mcp_client; +pub mod config; +pub mod prompt; +pub mod commands; + +pub use mcp_client::{MCPClient, MCPRequest, MCPResponse, ProjectContext}; +pub use config::Config; +pub use prompt::Prompt; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..566a030 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,163 @@ +use clap::{Parser, Subcommand}; +use colored::*; +use tokio; + +use ai_shell::{Config, MCPClient, Prompt}; +use ai_shell::commands::CommandHandler; + +#[derive(Parser)] +#[command(name = "ai-shell")] +#[command(about = "AI-powered shell for code generation and automation")] +#[command(version = "0.1.0")] +struct Cli { + #[command(subcommand)] + command: Option, + + /// Configuration file path + #[arg(short, long)] + config: Option, + + /// Run in TUI mode (future feature) + #[arg(long)] + tui: bool, +} + +#[derive(Subcommand)] +enum Commands { + /// Start interactive session + Chat, + /// Execute a single command + Exec { + /// The prompt to execute + prompt: String + }, + /// Analyze a file + Analyze { + /// Path to the file to analyze + file: String + }, + /// Check server health + Health, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + if cli.tui { + eprintln!("TUI mode is not yet implemented"); + return Ok(()); + } + + // Load configuration + let config = Config::load()?; + + // Initialize MCP client + let client = MCPClient::new(&config.server_url()); + + // Check server health + if !check_server_health(&client).await { + eprintln!("{}", "Error: MCP server is not running".red()); + eprintln!("Please start the server with: make run-server"); + return Ok(()); + } + + match cli.command { + None | Some(Commands::Chat) => interactive_mode(client, config).await, + Some(Commands::Exec { prompt }) => execute_once(client, config, &prompt).await, + Some(Commands::Analyze { file }) => analyze_file(client, config, &file).await, + Some(Commands::Health) => { + println!("{}", "MCP server is healthy".green()); + Ok(()) + } + } +} + +async fn check_server_health(_client: &MCPClient) -> bool { + // Use a simple HTTP GET request for health check + match reqwest::get("http://localhost:8765/health").await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(json) => json["status"] == "ok", + Err(_) => false, + } + } else { + false + } + } + Err(_) => false, + } +} + +async fn interactive_mode(client: MCPClient, config: Config) -> Result<(), Box> { + println!("{}", "AI Shell v0.1.0".bright_blue().bold()); + println!("Type '/help' for commands, 'exit' to quit\n"); + + let prompt = Prompt::new(config.cli.prompt.clone(), config.cli.color_output); + let handler = CommandHandler::new(client, prompt); + + loop { + handler.prompt.display()?; + + let input = handler.prompt.read_input()?; + + match input.as_str() { + "exit" | "quit" => { + handler.prompt.info("Goodbye!"); + break; + } + "" => continue, + _ => { + if let Err(e) = handler.handle(&input).await { + handler.prompt.error(&format!("Error: {}", e)); + } + } + } + } + + Ok(()) +} + +async fn execute_once(client: MCPClient, config: Config, prompt_text: &str) -> Result<(), Box> { + let prompt = Prompt::new(config.cli.prompt, config.cli.color_output); + prompt.info(&format!("Executing: {}", prompt_text)); + + match client.generate_code(prompt_text, "auto").await { + Ok(code) => { + if !code.is_empty() { + prompt.code_block(&code, ""); + } else { + prompt.info("No code generated"); + } + } + Err(e) => { + prompt.error(&format!("Execution failed: {}", e)); + } + } + + Ok(()) +} + +async fn analyze_file(client: MCPClient, config: Config, file: &str) -> Result<(), Box> { + let prompt = Prompt::new(config.cli.prompt, config.cli.color_output); + + if !std::path::Path::new(file).exists() { + prompt.error(&format!("File not found: {}", file)); + return Ok(()); + } + + prompt.info(&format!("Analyzing file: {}", file)); + + match client.analyze_file(file).await { + Ok(analysis) => { + prompt.success("Analysis complete:"); + println!("{}", analysis); + } + Err(e) => { + prompt.error(&format!("Analysis failed: {}", e)); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/mcp_client.rs b/src/mcp_client.rs new file mode 100644 index 0000000..4c99c18 --- /dev/null +++ b/src/mcp_client.rs @@ -0,0 +1,128 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::error::Error; + +#[derive(Debug, Serialize, Deserialize)] +pub struct MCPRequest { + pub method: String, + pub params: Value, + pub context: ProjectContext, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ProjectContext { + pub current_dir: String, + pub project_type: Option, // rust, python, node, etc. + pub files: Vec, + pub git_branch: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MCPResponse { + pub result: Option, + pub error: Option, +} + +pub struct MCPClient { + client: Client, + server_url: String, +} + +impl MCPClient { + pub fn new(server_url: &str) -> Self { + Self { + client: Client::new(), + server_url: server_url.to_string(), + } + } + + pub async fn request(&self, method: &str, params: Value) -> Result> { + let context = self.get_project_context()?; + + let request = MCPRequest { + method: method.to_string(), + params, + context, + }; + + let response = self.client + .post(&format!("{}/execute", self.server_url)) + .json(&request) + .send() + .await? + .json::() + .await?; + + Ok(response) + } + + pub async fn generate_code(&self, prompt: &str, language: &str) -> Result> { + let params = serde_json::json!({ + "prompt": prompt, + "language": language + }); + + let response = self.request("code_with_local_llm", params).await?; + + match response.result { + Some(result) => Ok(result["code"].as_str().unwrap_or("").to_string()), + None => Err(response.error.unwrap_or_else(|| "Unknown error".to_string()).into()), + } + } + + pub async fn analyze_file(&self, file_path: &str) -> Result> { + let params = serde_json::json!({ + "file_path": file_path, + "analysis_type": "general" + }); + + let response = self.request("read_file_with_analysis", params).await?; + + match response.result { + Some(result) => Ok(result["analysis"].as_str().unwrap_or("").to_string()), + None => Err(response.error.unwrap_or_else(|| "Unknown error".to_string()).into()), + } + } + + fn get_project_context(&self) -> Result> { + use std::env; + use std::fs; + + let current_dir = env::current_dir()?.to_string_lossy().to_string(); + + // Detect project type + let project_type = if fs::metadata("Cargo.toml").is_ok() { + Some("rust".to_string()) + } else if fs::metadata("package.json").is_ok() { + Some("node".to_string()) + } else if fs::metadata("requirements.txt").is_ok() || fs::metadata("setup.py").is_ok() { + Some("python".to_string()) + } else { + None + }; + + // Get git branch + let git_branch = std::process::Command::new("git") + .args(&["branch", "--show-current"]) + .output() + .ok() + .and_then(|output| { + if output.status.success() { + String::from_utf8(output.stdout).ok().map(|s| s.trim().to_string()) + } else { + None + } + }); + + // List relevant files + let files = vec![]; // TODO: Implement file listing logic + + Ok(ProjectContext { + current_dir, + project_type, + files, + git_branch, + }) + } +} \ No newline at end of file diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..9b4c25c --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,75 @@ +use colored::*; +use std::io::{self, Write}; + +pub struct Prompt { + pub prompt_text: String, + pub color_enabled: bool, +} + +impl Prompt { + pub fn new(prompt_text: String, color_enabled: bool) -> Self { + Self { + prompt_text, + color_enabled, + } + } + + pub fn display(&self) -> io::Result<()> { + if self.color_enabled { + print!("{}", self.prompt_text.bright_blue().bold()); + } else { + print!("{}", self.prompt_text); + } + io::stdout().flush() + } + + pub fn read_input(&self) -> io::Result { + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) + } + + pub fn success(&self, message: &str) { + if self.color_enabled { + println!("{} {}", "✓".green(), message.green()); + } else { + println!("✓ {}", message); + } + } + + pub fn error(&self, message: &str) { + if self.color_enabled { + eprintln!("{} {}", "✗".red(), message.red()); + } else { + eprintln!("✗ {}", message); + } + } + + pub fn info(&self, message: &str) { + if self.color_enabled { + println!("{} {}", "ℹ".blue(), message); + } else { + println!("ℹ {}", message); + } + } + + pub fn warning(&self, message: &str) { + if self.color_enabled { + println!("{} {}", "⚠".yellow(), message.yellow()); + } else { + println!("⚠ {}", message); + } + } + + pub fn code_block(&self, code: &str, language: &str) { + if self.color_enabled { + println!("\n{}", format!("```{}", language).bright_black()); + println!("{}", code.cyan()); + println!("{}\n", "```".bright_black()); + } else { + println!("\n```{}", language); + println!("{}", code); + println!("```\n"); + } + } +} \ No newline at end of file