スマホだけで完結!TermuxでGemini AIエージェントを自作した話

📅 2026年2月19日 | 📁 開発体験談

📣 2026/02/20 追記あり:記事の末尾に、公式「Gemini CLI」の導入成功レポートを追記しました。

1. はじめに:ジェンスパークのコストが悩みの種だった

このブログ「ジェンスパーク開発奮闘記」は、ブログシステム自体からジェンスパーク(Genspark)で作っています。記事の体裁を整えたり、投稿するのも全部ジェンスパーク任せです。とても便利なのですが、使用するにはコストがかかります。記事を追加するたびにクレジットが減っていくのが、正直なところ悩みの種でした。

そこで「維持管理や記事追加だけ別のツールでできないか」と色々探し始めました。しかし、自分の要望が特殊なのか、しっくりくるものが全然見つかりませんでした。

筆者の要望
  • とにかくスマホだけで完結したい
  • ブログのソースはGitHub管理、Cloudflareで公開、D1データベースに記事保存という構成
  • 自然言語で指示して、デプロイまでの工程を自動化したい

Cursor や Claude Code はPC前提ですし、そもそも有料です。Firebase は環境的には良さそうでしたが、入っているAIエージェントの能力が低く、GitHubなど外部へのアクセスができませんでした。

2. スマホ完結の開発環境を探して

スマホで開発作業を完結させたいというのは、一見すると無茶な要望に聞こえるかもしれません。しかし、20年間ソフトウェアエンジニアをやってきた筆者としては「もうパソコンを広げたくない」というわがままな理由がありました。

スマホのみで自然言語指示 → GitHub操作 → デプロイまで完結できる環境は、2026年現在でも既製品としてはほぼ存在しない。

既存のモバイル向け開発ツールをいくつか試しましたが、GitHubへのアクセス権限がなかったり、自然言語でのコマンド実行ができなかったりと、いずれも要件を満たしませんでした。そこでたどり着いたのが、AndroidスマホにLinux環境を構築できる Termux というアプリです。

3. Termuxとの出会いと苦難のインストール

Termuxは、Android上でLinuxのターミナル環境を動かせるアプリです。bash、python3、git、curlなどの標準的なツールが使えるため、理論上はLinuxでできることの多くがスマホ上で実現できます。

注意:Google Playストア版のTermuxは開発が止まっており、最新のパッケージが入らない場合があります。F-Droid版を使うことを強くおすすめします。

インストール自体がなかなかの難関でした。最初にPlayストア版を入れたものの、開発停止と聞いてF-Droid版へ入れ直し。インストールが途中で固まって何も起きない時間が続き、2回目でなぜかすんなり完了。さらにGoogleからセキュリティ警告が何度も届くという洗礼を受けながら、なんとか環境構築に成功しました。

Termuxのインストール後は pkg update && pkg upgrade でパッケージを最新化し、pkg install git python curl で基本ツールを入れておきましょう。

4. AIエージェントを自作することにした

環境が整ったら次は自然言語で指示できるツールです。Termuxで使えるAIチャットツール(ai chatなど)を探しましたが、どうもTermuxには対応していないようでインストールできず。Gemini公式CLIツールも同様でした。

そこで発想を転換し、「Gemini APIを直接叩いて、自分でエージェントを作ってしまおう」という結論に至りました。必要な材料はbash、python3、curlだけ。どれもTermuxに標準で入っています。

既製品がなければ作ればいい。bash + curl + Gemini APIという最小構成で、自律型AIエージェントは実現できる。

AIエージェントの作成自体は、Claude(Anthropic)に相談しながら進めました。試行錯誤の末、デバッグや設計方針の見直しを繰り返し、最終的にv1.08として動作する状態になりました。

5. エージェントの処理フロー

完成したエージェントの基本的な処理フローはこのようになっています。

処理フロー
ユーザー指示 → ①Geminiが指示を分析しコマンド判断 → コマンド実行(git, curl, npm など) → ②Geminiが結果を判断・説明 → ユーザーが続行/終了/追加指示を選択

ポイントは「Geminiを2回呼ぶ」設計です。1回目(①)で何をすべきかを判断し、コマンドを実行。2回目(②)でその結果を評価し、タスクが完了したか次のステップが必要かを判断します。ユーザーはその②の判断を見た上で y(続行) / n(終了) / 自由なメッセージで追加指示を出せます。

対応している応答タイプ:
  • TYPE: COMMAND — シェルコマンドを実行
  • TYPE: SEARCH — Brave APIまたはDuckDuckGoで検索
  • TYPE: IMAGE — スクリーンショットを解析
  • TYPE: NEED_INPUT — ユーザーへの確認
  • TYPE: DONE — タスク完了
  • TYPE: NEXT — 次のステップへ継続

使用モデルは当初Gemini 2.5 Flashでしたが、能力的に追いつかない場面が多く、Gemini 3.0 Flash Preview(gemini-3-flash-preview)に変更しました。APIキーは1つで全モデルに対応しているため、モデル名を変えるだけで切り替えられます。

6. 出来上がったもの・出来なかったもの

✅ できるようになったこと
  • GitHubへのアクセス(clone、push、pull など)
  • GitHub Actionsを使った自動デプロイ(pushでCloudflareへ自動反映)
  • プロジェクトごとの設定・履歴管理(.ai_config / .ai_history)
  • ちょっとしたソースコード修正やファイル操作
⚠️ できなかったこと・断念したこと
  • 長い文章の記事作成(Gemini APIのトークン制限の関係で、記事作成はジェンスパークのAIチャットで行う)
  • Wranglerを使ったCloudflareへの直接デプロイ(TermuxへのWranglerインストールが困難なため断念し、GitHub Actions経由のCI/CDで代替

最初は「大型バイクが欲しかったけど、苦労の末に手に入れたのは原チャリだった」という感覚もありました(笑)。ただ、スマホ単体でGitHub連携・ビルド・デプロイまでの一連の作業を自然言語で指示できるのは、それなりに実用的です。

Wranglerが使えなくてもGitHub ActionsでCI/CDを組めば、pushするだけでCloudflareへ自動デプロイできる。詰まったら迂回路を探すのがスマホ開発のコツ。

7. どんな人におすすめか

このアプローチが特に合いそうな方を考えてみました。

  • 移動が多いSEやエンジニア — 電車や出張先でのちょっとした修正・確認作業に使えます
  • PCを持ち歩きたくない開発者 — スマホ1台で軽微な開発作業を完結させたい方
  • Linux/bashに慣れている方 — スクリプトをカスタマイズして自分好みの環境に育てられます
  • Gemini APIを試してみたい方 — フレームワーク不要でAPIの動作を手軽に確認できます

逆に、大規模なCLI自動化や複雑なフロントエンド開発には向きません。あくまでも「スマホで軽くGitHub操作や設定変更をしたい」という用途にフィットします。

このスクリプトはTermux固有の部分(スクリーンショットのパスなど)を変えるだけで、通常のLinuxやMacでも動作します。bash + python3 + curlさえあればOKです。

8. 完成スクリプト(全文公開)

試行錯誤の末に完成したスクリプトを全文公開します。ClaudeのAPIを使って作成したものです。中身はほとんど確認していないので、ご利用は自己責任でお願いします。

事前準備:
GOOGLE_API_KEY を環境変数に設定してください。
export GOOGLE_API_KEY='your-api-key'~/.bashrc に追記して source ~/.bashrc を実行します。
APIキーは Google AI Studio から取得できます。

スクリプトを ~/bin/ai として保存し、chmod +x ~/bin/ai で実行権限を付与してください。

#!/bin/bash
# AI Assistant v1.08
# Mobile Development AI Assistant powered by Gemini

# ============================================================
# 定数・環境変数
# ============================================================
GOOGLE_API_KEY="${GOOGLE_API_KEY:-}"
BRAVE_API_KEY="${BRAVE_API_KEY:-}"
MODEL="gemini-3-flash-preview"
API_URL="https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent"

SCREENSHOT_DIR="${HOME}/storage/pictures/Screenshots"
CURRENT_PROJECT_FILE="${HOME}/.ai_current_project"
IMAGE_SCRIPT="${HOME}/bin/ai_image.py"
TASK_RESULT_FILE="${HOME}/tmp/ai_task_result.tmp"
HISTORY_FILE="${HOME}/.ai_history"
TMP_DIR="${HOME}/tmp"
PAYLOAD_FILE="${TMP_DIR}/ai_payload.tmp"

# ============================================================
# プロジェクト管理
# ============================================================
PROJECT_NAME_CURRENT=""
PROJECT_DIR_CURRENT=""
PROJECT_CONFIG=""
PROJECT_CONTEXT=""

load_config() {
  PROJECT_NAME_CURRENT=""
  PROJECT_DIR_CURRENT=""
  PROJECT_CONFIG=""
  PROJECT_CONTEXT=""
  HISTORY_FILE="${HOME}/.ai_history"

  if [[ -f "$CURRENT_PROJECT_FILE" ]]; then
    local project_path
    project_path=$(cat "$CURRENT_PROJECT_FILE")
    if [[ -n "$project_path" && -d "$project_path" ]]; then
      PROJECT_DIR_CURRENT="$project_path"
      PROJECT_NAME_CURRENT=$(basename "$project_path")
      HISTORY_FILE="${PROJECT_DIR_CURRENT}/.ai_history"

      [[ -f "${PROJECT_DIR_CURRENT}/.ai_config" ]] && PROJECT_CONFIG=$(cat "${PROJECT_DIR_CURRENT}/.ai_config")
      [[ -f "${PROJECT_DIR_CURRENT}/.ai_context" ]] && PROJECT_CONTEXT=$(cat "${PROJECT_DIR_CURRENT}/.ai_context")

      while IFS='=' read -r key value; do
        [[ "$key" =~ ^[[:space:]]*# ]] && continue
        [[ -z "$key" ]] && continue
        key=$(echo "$key" | tr -d ' ')
        value=$(echo "$value" | tr -d '"' | tr -d "'")
        export "$key=$value"
      done < <(grep '=' "${PROJECT_DIR_CURRENT}/.ai_config" 2>/dev/null)
    fi
  fi
}

restore_last_project() { load_config; }

detect_project_switch() {
  local input="$1"
  echo "$input" | grep -qiE "プロジェクト.*(切|替|変)|switch.*project|project.*switch"
}

# ============================================================
# システムプロンプト
# ============================================================
build_system_prompt() {
  cat << SYSPROMPT
あなたはモバイル開発支援AIアシスタントです。

## 【設定ファイル】プロジェクト情報
- プロジェクト名: ${PROJECT_NAME_CURRENT:-未選択}
- プロジェクトディレクトリ: ${PROJECT_DIR_CURRENT:-未設定}
- 履歴ファイル: ${HISTORY_FILE}

## 【設定ファイル】.ai_config
${PROJECT_CONFIG:-設定なし}

## 【設定ファイル】.ai_context
${PROJECT_CONTEXT:-コンテキストなし}

## コマンド実行ルール(最重要・厳守)
- git, curl, npm, cp, mv などは必ずCOMMAND形式で自分で実行すること
- 「ユーザーが実行してください」は絶対禁止
- cdコマンド単体は使わず「cd /path && 次のコマンド」の形式にすること

## 応答形式(TYPE行を最初に出力)
TYPE: COMMAND / SEARCH / IMAGE / NEED_INPUT / DONE / NEXT
SYSPROMPT
}

build_continuation_prompt() {
  local original_task="$1"
  local last_result="$2"
  cat << CONTPROMPT
## 【元のタスク】
${original_task}

## 【直前の実行結果】
${last_result}

タスクは完了しましたか?結果をわかりやすく説明した上で継続判断してください。
応答形式: TYPE: DONE / NEXT / NEED_INPUT
CONTPROMPT
}

# ============================================================
# Gemini API呼び出し
# ============================================================
_call_gemini_with_prompt() {
  local user_message="$1"
  local system_prompt history_content payload response text
  system_prompt=$(build_system_prompt)
  [[ -f "$HISTORY_FILE" ]] && history_content=$(tail -50 "$HISTORY_FILE")

  payload=$(python3 -c "
import json, sys
sp = sys.argv[1]; msg = sys.argv[2]; hist = sys.argv[3]
full_sys = sp + '\n\n## 【過去の会話履歴】\n' + (hist if hist else '履歴なし') + '\n---以上が過去の履歴---'
print(json.dumps({'system_instruction':{'parts':[{'text':full_sys}]},'contents':[{'role':'user','parts':[{'text':msg}]}],'generationConfig':{'temperature':0.7,'maxOutputTokens':2048}}))
" "$system_prompt" "$user_message" "$history_content" 2>/dev/null)

  [[ -z "$payload" ]] && echo "ERROR: payload build failed" >&2 && return 1
  echo "$payload" > "$PAYLOAD_FILE"

  response=$(curl -s -X POST "${API_URL}?key=${GOOGLE_API_KEY}" \
    -H "Content-Type: application/json" -d @"$PAYLOAD_FILE" 2>/dev/null)
  [[ -z "$response" ]] && echo "ERROR: empty response" >&2 && return 1

  text=$(echo "$response" | python3 -c "
import json,sys
try:
  d=json.load(sys.stdin); print(d['candidates'][0]['content']['parts'][0]['text'])
except Exception as e:
  print('ERROR:'+str(e),file=sys.stderr); sys.exit(1)
" 2>/dev/null)
  [[ -z "$text" ]] && echo "ERROR: text extract failed" >&2 && return 1
  echo "$text"
}

call_gemini() {
  _call_gemini_with_prompt "$(printf '## 【最新の指示】\n%s' "$1")"
}

call_gemini_continuation() {
  _call_gemini_with_prompt "$(build_continuation_prompt "$1" "$2")"
}

# ============================================================
# 履歴管理
# ============================================================
save_history() {
  printf "[%s] %s: %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >> "$HISTORY_FILE"
  local lc; lc=$(wc -l < "$HISTORY_FILE" 2>/dev/null || echo 0)
  [[ "$lc" -gt 200 ]] && tail -100 "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" && mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
}

# ============================================================
# Web検索
# ============================================================
search_web() {
  local query="$1" results=""
  local enc; enc=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$query" 2>/dev/null)

  [[ -n "$BRAVE_API_KEY" ]] && results=$(curl -s \
    -H "X-Subscription-Token: ${BRAVE_API_KEY}" \
    "https://api.search.brave.com/res/v1/web/search?q=${enc}&count=3" 2>/dev/null | \
    python3 -c "
import json,sys
try:
  d=json.load(sys.stdin)
  for r in d.get('web',{}).get('results',[])[:3]:
    print(f'タイトル: {r.get(\"title\",\"\")}\nURL: {r.get(\"url\",\"\")}\n概要: {r.get(\"description\",\"\")}\n---')
except: pass
" 2>/dev/null)

  [[ -z "$results" ]] && results=$(curl -s "https://html.duckduckgo.com/html/?q=${enc}" 2>/dev/null | \
    grep -o '<a class="result__a"[^>]*>[^<]*</a>' | head -3 | sed 's/<[^>]*>//g')
  echo "${results:-検索結果なし}"
}

# ============================================================
# 画像解析
# ============================================================
handle_image_request() {
  local image_path="$1"
  [[ "$image_path" == "latest" || -z "$image_path" ]] && \
    image_path=$(ls -t "$SCREENSHOT_DIR"/*.png "$SCREENSHOT_DIR"/*.jpg 2>/dev/null | head -1)
  [[ -z "$image_path" || ! -f "$image_path" ]] && echo "画像ファイルが見つかりません" && return 1
  python3 "$IMAGE_SCRIPT" "$image_path" "この画像を詳しく説明してください" 2>/dev/null || echo "画像解析エラー"
}

# ============================================================
# コマンド実行
# ============================================================
execute_command() {
  local cmd="$1"
  local dangerous_patterns=("rm -rf /" "mkfs" "dd if=" ":(){ :|:& };" "> /dev/sda")
  for pattern in "${dangerous_patterns[@]}"; do
    echo "$cmd" | grep -qF "$pattern" && echo "⚠️ 危険なコマンドをブロック: $cmd" && return 1
  done
  if echo "$cmd" | grep -qE "^rm "; then
    echo -n "⚠️ 削除コマンドを実行しますか? ($cmd) [y/N]: "
    read -r confirm
    [[ "$confirm" != "y" && "$confirm" != "Y" ]] && echo "キャンセル" && return 1
  fi
  echo "🔧 実行: $cmd"
  local result exit_code
  result=$(eval "$cmd" 2>&1); exit_code=$?
  echo "$result"; echo "終了コード: $exit_code"
  printf "%s\n終了コード: %d" "$result" "$exit_code" > "$TASK_RESULT_FILE"
  return $exit_code
}

# ============================================================
# レスポンス解析・ディスパッチ
# ============================================================
dispatch_response() {
  local response="$1"
  local task_type task_content
  task_type=$(echo "$response" | grep "^TYPE:" | head -1 | sed 's/^TYPE: *//' | tr -d '\r')
  task_content=$(echo "$response" | grep -v "^TYPE:" | sed '/^[[:space:]]*$/d' | head -1)
  echo "📌 [DEBUG] task_type=[$task_type]"

  case "$task_type" in
    COMMAND)
      save_history "AI" "COMMAND実行: $task_content"
      execute_command "$task_content"
      save_history "SYSTEM" "実行結果: $(cat "$TASK_RESULT_FILE" 2>/dev/null | head -c 300)"
      return 3 ;;
    SEARCH)
      echo "🔍 検索中: $task_content"
      local results; results=$(search_web "$task_content")
      echo "$results"; printf "%s" "$results" > "$TASK_RESULT_FILE"
      save_history "SYSTEM" "検索結果: ${results:0:300}"
      return 3 ;;
    IMAGE)
      local img_result; img_result=$(handle_image_request "$task_content")
      echo "$img_result"; printf "%s" "$img_result" > "$TASK_RESULT_FILE"
      return 3 ;;
    NEED_INPUT)
      echo "❓ $task_content"; echo -n "回答: "; read -r user_answer
      save_history "USER" "$user_answer"
      local new_response; new_response=$(call_gemini "$user_answer")
      dispatch_response "$new_response"; return $? ;;
    DONE)
      echo "✅ $task_content"; save_history "AI" "完了: $task_content"; return 0 ;;
    NEXT)
      echo "⏭️ 次: $task_content"
      printf "%s" "$task_content" > "$TASK_RESULT_FILE"; return 3 ;;
    *)
      echo "💬 $response"; save_history "AI" "${response:0:200}"; return 0 ;;
  esac
}

# ============================================================
# メインタスクループ
# ============================================================
execute_task() {
  local user_input="$1" max_iterations=10 iteration=0
  local original_task="$user_input" current_input="$user_input"
  save_history "USER" "$user_input"
  detect_project_switch "$user_input" && echo "🔄 プロジェクト切り替え検知..." && load_config

  while [[ $iteration -lt $max_iterations ]]; do
    ((iteration++))
    echo ""; echo "═══ ステップ $iteration ═══"
    echo "🌐 ①Gemini分析中..."
    local response; response=$(call_gemini "$current_input")
    [[ -z "$response" ]] && echo "❌ ①Geminiエラー" && return 1

    dispatch_response "$response"
    local action_status=$?

    case $action_status in
      0) echo "✅ タスク完了"; return 0 ;;
      1) echo "❌ エラー"; return 1 ;;
      3)
        echo ""; echo "🌐 ②Gemini継続判断中..."
        local last_result; last_result=$(cat "$TASK_RESULT_FILE" 2>/dev/null || echo "結果なし")
        local cont_response; cont_response=$(call_gemini_continuation "$original_task" "$last_result")
        [[ -z "$cont_response" ]] && echo "❌ ②Geminiエラー" && return 1

        local cont_type cont_content
        cont_type=$(echo "$cont_response" | grep "^TYPE:" | head -1 | sed 's/^TYPE: *//' | tr -d '\r')
        cont_content=$(echo "$cont_response" | grep -v "^TYPE:" | sed '/^[[:space:]]*$/d' | head -1)
        echo ""; echo "── ②Gemini判断: [$cont_type] ──"; echo "$cont_content"; echo ""
        echo -n "続行しますか? [y=続行 / n=終了 / メッセージ=指示追加]: "; read -r user_choice

        case "$user_choice" in
          n|N) echo "中断しました"; return 0 ;;
          ""|y|Y)
            case "$cont_type" in
              DONE) echo "✅ 完了: $cont_content"; save_history "AI" "完了: $cont_content"; return 0 ;;
              NEXT|COMMAND|SEARCH|IMAGE) current_input="$cont_content" ;;
              NEED_INPUT)
                echo "❓ $cont_content"; echo -n "回答: "; read -r user_answer
                save_history "USER" "$user_answer"; current_input="$user_answer" ;;
              *) echo "💬 $cont_response"; return 0 ;;
            esac ;;
          *) save_history "USER" "$user_choice"; current_input="$user_choice" ;;
        esac ;;
    esac
  done
  echo "⚠️ 最大ステップ数($max_iterations)に達しました"; return 0
}

# ============================================================
# チャットモード
# ============================================================
chat_mode() {
  echo "🤖 AI Assistant v1.08 | プロジェクト: ${PROJECT_NAME_CURRENT:-未選択}"
  echo "exit=終了 / clear=履歴クリア / history=履歴表示 / project <dir>=プロジェクト切替"
  echo "════════════════════════════════════════"
  while true; do
    echo ""; echo -n "あなた: "; read -r user_input
    case "$user_input" in
      exit|quit) echo "終了します"; break ;;
      clear) > "$HISTORY_FILE"; echo "✅ 履歴クリア (${HISTORY_FILE})" ;;
      history) cat "$HISTORY_FILE" 2>/dev/null || echo "履歴なし" ;;
      project\ *)
        local np="${user_input#project }"
        if [[ -d "$np" ]]; then echo "$np" > "$CURRENT_PROJECT_FILE"; load_config
          echo "✅ プロジェクト切替: $PROJECT_NAME_CURRENT | 履歴: $HISTORY_FILE"
        else echo "❌ ディレクトリなし: $np"; fi ;;
      "") continue ;;
      *) execute_task "$user_input" ;;
    esac
  done
}

# ============================================================
# 画像スクリプト生成
# ============================================================
generate_image_script() {
  cat > "$IMAGE_SCRIPT" << 'IMGSCRIPT'
#!/usr/bin/env python3
import sys, base64, json, urllib.request, os
def analyze_image(path, inst="この画像を詳しく説明してください"):
    key = os.environ.get("GOOGLE_API_KEY","")
    if not key: print("GOOGLE_API_KEY not set"); return
    with open(path,"rb") as f: img=base64.b64encode(f.read()).decode()
    ext=path.rsplit(".",1)[-1].lower()
    mime={"jpg":"image/jpeg","jpeg":"image/jpeg","png":"image/png","gif":"image/gif","webp":"image/webp"}.get(ext,"image/jpeg")
    payload={"contents":[{"parts":[{"inline_data":{"mime_type":mime,"data":img}},{"text":inst}]}],"generationConfig":{"temperature":0.7,"maxOutputTokens":1024}}
    url=f"https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key={key}"
    req=urllib.request.Request(url,data=json.dumps(payload).encode(),headers={"Content-Type":"application/json"})
    try:
        with urllib.request.urlopen(req) as r: print(json.loads(r.read())["candidates"][0]["content"]["parts"][0]["text"])
    except Exception as e: print(f"エラー: {e}")
if __name__=="__main__":
    if len(sys.argv)<2: print("使用方法: ai_image.py <path> [instruction]"); sys.exit(1)
    analyze_image(sys.argv[1], sys.argv[2] if len(sys.argv)>2 else "この画像を詳しく説明してください")
IMGSCRIPT
  chmod +x "$IMAGE_SCRIPT"
}

# ============================================================
# メイン
# ============================================================
main() {
  [[ -z "$GOOGLE_API_KEY" ]] && echo "❌ GOOGLE_API_KEY未設定" && exit 1
  mkdir -p "$TMP_DIR"
  [[ ! -f "$IMAGE_SCRIPT" ]] && generate_image_script
  restore_last_project
  [[ $# -gt 0 ]] && execute_task "$*" || chat_mode
}

main "$@"

9. まとめ

スマホ完結の開発環境を求めて辿り着いたTermux × Gemini APIという組み合わせ。苦労した割に用途は限られますが、「スマホのターミナルからGitHubを自然言語で操作できる」という体験は、一度やると手放せなくなります。

この構成のポイントまとめ
  • bash + python3 + curl だけで動く最小構成のAIエージェント
  • Geminiを2回呼ぶ設計(①分析・②結果判断)で自律性を確保
  • プロジェクトごとに設定・履歴を分離(.ai_config / .ai_history)
  • Wranglerが使えなくてもGitHub Actionsで自動デプロイを実現

このスクリプトはTermux専用ではなく、SCREENSHOT_DIR のパスを変えるだけで通常のLinux/Macでも動きます。フレームワーク不要でGemini APIを手軽に試したい方にも参考になれば幸いです。

APIキーの管理には十分注意してください。スクリプト内にAPIキーをハードコードせず、必ず環境変数で管理しましょう。Gitへのコミット時にもAPIキーが含まれていないか確認する習慣を。

📚 関連記事

🔗 参考リンク

📅 2026/02/20 追記:ついに公式(Gemini CLI)が動いた!

自作スクリプトが完成し、喜び勇んでこの記事をデータベースに登録しようとしたのですが、ここで大きな壁にぶつかりました。HTMLファイルのサイズが大きすぎたのか、自作スクリプトでは処理しきれず、記事の登録ができませんでした……。やはり、急場しのぎの自作ツールには限界があったようです。

「やはりスマホでこれ以上は無理か」と悔しい思いをしましたが、執念でもう一度ジェンスパークに「TermuxでGemini CLIを動かす方法」を尋ねてみたところ、なんと驚きの解決策が提示されました!

解決策: インストール時に出る特定のエラーを回避するため、スクリプト実行をスキップするオプションを付ければよかったのです。
npm install -g @google/gemini-cli --ignore-scripts

この魔法のオプションで見事にインストール成功! 立ち上げて話してみると、最初は画面右下のモデルが「Gemini 2.5」になっており、少し期待外れな回答もありました。そこで「モデルを3に変えられるか」と聞いたところ、直接の指定コマンドはないものの、設定で general.previewFeaturestrue にすればいけるかもしれないとのこと。

指示通り設定を変えて再起動すると、無事に **Gemini 3 (Flash Preview)** が立ち上がりました! 自作スクリプトよりも遥かに賢く、大きなファイルも難なく扱えます。

現在の状況:
実は、今読んでいただいているこの記事のD1データベースへの登録作業は、この **Gemini CLI** を使ってスマホから行っています。無事にデータベース登録も完了し、ついに理想のスマホ開発環境が手に入りました!