Skip to content

Instantly share code, notes, and snippets.

@oikon48
Last active June 28, 2025 02:48
Show Gist options
  • Select an option

  • Save oikon48/4165b064290b33af70c9ae9d27a9ce4b to your computer and use it in GitHub Desktop.

Select an option

Save oikon48/4165b064290b33af70c9ae9d27a9ce4b to your computer and use it in GitHub Desktop.
# Gemini CLI 主要機能の詳細解説(学習用資料)

Gemini CLI 主要機能の詳細解説

重要な注意事項: このドキュメントはClaude Codeが初稿を作り、Gemini CLIとのやり取りでUpdateしたものです。この重要な注意事項以外は全てAIが作成しています。

このドキュメントは、Gemini CLIの主要な機能について、その内部実装を交えて詳しく解説する学習用資料です。

重要な注意事項: このドキュメントは、プロジェクトの基本的な設計と主要な概念を解説するものです。最新かつ正確な実装の詳細については、必ずソースコード自体を参照してください。本文書に含まれる疑似コードや簡略化された例は、あくまで理解を助けるためのものであり、実際の実装とは異なる場合があります。

目次

  1. アーキテクチャ概要
  2. ストリーミング機能の仕組み
  3. ツールシステムの実装
  4. Web検索機能(WebSearchツール)
  5. コマンド処理の仕組み
  6. ファイル編集と差分表示
  7. 会話コンテキスト管理
  8. シェルコマンド実行とサンドボックス
  9. 認証とセキュリティ
  10. MCP(Model Context Protocol)統合
  11. メモリ管理機能
  12. テレメトリとモニタリング
  13. 開発のベストプラクティス

1. アーキテクチャ概要

パッケージ構成

Gemini CLIは、npm workspacesを利用したモノレポ構造を採用しており、主に以下の2つのパッケージから構成されています。

gemini-cli/
├── packages/
│   ├── cli/                    # ユーザーインターフェース層
│   │   ├── src/
│   │   │   ├── gemini.tsx      # メインエントリーポイント
│   │   │   ├── ui/
│   │   │   │   ├── App.tsx     # ルートUIコンポーネント
│   │   │   │   ├── components/ # React/Inkコンポーネント
│   │   │   │   ├── hooks/      # カスタムReactフック
│   │   │   │   └── themes/     # テーマ定義
│   │   │   ├── config/         # 設定管理
│   │   │   └── utils/          # ユーティリティ関数
│   │   └── package.json
│   │
│   └── core/                   # ビジネスロジック層
│       ├── src/
│       │   ├── config/         # 設定管理
│       │   │   ├── config.ts   # 中央設定クラス
│       │   ├── core/           # コアクライアント実装
│       │   │   ├── client.ts   # GeminiClient
│       │   │   ├── geminiChat.ts
│       │   │   └── turn.ts     # 注: 実際のファイル名は小文字
│       │   ├── tools/          # ツール実装
│       │   │   ├── tools.ts    # 基底クラス
│       │   │   ├── edit.ts
│       │   │   ├── shell.ts
│       │   │   └── mcp-*.ts    # MCP統合
│       │   ├── services/       # 各種サービス
│       │   ├── telemetry/      # 監視機能
│       │   └── utils/          # 共通ユーティリティ
│       └── package.json

主要コンポーネント

CLI層のコンポーネント

  • gemini.tsx: CLIのエントリーポイント。認証、設定読み込み、UIの初期化を担当します。
  • App.tsx: React/Inkアプリケーションのルートコンポーネントです。
  • useGeminiStream.ts: ストリーミング応答を管理するカスタムReactフックです。
  • useReactToolScheduler.ts: ツール実行のUI側制御を担うカスタムフックです。

Core層のコンポーネント

  • Config: アプリケーション全体の設定を一元管理する中央クラスです (config/config.ts)。
  • GeminiClient: API通信とチャット管理の中核を担います (core/client.ts)。
  • GeminiChat: 会話履歴とトークン管理を担います (core/geminiChat.ts)。
  • Turn: 単一のやり取りを表現するクラスです (core/turn.ts)。
  • Tool Registry: ツールの登録と管理を行います (tools/tool-registry.ts)。
  • CoreToolScheduler: ツール実行のオーケストレーションを行います (core/coreToolScheduler.ts)。

アーキテクチャの特徴

  1. 階層化設計: UI層(CLI)とビジネスロジック層(Core)が明確に分離されています。
  2. 非同期中心: すべてのI/O操作でasync/awaitパターンが使用されています。
  3. ストリーミング対応: AsyncGeneratorベースでリアルタイム応答を実現しています。
  4. 拡張性: ツールシステムとMCPによるプラグイン的なアーキテクチャです。
  5. 型安全: TypeScriptによる厳密な型定義がされています。

主要な型定義

// MessageType: CLI内部のメッセージタイプ(packages/cli/src/ui/types.ts)
export enum MessageType {
  INFO = 'info',
  ERROR = 'error',
  USER = 'user',
  ABOUT = 'about',
  STATS = 'stats',
  QUIT = 'quit',
  GEMINI = 'gemini',
  COMPRESSION = 'compression',
}

// 注: 実際の実装では Item インターフェースは存在しません。
// 代わりに HistoryItem という discriminated union 型が使用されています。

// HistoryItem: 会話履歴の各要素(packages/cli/src/ui/types.ts)
export type HistoryItem = HistoryItemWithoutId & { id: number };

export type HistoryItemWithoutId =
  | HistoryItemUser
  | HistoryItemGemini
  | HistoryItemGeminiContent
  | HistoryItemInfo
  | HistoryItemError
  | HistoryItemToolGroup
  | HistoryItemCompression
  | HistoryItemShellCommand
  | HistoryItemAbout
  | HistoryItemStats
  | HistoryItemQuit;

// GeminiEventType: サーバー側のイベントタイプ(packages/core/src/core/turn.ts)
export enum GeminiEventType {
  Content = 'content',
  ToolCallRequest = 'tool_call_request',
  ToolCallResponse = 'tool_call_response',
  ToolCallConfirmation = 'tool_call_confirmation',
  UserCancelled = 'user_cancelled',
  Error = 'error',
  ChatCompressed = 'chat_compressed',
  UsageMetadata = 'usage_metadata',
  Thought = 'thought',
}

// CumulativeStats: セッション統計(SessionStatsではなく)
export interface CumulativeStats {
  tokensUsed: CumulativeTokenStats;
  apiCalls: number;
  turnsCount: number;
  model: string;
}

Config クラスの詳細

設定管理の中心的役割(packages/core/src/config/config.ts

Config クラスは、Gemini CLI の全体的な設定を管理する中央集権的なクラスです:

export class Config {
  // 主要なプロパティ
  private toolRegistry!: ToolRegistry;
  private geminiClient!: GeminiClient;
  private contentGeneratorConfig!: ContentGeneratorConfig;
  
  // 設定パラメータ
  private readonly sessionId: string;
  private readonly model: string;
  private readonly embeddingModel: string;
  private readonly sandbox: SandboxConfig | undefined;
  private readonly targetDir: string;
  private readonly debugMode: boolean;
  private readonly fullContext: boolean;
  private approvalMode: ApprovalMode;
  
  // サービス
  private fileDiscoveryService: FileDiscoveryService | null = null;
  private gitService: GitService | undefined = undefined;
  
  // 機能フラグ
  private readonly telemetrySettings: TelemetrySettings;
  private readonly usageStatisticsEnabled: boolean;
  private readonly checkpointing: boolean;
  
  constructor(params: ConfigParameters) {
    // 設定の初期化とデフォルト値の適用
    this.model = params.model;
    this.embeddingModel = params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
    this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
    // ...
  }
}

主要な責務

  1. 依存性の集約

    • ToolRegistry、GeminiClient、各種サービスのインスタンス管理
    • サービス間の依存関係の解決
  2. 設定値の一元管理

    • モデル設定(model、embeddingModel)
    • 実行環境設定(sandbox、targetDir、cwd)
    • 機能フラグ(telemetry、checkpointing、fullContext)
    • UI設定(accessibility、approvalMode)
  3. 動的な設定変更

    • setModel(): セッション中のモデル変更
    • refreshAuth(): 認証方法の切り替え
    • flashFallbackHandler: 429エラー時のフォールバック処理
  4. サービスの遅延初期化

    async getToolRegistry(): Promise<ToolRegistry> {
      if (!this.toolRegistry) {
        await this.initializeToolRegistry();
      }
      return this.toolRegistry;
    }

設定の階層構造

export interface ConfigParameters {
  // 必須パラメータ
  sessionId: string;
  targetDir: string;
  cwd: string;
  model: string;
  
  // オプショナルパラメータとデフォルト値
  embeddingModel?: string;              // DEFAULT_GEMINI_EMBEDDING_MODEL
  approvalMode?: ApprovalMode;          // ApprovalMode.DEFAULT
  telemetry?: TelemetrySettings;        // { enabled: false, ... }
  usageStatisticsEnabled?: boolean;     // true
  
  // 機能設定
  sandbox?: SandboxConfig;
  mcpServers?: Record<string, MCPServerConfig>;
  fileFiltering?: {
    respectGitIgnore?: boolean;        // true
    enableRecursiveFileSearch?: boolean; // true
  };
}

承認モードの管理

export enum ApprovalMode {
  DEFAULT = 'default',    // 通常の確認ダイアログ
  AUTO_EDIT = 'autoEdit', // 編集ツールのみ自動承認
  YOLO = 'yolo',          // すべて自動承認(危険)
}

2. ストリーミング機能の仕組み

サーバー側の実装

turn.tsの実装(packages/core/src/core/turn.ts

// Turn.run()メソッドの詳細実装
// 注: 以下は簡略化されたコード例です
// 実際の実装では、GeminiChatはコンストラクタで渡され、
// debugResponsesの保存、pendingToolCallsの管理なども行われています
async *run(
  req: PartListUnion,
  signal: AbortSignal,
): AsyncGenerator<ServerGeminiStreamEvent> {
  try {
    // GeminiChatクラスを使用してストリーミングリクエスト
    const responseStream = await this.chat.sendMessageStream({
      message: req,
      config: {
        abortSignal: signal,
      },
    });
    
    // デバッグ用に応答を保存(実際の実装)
    // this.debugResponses.push(resp);
    
    // ストリームからイベントを生成
    for await (const resp of responseStream) {
      if (signal?.aborted) {
        yield { type: GeminiEventType.UserCancelled };
        return;
      }
      
      // 思考イベント(Gemini 2.0+)の処理
      const thoughtPart = resp.candidates?.[0]?.content?.parts?.[0];
      if (thoughtPart?.thought) {
        // 思考内容を subject と description に分解
        const rawText = thoughtPart.text ?? '';
        const subjectStringMatches = rawText.match(/\*\*(.*?)\*\*/s);
        const subject = subjectStringMatches
          ? subjectStringMatches[1].trim()
          : '';
        const description = rawText.replace(/\*\*(.*?)\*\*/s, '').trim();
        
        yield {
          type: GeminiEventType.Thought,
          value: { subject, description }
        };
        continue;
      }
      
      // コンテンツイベント
      const text = getResponseText(resp);
      if (text) {
        yield { type: GeminiEventType.Content, value: text };
      }
      
      // ツール呼び出しイベント
      // 実際の実装では handlePendingFunctionCall() メソッドを使用
      const functionCalls = resp.functionCalls ?? [];
      for (const fnCall of functionCalls) {
        // pendingToolCalls配列で管理される
        const callId = fnCall.id ?? 
          `${fnCall.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
        
        yield { 
          type: GeminiEventType.ToolCallRequest,
          value: {
            callId,
            name: fnCall.name || 'undefined_tool_name',
            args: (fnCall.args || {}) as Record<string, unknown>,
            isClientInitiated: false
          }
        };
      }
      
      // 使用状況メタデータ
      if (resp.usageMetadata) {
        this.lastUsageMetadata = resp.usageMetadata;
      }
    }
    
    // APIタイムを含む使用統計を最後に送信
    if (this.lastUsageMetadata) {
      const durationMs = Date.now() - startTime;
      yield {
        type: GeminiEventType.UsageMetadata,
        value: { ...this.lastUsageMetadata, apiTimeMs: durationMs }
      };
    }
  } catch (error) {
    // UserCancelledイベントとエラーハンドリング
    if (signal.aborted) {
      yield { type: GeminiEventType.UserCancelled };
      return;
    }
    
    yield {
      type: GeminiEventType.Error,
      value: { error: structuredError }
    };
  }
}

クライアント側の処理

useGeminiStream.tsの実装(packages/cli/src/ui/hooks/useGeminiStream.ts

// ストリームイベントの処理フロー
// 注: 実装では GeminiEventType が ServerGeminiEventType としてエイリアスされている
// GeminiEvent は ServerGeminiStreamEvent のエイリアス
const processGeminiStreamEvents = useCallback(
  async (
    stream: AsyncIterable<GeminiEvent>,
    userMessageTimestamp: number,
    signal: AbortSignal,
  ): Promise<StreamProcessingStatus> => {
    let geminiMessageBuffer = '';
    const toolCallRequests: ToolCallRequestInfo[] = [];
    
    for await (const event of stream) {
      switch (event.type) {
        case ServerGeminiEventType.Content:
          // コンテンツのバッファリングと更新
          geminiMessageBuffer = handleContentEvent(
            event.value,
            geminiMessageBuffer,
            userMessageTimestamp,
          );
          break;
          
        case ServerGeminiEventType.ToolCallRequest:
          // ツール呼び出しの収集
          toolCallRequests.push(event.value);
          break;
          
        case ServerGeminiEventType.ToolCallConfirmation:
        case ServerGeminiEventType.ToolCallResponse:
          // 実装では何もしない(do nothing)
          break;
          
        case ServerGeminiEventType.Thought:
          // 思考内容の更新
          setThought(event.value);
          break;
          
        case ServerGeminiEventType.ChatCompressed:
          // 圧縮通知の処理
          handleChatCompressionEvent(event.value);
          break;
          
        case ServerGeminiEventType.UsageMetadata:
          // 使用統計の更新(sessionStatsに追加)
          addUsage(event.value);
          break;
          
        case ServerGeminiEventType.UserCancelled:
          // ユーザーによるキャンセル
          return StreamProcessingStatus.UserCancelled;
          
        case ServerGeminiEventType.Error:
          // エラー処理
          handleError(event.value.error);
          return StreamProcessingStatus.Error;
          
        default:
          // 型安全性のための exhaustive check
          const _exhaustiveCheck: never = event;
          break;
      }
    }
    
    // 収集したツール呼び出しをスケジュール
    if (toolCallRequests.length > 0) {
      scheduleToolCalls(toolCallRequests, signal);
    }
    
    return StreamProcessingStatus.Completed;
  },
  [handleContentEvent, scheduleToolCalls, handleChatCompressionEvent, addUsage],
);

リアルタイムUI更新の仕組み

パフォーマンス最適化戦略

  1. ペンディングアイテム管理:

    // 応答中のメッセージは pendingHistoryItem として管理
    const [pendingHistoryItemRef, setPendingHistoryItem] = 
      useStateAndRef<HistoryItemWithoutId | null>(null);
  2. 静的レンダリング最適化:

    // App.tsx内のStaticコンポーネント使用
    <Static items={history.slice(0, -1)}>
      {(item) => <HistoryItemComponent item={item} />}
    </Static>
    // 最後のアイテムのみ動的レンダリング
    {lastItem && <HistoryItemComponent item={lastItem} />}
  3. メッセージ分割アルゴリズム:

    // findLastSafeSplitPoint()でマークダウン境界を検出
    const splitPoint = findLastSafeSplitPoint(newGeminiMessageBuffer);
    if (splitPoint < newGeminiMessageBuffer.length) {
      // 安全な位置で分割して静的部分に移動
      const beforeText = newGeminiMessageBuffer.substring(0, splitPoint);
      const afterText = newGeminiMessageBuffer.substring(splitPoint);
      addItem({ type: 'gemini', text: beforeText }, timestamp);
      setPendingHistoryItem({ type: 'gemini_content', text: afterText });
    }

イベントタイプと状態管理

// ストリーミング状態の定義(packages/cli/src/ui/types.ts)
// 実際の実装では文字列値を持つ
export enum StreamingState {
  Idle = 'idle',
  Responding = 'responding',
  WaitingForConfirmation = 'waiting_for_confirmation',
}

// ツール呼び出し状態(packages/cli/src/ui/types.ts)
export enum ToolCallStatus {
  Pending = 'pending',
  Canceled = 'canceled',
  Confirming = 'confirming',
  Executing = 'executing',
  Success = 'success',
  Error = 'error',
}

// StreamProcessingStatus(packages/cli/src/ui/hooks/useGeminiStream.ts)
enum StreamProcessingStatus {
  Completed = 'completed',
  UserCancelled = 'user_cancelled',
  Error = 'error',
}

3. ツールシステムの実装

ツールの基本構造

Tool インターフェース定義(packages/core/src/tools/tools.ts

export interface Tool<TParams = unknown, TResult extends ToolResult = ToolResult> {
  // 内部API用の名前(64文字以内、英数字とアンダースコア)
  name: string;
  
  // ユーザー向け表示名
  displayName: string;
  
  // ツールの詳細説明
  description: string;
  
  // Google GenAI用のスキーマ定義
  schema: FunctionDeclaration;
  
  // 出力をMarkdownとしてレンダリングするか
  isOutputMarkdown: boolean;
  
  // ライブ出力更新をサポートするか
  canUpdateOutput: boolean;
  
  // パラメータ検証
  validateToolParams(params: TParams): string | null;
  
  // 実行前の説明文生成
  getDescription(params: TParams): string;
  
  // 確認が必要かの判定
  shouldConfirmExecute(
    params: TParams,
    abortSignal: AbortSignal,
  ): Promise<ToolCallConfirmationDetails | false>;
  
  // ツール実行
  execute(
    params: TParams,
    signal: AbortSignal,
    updateOutput?: (output: string) => void,
  ): Promise<TResult>;
}

BaseTool 基底クラス

export abstract class BaseTool<TParams = unknown, TResult extends ToolResult = ToolResult>
  implements Tool<TParams, TResult> {
  
  constructor(
    readonly name: string,
    readonly displayName: string,
    readonly description: string,
    readonly parameterSchema: Record<string, unknown>,
    readonly isOutputMarkdown: boolean = true,
    readonly canUpdateOutput: boolean = false,
  ) {}
  
  get schema(): FunctionDeclaration {
    return {
      name: this.name,
      description: this.description,
      parameters: this.parameterSchema as Schema,
    };
  }
}

組み込みツール一覧

ツールの命名規則

Gemini CLIのツールは3層の命名システムを持っています。

  1. クラス名: TypeScriptクラス名(例:GrepTool
  2. 内部API名(Static Name): ツール登録に使用される名前(例:search_file_content
  3. 表示名(Display Name): ユーザーに表示される名前(例:Grep

/toolsコマンドでは、説明付きの場合は「表示名 (内部API名)」形式で、説明なしの場合は表示名のみが表示されます。

ファイルシステムツール

クラス名 内部API名 表示名 主な機能 確認要否
ReadFileTool read_file ReadFile ファイル内容を行番号付きで読み取り
WriteFileTool write_file WriteFile ファイルへの書き込み(上書き保護付き)
EditTool replace Edit 文字列置換によるファイル編集
GlobTool glob FindFiles パターンマッチングによるファイル検索
GrepTool search_file_content SearchText 正規表現による内容検索
LSTool list_directory ReadFolder ディレクトリ一覧
ReadManyFilesTool read_many_files ReadManyFiles 複数ファイルの一括読み取り

システムツール

クラス名 内部API名 表示名 主な機能 確認要否
ShellTool run_shell_command Shell シェルコマンド実行
MemoryTool save_memory Memory 永続メモリへの保存
WebFetchTool web_fetch WebFetch Web コンテンツの取得
WebSearchTool google_web_search WebSearch Google 検索

ツール実行のステートマシン

stateDiagram-v2
    [*] --> validating: ツール呼び出し
    validating --> awaiting_approval: 確認が必要
    validating --> scheduled: 確認不要
    awaiting_approval --> scheduled: 承認
    awaiting_approval --> cancelled: 拒否
    scheduled --> executing: 実行開始
    executing --> success: 成功
    executing --> error: エラー
    executing --> cancelled: キャンセル
    success --> [*]
    error --> [*]
    cancelled --> [*]
Loading

ツール実装の詳細例

EditTool の実装(packages/core/src/tools/edit.ts

export class EditTool extends BaseTool<EditToolParams, ToolResult> 
  implements ModifiableTool<EditToolParams> {
  
  static readonly Name = 'replace';
  
  constructor(config: Config) {
    super(
      EditTool.Name,
      'Edit',
      `Replaces text within a file. By default, replaces a single occurrence...`,
      {
        properties: {
          file_path: {
            description: "The absolute path to the file to modify. Must start with '/'.",
            type: 'string',
          },
          old_string: {
            description: 'The exact literal text to replace, preferably unescaped...',
            type: 'string',
          },
          new_string: {
            description: 'The exact literal text to replace `old_string` with...',
            type: 'string',
          },
          expected_replacements: {
            type: 'number',
            description: 'Number of replacements expected. Defaults to 1 if not specified.',
            minimum: 1,
          },
        },
        required: ['file_path', 'old_string', 'new_string'],
        type: 'object',
      },
    );
  }
  
  // 最も重要な機能: AIによる編集内容の自動修正
  private async calculateEdit(
    params: EditToolParams,
    abortSignal: AbortSignal,
  ): Promise<CalculatedEdit> {
    // ファイルの現在の内容を読み取り
    const currentContent = fs.readFileSync(params.file_path, 'utf8')
      .replace(/\r\n/g, '\n'); // 改行コードの正規化
    
    // AIを使用して編集パラメータを修正(重要な機能!)
    // editCorrector.ts の ensureCorrectEdit が呼び出され、
    // old_string が正確に一致しない場合でも、LLMを使って
    // 意図した編集を実現できるよう自動修正を試みます
    const correctedEdit = await ensureCorrectEdit(
      currentContent,
      params,
      this.client,
      abortSignal,
    );
    
    // 修正されたパラメータを使用
    const finalOldString = correctedEdit.params.old_string;
    const finalNewString = correctedEdit.params.new_string;
    const occurrences = correctedEdit.occurrences;
    
    // エラーチェック
    if (occurrences === 0) {
      throw new Error('Failed to find the string to replace');
    }
    if (occurrences !== (params.expected_replacements ?? 1)) {
      throw new Error(`Expected ${params.expected_replacements} occurrences but found ${occurrences}`);
    }
    
    // 新しい内容を生成
    const newContent = currentContent.replaceAll(finalOldString, finalNewString);
    
    return {
      currentContent,
      newContent,
      occurrences,
      isNewFile: false
    };
  }
  
  async shouldConfirmExecute(
    params: EditToolParams,
    abortSignal: AbortSignal,
  ): Promise<ToolCallConfirmationDetails | false> {
    if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
      return false; // 自動承認モード
    }
    
    // 編集内容の計算(AIによる修正を含む)
    const editData = await this.calculateEdit(params, abortSignal);
    
    // 差分を生成
    const fileDiff = Diff.createPatch(
      path.basename(params.file_path),
      editData.currentContent ?? '',
      editData.newContent,
      'Current',
      'Proposed',
      DEFAULT_DIFF_OPTIONS,
    );
    
    return {
      type: 'edit',
      title: `Confirm Edit: ${shortenPath(makeRelative(params.file_path, this.rootDirectory))}`,
      fileName: path.basename(params.file_path),
      fileDiff,
      onConfirm: async (outcome) => {
        if (outcome === ToolConfirmationOutcome.ProceedAlways) {
          this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
        }
      },
    };
  }
  
  async execute(
    params: EditToolParams,
    signal: AbortSignal,
  ): Promise<ToolResult> {
    // 編集内容の計算(AIによる修正を含む)
    const editData = await this.calculateEdit(params, signal);
    
    // ファイルへの書き込み
    this.ensureParentDirectoriesExist(params.file_path);
    fs.writeFileSync(params.file_path, editData.newContent, 'utf8');
    
    return {
      llmContent: `Successfully modified file: ${params.file_path} (${editData.occurrences} replacements).`,
      returnDisplay: { 
        fileDiff: Diff.createPatch(...), 
        fileName: path.basename(params.file_path) 
      },
    };
  }
}

ツールの登録と動的発見

// packages/core/src/tools/tool-registry.ts
export class ToolRegistry {
  private tools: Map<string, Tool> = new Map();
  private discovery: Promise<void> | null = null;
  private config: Config;
  
  constructor(config: Config) {
    this.config = config;
  }
  
  registerTool(tool: Tool): void {
    if (this.tools.has(tool.name)) {
      // 警告を出力するが、上書きを許可
      console.warn(
        `Tool with name "${tool.name}" is already registered. Overwriting.`,
      );
    }
    this.tools.set(tool.name, tool);
  }
  
  // 注意: discoverBuiltInTools() メソッドは存在しない!
  // 代わりに discoverTools() メソッドが実装されている
  async discoverTools(): Promise<void> {
    // 以前に発見されたツールを削除
    for (const tool of this.tools.values()) {
      if (tool instanceof DiscoveredTool || tool instanceof DiscoveredMCPTool) {
        this.tools.delete(tool.name);
      }
    }
    
    // ツール発見コマンドを使用した動的発見(設定されている場合)
    const discoveryCmd = this.config.getToolDiscoveryCommand();
    if (discoveryCmd) {
      // 発見コマンドを実行して関数宣言を抽出
      const functions: FunctionDeclaration[] = [];
      for (const tool of JSON.parse(execSync(discoveryCmd).toString().trim())) {
        if (tool['function_declarations']) {
          functions.push(...tool['function_declarations']);
        } else if (tool['functionDeclarations']) {
          functions.push(...tool['functionDeclarations']);
        } else if (tool['name']) {
          functions.push(tool);
        }
      }
      
      // 各関数をツールとして登録
      for (const func of functions) {
        this.registerTool(
          new DiscoveredTool(
            this.config,
            func.name!,
            func.description!,
            func.parameters! as Record<string, unknown>,
          ),
        );
      }
    }
    
    // MCPサーバーを使用したツールの発見(設定されている場合)
    await discoverMcpTools(
      this.config.getMcpServers() ?? {},
      this.config.getMcpServerCommand(),
      this,
    );
  }
}

// 注: 以前のバージョンにあった discoverBuiltInTools メソッドは削除されました。
// 組み込みツールの登録は、packages/core/src/config/config.ts の
// createToolRegistry() 関数内で行われます。

// 実際の初期化フロー(packages/core/src/config/config.ts より)
export async function createToolRegistry(
  config: Config,
  geminiClient: GeminiClient,
): Promise<ToolRegistry> {
  const toolRegistry = new ToolRegistry();
  
  // registerCoreTool ヘルパー関数で組み込みツールを登録
  const registerCoreTool = (tool: Tool) => {
    tool.assignClient(geminiClient);
    toolRegistry.registerTool(tool);
  };
  
  // 組み込みツールの個別登録
  registerCoreTool(new ReadFileTool());
  registerCoreTool(new WriteFileTool(config));
  registerCoreTool(new EditTool(config));
  registerCoreTool(new ShellTool(config));
  registerCoreTool(new ListFilesTool());
  registerCoreTool(new MemoryTool(config));
  registerCoreTool(new SearchFilesTool());
  // ... その他のツール
  
  // 動的ツール(カスタムツール、MCPツール)の発見
  await toolRegistry.discoverTools(config, geminiClient);
  
  return toolRegistry;
}

4. Web検索機能(WebSearchツール)

概要

WebSearchツールは、Gemini APIのグラウンディング機能googleSearch)を活用して、ウェブ検索結果に基づいた信頼性の高い応答を生成するツールです。このツールは、Google検索の生の結果を直接処理するのではなく、Gemini APIの内部機能を通じて検索・要約・引用付けを行います。

アーキテクチャと実装

ツールの基本情報

  • クラス名: WebSearchToolpackages/core/src/tools/web-search.ts
  • 静的名(内部API名): google_web_search
  • 表示名(ユーザー向け): GoogleSearch
  • 継承: BaseToolクラスを拡張

処理フロー

// 簡略化された処理フロー
1. ユーザークエリ受信
   
2. Gemini APIにgoogleSearchツールを指定してリクエスト
   const response = await geminiClient.generateContent(
     [{ role: 'user', parts: [{ text: params.query }] }],
     { tools: [{ googleSearch: {} }] },  // グラウンディング機能の指定
     signal,
   );
   
3. Gemini APIが内部で:
   - Google検索を実行
   - 検索結果を分析・要約
   - グラウンディング情報(引用元)を生成
   
4. WebSearchツールがレスポンスを処理:
   - groundingSupportsから引用位置情報を抽出
   - テキスト内の正確な位置に引用番号[1], [2]を挿入
   - groundingChunksからソースリストを生成
   
5. フォーマットされた結果をユーザーに返却

主要な実装詳細

1. Gemini APIリクエスト

// packages/core/src/tools/web-search.ts より
async execute(params: { query: string }, signal?: AbortSignal): Promise<WebSearchToolResult> {
  // Gemini APIにgoogleSearchツールを使用してリクエスト
  const response = await this.geminiClient.generateContent(
    [{ role: 'user', parts: [{ text: params.query }] }],
    { tools: [{ googleSearch: {} }] },  // グラウンディング機能を有効化
    signal,
  );
  
  // レスポンスからテキストとメタデータを抽出
  const text = response.text();
  const groundingMetadata = response.groundingMetadata();
  
  // 引用の挿入とソースリストの生成
  return this.formatResponse(text, groundingMetadata);
}

2. 引用の挿入処理

引用の挿入は、groundingSupportsに含まれる文字位置情報を使用して、正確な位置に引用番号を配置します:

// 疑似コード:実際の実装とは異なります
function insertCitations(text: string, groundingSupports: GroundingSupport[]): string {
  // groundingSupportsを終了位置の降順でソート(後ろから処理)
  const sortedSupports = [...groundingSupports].sort((a, b) => 
    b.segment.endIndex - a.segment.endIndex
  );
  
  let result = text;
  for (const support of sortedSupports) {
    const citationNumbers = support.groundingChunkIndices
      .map(idx => `[${idx + 1}]`)
      .join('');
    
    // 指定された位置に引用を挿入
    result = 
      result.slice(0, support.segment.endIndex) + 
      citationNumbers + 
      result.slice(support.segment.endIndex);
  }
  
  return result;
}

3. ソースリストの生成

// 疑似コード
function formatSources(groundingChunks: GroundingChunk[]): string {
  return '\n\nSources:\n' + groundingChunks
    .map((chunk, idx) => `[${idx + 1}] ${chunk.web?.title || 'Untitled'} (${chunk.web?.uri})`) 
    .join('\n');
}

グラウンディング機能の特徴

利点

  1. 信頼性: Geminiが提供する情報源に基づいた回答
  2. 透明性: 各主張に対する明確な引用元の提示
  3. 最新性: リアルタイムのWeb検索結果を反映
  4. 簡潔性: 検索結果の要約と統合

制限事項

  1. カスタマイゼーション不可: 検索パラメータの細かい制御は不可能
  2. 単一クエリ: 複数段階の検索や深い調査には不向き
  3. Gemini依存: 検索ロジックは完全にGemini APIに委譲

Deep Researchとの違い

Gemini CLIには「Deep Research」機能は実装されていません。WebSearchツールは以下の特徴を持つ軽量検索ツールとして設計されています:

特徴 WebSearchツール Deep Research(一般的な概念)
検索の深さ 単一クエリ 多段階・反復的検索
処理時間 高速(数秒) 時間をかけた調査
結果の包括性 要約された回答 詳細な調査レポート
カスタマイズ 不可 検索戦略の制御可能
用途 即座の情報取得 深い調査・研究

実装上の注意点

  1. エラーハンドリング: グラウンディング情報が存在しない場合の処理
if (!groundingMetadata?.groundingChunks || groundingChunks.length === 0) {
  // 引用なしでテキストのみを返す
  return { llmContent: text, returnDisplay: 'Search completed' };
}
  1. 型安全性: TypeScriptによる厳密な型定義
interface GroundingSupport {
  segment: {
    startIndex: number;
    endIndex: number;
    text: string;
  };
  groundingChunkIndices: number[];
  confidenceScores: number[];
}
  1. 非同期処理: AbortSignalによるキャンセル対応

まとめ

WebSearchツールは、Gemini APIのグラウンディング機能を薄くラップしたツールであり、検索の実行から結果の要約まですべてをGemini APIに委譲しています。ツール自体の役割は、返されたグラウンディング情報を使って引用番号とソースリストを適切にフォーマットすることに限定されており、これにより高速で信頼性の高い検索機能を提供しています。


5. コマンド処理メカニズム

スラッシュコマンド(/)

実装詳細(packages/cli/src/ui/hooks/slashCommandProcessor.ts

export type SlashCommandActionReturn = {
  shouldScheduleTool: true;
  toolName: string;
  toolArgs: Record<string, unknown>;
} | {
  shouldScheduleTool: false;
};

const commandHandlers: Record<string, CommandHandler> = {
  '/help': async (context) => {
    context.setShowHelp(true);
    return true; // コマンドを処理済み
  },
  
  '/clear': async (context) => {
    context.clearHistory();
    return true;
  },
  
  '/theme': async (context) => {
    context.setShowThemeDialog(true);
    return true;
  },
  
  '/auth': async (context) => {
    context.setShowAuthDialog(true);
    return true;
  },
  
  '/stats': async (context) => {
    context.setShowStats(true);
    return true;
  },
  
  '/memory': async (context, args) => {
    const subcommand = args[0];
    if (subcommand === 'add') {
      // ツールをスケジュールする例
      return {
        shouldScheduleTool: true,
        toolName: 'save_memory',
        toolArgs: { memory: args.slice(1).join(' ') }
      };
    }
    // その他のサブコマンド処理
  },
  
  '/tools': async (context) => {
    const tools = context.getAvailableTools();
    context.addItem({ 
      type: MessageType.INFO, 
      text: formatToolList(tools) 
    });
    return true;
  },
  
  '/mcp': async (context) => {
    const servers = context.getMcpServers();
    context.addItem({
      type: MessageType.INFO,
      text: formatMcpServerList(servers)
    });
    return true;
  }
};

実装されているスラッシュコマンド一覧(完全版)

コマンド 説明 ツール呼び出し
/help または /? ヘルプを表示
/docs 完全なGemini CLIドキュメントをブラウザで開く
/clear 会話履歴をクリア(Ctrl+Lでも可)
/theme テーマ選択ダイアログ
/auth 認証方法の変更
/stats 使用統計を表示
/memory メモリ管理(add/show/refresh) ✅(add時)
/tools 利用可能なツール一覧
/mcp MCPサーバー一覧と状態
/compress 会話を手動圧縮
/bug <description> バグレポートの提出
/about バージョン情報とシステム詳細を表示
/chat 会話履歴管理(list/save/load/resume/delete)
/restore [tool_call_id] チェックポイントから復元(要--checkpointing)
/quit Gemini CLIを終了(exitやCtrl+Dでも可)
/editor エディタ設定ダイアログを開く
/corgi イースターエッグ(🐕)

アットコマンド(@)

実装詳細(packages/cli/src/ui/hooks/atCommandProcessor.ts

export async function handleAtCommand({
  query,
  config,
  addItem,
  onDebugMessage,
  messageId,
  signal,
}: AtCommandParams): Promise<AtCommandResult> {
  const parts = query.split(' ');
  const atCommand = parts[0];
  const atTarget = atCommand.substring(1); // '@'を除去
  
  if (!atTarget) {
    return { shouldProceed: false };
  }
  
  // パス解決とファイル探索
  const resolvedPath = path.resolve(config.getTargetDir(), atTarget);
  
  // ディレクトリの場合
  if (await isDirectory(resolvedPath)) {
    const files = await discoverFiles(resolvedPath, {
      maxFiles: 50,
      ignorePatterns: config.getIgnorePatterns()
    });
    
    // ファイル内容を読み込んでコンテキストに追加
    const contents = await readMultipleFiles(files);
    const contextParts = formatFileContents(contents);
    
    // ユーザーメッセージとして追加
    addItem({
      type: MessageType.USER,
      text: `${query}`,
      parts: contextParts
    }, messageId);
    
    return {
      shouldProceed: true,
      processedQuery: parts.slice(1).join(' ') || 'Please analyze these files'
    };
  }
  
  // Globパターンの場合
  if (containsGlobPattern(atTarget)) {
    const matches = await glob(atTarget, {
      cwd: config.getTargetDir(),
      ignore: config.getIgnorePatterns()
    });
    // ... 同様の処理
  }
  
  // 単一ファイルの場合
  const content = await readFile(resolvedPath);
  // ... ファイル内容の処理
}

@コマンドの使用例

# 単一ファイル
@package.json explain this file

# Globパターン
@*.ts list all TypeScript files

# ディレクトリ
@src/ analyze this directory structure

# 複数ファイル
@src/*.test.ts review these test files

@コマンドの特徴と高度な機能

  1. ファイル内容の自動読み込み: 指定されたファイルやディレクトリの内容を自動的にプロンプトに含める
  2. Git認識フィルタリング: .gitignoreパターンを尊重し、不要なファイルを除外
    • config.getFileFilteringRespectGitIgnore() で有効/無効を切り替え可能
    • .geminiignore パターンも同時にサポート
  3. 柔軟なパス指定: 相対パス、絶対パス、Globパターンをサポート
  4. 複数ファイルの同時指定: 一つのクエリ内で複数の@コマンドを使用可能
    @src/index.ts @src/types.ts analyze these files
    # または
    Check @file1.txt and compare with @file2.md please
  5. 自動Glob変換とフォールバック検索:
    • ディレクトリパスは自動的に dir/** に変換
    • ファイルが見つからない場合、**/*{filename}* パターンで再帰的検索を試行
  6. ディレクトリの再帰的読み込み: ディレクトリ指定時は設定可能な上限まで自動的に読み込み
  7. エスケープされたスペースのサポート: バックスラッシュでスペースをエスケープ可能
    @My\ Documents/file.txt  # スペースを含むパスに対応
  8. スマートなエラーハンドリング: パスが見つからない場合でも処理を継続し、有効なファイルのみを使用
  9. 画像/PDFサポート: 明示的に要求された場合、非テキストファイルも処理可能

シェルモード

実装詳細(packages/cli/src/ui/hooks/shellCommandProcessor.ts

export const useShellCommandProcessor = (
  addItem: (item: HistoryItemWithoutId, timestamp: number) => void,
  setPendingHistoryItem: (item: HistoryItemWithoutId | null) => void,
  onExec: (done: Promise<void>) => Promise<void>,
  onDebugMessage: (message: string) => void,
  config: Config,
  geminiClient: GeminiClient,
) => {
  const handleShellCommand = useCallback(
    (command: string, signal: AbortSignal): boolean => {
      if (!command.trim()) return false;
      
      // シェルコマンドとして処理
      const toolCall: ToolCallRequestInfo = {
        callId: `shell-${Date.now()}`,
        name: 'run_shell_command',
        args: { command },
        isClientInitiated: true
      };
      
      // ツールスケジューラーに委譲
      const scheduler = new CoreToolScheduler(
        config.getToolRegistry(),
        config,
        geminiClient
      );
      
      onExec(scheduler.executeTool(toolCall, signal));
      return true;
    },
    [config, geminiClient, onExec]
  );
  
  return { handleShellCommand };
};

シェルモードの特徴

  1. 直接実行: !<command>でシェルコマンドを直接実行
  2. Geminiバイパス: AIを介さずに直接シェルツールを呼び出し、高速実行
  3. サンドボックス対応: 設定に応じて隔離環境で実行
  4. 出力ストリーミング: リアルタイムで結果を表示
  5. 履歴への記録: 実行結果は会話履歴に自動的に追加
  6. プロセス管理: 適切なシグナルハンドリングとクリーンアップ

注: 現在の実装では、単独の!によるシェルモードのトグル機能は実装されていません。各コマンドは!プレフィックスで個別に実行する必要があります。

シェルコマンドの使用例

# 直接実行
!ls -la
!git status
!npm install

6. ファイル編集と差分表示

編集の実装詳細

EditTool.calculateEdit()の処理フロー(packages/core/src/tools/edit.ts

private async calculateEdit(
  params: EditToolParams,
  abortSignal: AbortSignal,
): Promise<CalculatedEdit> {
  try {
    // 1. ファイルの存在確認(実際は同期的に処理)
    const exists = fs.existsSync(params.file_path);
    
    // 2. 既存内容の読み取り(存在する場合)
    const originalContent = exists 
      ? fs.readFileSync(params.file_path, 'utf8').replace(/\r\n/g, '\n')
      : '';
    
    // 3. 編集の適用(正確な文字列マッチング)
    let newContent = originalContent;
    let occurrences = 0;
    
    // 全ての出現箇所を検索
    const indices: number[] = [];
    let index = originalContent.indexOf(params.old_string);
    while (index !== -1) {
      indices.push(index);
      index = originalContent.indexOf(params.old_string, index + 1);
    }
    occurrences = indices.length;
    
    // 期待される置換数の検証
    const expectedReplacements = params.expected_replacements ?? 1;
    if (occurrences !== expectedReplacements) {
      throw new Error(
        `Expected ${expectedReplacements} replacements, but found ${occurrences}`
      );
    }
    
    // 置換の実行
    if (occurrences > 0) {
      newContent = originalContent.split(params.old_string)
        .join(params.new_string);
    }
    
    // 4. 差分の生成
    const diff = Diff.createPatch(
      params.file_path,
      originalContent,
      newContent,
      'Original',
      'Modified',
      { context: 3 } // 前後3行のコンテキストを含む
    );
    
    return {
      currentContent: originalContent,
      newContent,
      occurrences,
      isNewFile: !exists
    };
  } catch (error) {
    return {
      currentContent: null,
      newContent: '',
      occurrences: 0,
      error: {
        display: getErrorMessage(error),
        raw: String(error)
      },
      isNewFile: false
    };
  }
}

差分表示の仕組み

確認ダイアログの実装(packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx

// 注: 実際の実装では、EditConfirmationは独立コンポーネントではなく、
// ToolConfirmationMessageコンポーネント内で統合実装されている
// また、4つ目の選択肢として外部エディタでの編集オプションも提供される

const ToolConfirmationMessage: React.FC<Props> = ({ item, onConfirm, onReject }) => {
  const theme = useTheme();
  
  // edit タイプの場合の実装
  if (item.toolConfirmation.type === 'edit') {
    return (
      <Box flexDirection="column" gap={1}>
        <Text color={theme.colors.warning}>
          {item.toolConfirmation.title}
        </Text>
        
        <Box flexDirection="column">
          <Text color={theme.colors.muted}>
            File: {item.toolConfirmation.fileName}
          </Text>
          
          {/* 差分の表示 */}
          <Box marginTop={1}>
            <DiffDisplay fileDiff={item.toolConfirmation.fileDiff} />
          </Box>
        </Box>
        
        {/* 確認オプション(実際は4つ) */}
        <Box gap={1}>
          <Text color={theme.colors.info}>
            [Y] Yes  [N] No  [A] Always (this session)  [E] Edit in external editor
          </Text>
        </Box>
      </Box>
    );
  }
  
  // exec, info, mcp タイプの処理も含まれる
};

DiffDisplay コンポーネント(packages/cli/src/ui/components/messages/DiffDisplay.tsx

// 注: 実際の実装では DiffDisplay として実装され、より高度な機能を持つ
// 新規ファイルの場合はシンタックスハイライト表示に切り替わる
const DiffDisplay: React.FC<{ fileDiff: string }> = ({ fileDiff }) => {
  const theme = useTheme();
  
  // ファイルが新規作成の場合の特別処理
  const isNewFile = isNewFileDiff(fileDiff);
  if (isNewFile) {
    // シンタックスハイライト付きで表示
    const content = extractNewFileContent(fileDiff);
    return <CodeView content={content} fileName={fileName} />;
  }
  
  // 通常の差分表示(最適化された実装)
  const lines = fileDiff.split('\n');
  
  return (
    <Box flexDirection="column">
      {lines.map((line, index) => {
        // タブを空白に変換、最小インデントを削除
        const processedLine = processLine(line);
        let color = theme.colors.text;
        
        if (line.startsWith('+') && !line.startsWith('+++')) {
          color = theme.colors.success; // 追加行
        } else if (line.startsWith('-') && !line.startsWith('---')) {
          color = theme.colors.error; // 削除行
        } else if (line.startsWith('@@')) {
          color = theme.colors.info; // ハンク情報
        } else if (line.startsWith('+++') || line.startsWith('---')) {
          color = theme.colors.muted; // ファイル名
        }
        
        // 大きなギャップには区切り線を表示
        if (isLargeGap(line, prevLine)) {
          return (
            <>
              <Text color={theme.colors.muted}>{'─'.repeat(80)}</Text>
              <Text key={index} color={color}>{processedLine}</Text>
            </>
          );
        }
        
        return (
          <Text key={index} color={color}>
            {processedLine}
          </Text>
        );
      })}
    </Box>
  );
};

高度な編集機能

エディター統合(packages/core/src/utils/editor.ts

// 注: 実際の実装では openDiff メソッドとして実装されている
export async function openDiff({
  oldContent,
  newContent,
  fileName,
  editor,
  originalFilePath,
}: OpenDiffOptions): Promise<ExternalEditorResult> {
  const editorType = editor || (await detectDefaultEditor());
  
  // サンドボックス環境では外部エディタは使用不可
  if (config.getSandboxConfig()) {
    throw new Error('External editor not available in sandbox');
  }
  
  // 実際にサポートされているエディタ
  switch (editorType) {
    case EditorType.VSCode:
      return openVSCodeDiff({ oldContent, newContent, fileName, originalFilePath });
    case EditorType.Cursor:
      return openCursorDiff({ oldContent, newContent, fileName, originalFilePath });
    case EditorType.Vim:
      // Vimには詳細な差分表示設定が実装されている
      return openVimDiff({ 
        oldContent, 
        newContent, 
        fileName,
        vimCommand: 'vim -d', // vimdiff モード
        diffOptions: '+set diffopt=filler,context:3'
      });
    case EditorType.Windsurf:
      return openWindsurfDiff({ oldContent, newContent, fileName, originalFilePath });
    case EditorType.Zed:
      return openZedDiff({ oldContent, newContent, fileName, originalFilePath });
    // 注: Emacs と Nano は実装されていない
    default:
      throw new Error(`Unsupported editor: ${editorType}`);
  }
}

編集の自動修正(AIによる補正機能)

// packages/core/src/utils/editCorrector.ts
// 注: 実際の実装には以下の高度な機能が含まれています:
// - LRUキャッシュによる最適化(最大50エントリ)
// - 複数段階の修正戦略(アンエスケープ → LLM修正 → トリミング)
// - Gemini特有のエスケープ問題への対処

export async function ensureCorrectEdit(
  currentContent: string,
  originalParams: EditToolParams,
  client: GeminiClient,
  abortSignal: AbortSignal,
): Promise<CorrectedEditResult> {
  // キャッシュのチェック
  const cacheKey = getCacheKey(currentContent, originalParams);
  const cached = correctionCache.get(cacheKey);
  if (cached) return cached;
  
  // まず、正確なマッチングを試行
  let params = originalParams;
  let occurrences = countOccurrences(currentContent, params.old_string);
  
  if (occurrences === (params.expected_replacements ?? 1)) {
    // 正確にマッチする場合は、そのまま返す
    return cacheAndReturn(cacheKey, { params, occurrences });
  }
  
  // ステップ1: Geminiのエスケープバグへの対処
  // unescapeStringForGeminiBug() で一般的なエスケープ問題を修正
  const unescapedParams = {
    ...params,
    old_string: unescapeStringForGeminiBug(params.old_string),
    new_string: unescapeStringForGeminiBug(params.new_string),
  };
  occurrences = countOccurrences(currentContent, unescapedParams.old_string);
  
  if (occurrences === (params.expected_replacements ?? 1)) {
    return cacheAndReturn(cacheKey, { params: unescapedParams, occurrences });
  }
  
  // ステップ2: LLMを使用した修正(実際は3種類の異なるプロンプトを使用)
  // DEFAULT_GEMINI_FLASH_MODEL を使用して高速に修正
  const correctedParams = await attemptLLMCorrection(
    currentContent,
    params,
    client,
    abortSignal
  );
  
  if (correctedParams) {
    occurrences = countOccurrences(currentContent, correctedParams.old_string);
    if (occurrences === (params.expected_replacements ?? 1)) {
      return cacheAndReturn(cacheKey, { params: correctedParams, occurrences });
    }
  }
  
  // ステップ3: トリミングによる修正
  // 前後の空白を削除して再試行
  const trimmedParams = {
    ...params,
    old_string: params.old_string.trim(),
    new_string: params.new_string.trim(),
  };
  occurrences = countOccurrences(currentContent, trimmedParams.old_string);
  
  // 最終的な結果を返す(修正できなくても元のパラメータを返す)
  return cacheAndReturn(cacheKey, { 
    params: occurrences > 0 ? trimmedParams : params, 
    occurrences 
  });

function countOccurrences(content: string, searchString: string): number {
  if (searchString === '') return 0;
  let count = 0;
  let index = content.indexOf(searchString);
  while (index !== -1) {
    count++;
    index = content.indexOf(searchString, index + 1);
  }
  return count;
}

7. 会話コンテキスト管理

トークン管理の実装

トークン制限の定義(packages/core/src/core/tokenLimits.ts

// 注: 実際の実装では TOKEN_LIMITS 定数は存在しません
// tokenLimit 関数内で switch 文を使用してモデルごとの制限を定義しています

const DEFAULT_TOKEN_LIMIT = 1_048_576; // 1M tokens

export function tokenLimit(model: string): number {
  // Remove "models/" prefix if present
  const modelName = model.replace(/^models\//, '');
  
  switch (modelName) {
    // Gemini 1.5 Pro variants - 2M tokens
    case 'gemini-1.5-pro':
    case 'gemini-1.5-pro-latest':
    case 'gemini-1.5-pro-001':
    case 'gemini-1.5-pro-002':
      return 2_097_152;
      
    // Gemini 1.5 Flash variants - 1M tokens
    case 'gemini-1.5-flash':
    case 'gemini-1.5-flash-latest':
    case 'gemini-1.5-flash-001':
    case 'gemini-1.5-flash-002':
    case 'gemini-1.5-flash-8b':
    case 'gemini-1.5-flash-8b-latest':
    case 'gemini-1.5-flash-8b-001':
      return 1_048_576;
      
    // Gemini 2.0 Flash variants
    case 'gemini-2.0-flash':
    case 'gemini-2.0-flash-latest':
      return 1_048_576;
      
    // Thinking models - 32K tokens
    case 'gemini-2.0-flash-thinking-exp':
    case 'gemini-2.0-flash-thinking-exp-01-21':
    case 'gemini-2.0-flash-thinking-exp-1219':
      return 32_767;
      
    // Gemini 2.5 Pro - 2M tokens
    case 'gemini-2.5-pro':
    case 'gemini-2.5-pro-latest':
    case 'gemini-2.5-pro-002':
      return 2_097_152;
      
    // Gemini 2.5 Flash - 1M tokens
    case 'gemini-2.5-flash':
    case 'gemini-2.5-flash-latest':
    case 'gemini-2.5-flash-002':
      return 1_048_576;
      
    // LearnLM models - 128K tokens
    case 'learnlm-1.5-pro-experimental':
      return 131_072;
      
    default:
      return DEFAULT_TOKEN_LIMIT;
  }
}

トークンカウントの実装

トークンカウントは GeminiChat クラスに直接的な getTokenCount() メソッドは存在せず、実際には GeminiClient クラスが ContentGenerator を通じて処理します。

// packages/core/src/core/client.ts 内の実装
export class GeminiClient {
  async tryCompressChat(): Promise<boolean> {
    // トークンカウントは ContentGenerator を通じて取得
    const countResult = await this.getContentGenerator().countTokens({
      generateContentRequest: {
        contents: this.geminiChat.getHistory(),
        systemInstruction: this.systemInstruction,
        model: this.model,
      },
    });
    
    const currentTokens = countResult.totalTokens;
    const limit = tokenLimit(this.model);
    
    if (currentTokens <= limit * 0.95) {
      return false; // 圧縮不要
    }
    
    // 圧縮処理を続行...
  }
}

圧縮メカニズム

圧縮プロセス(packages/core/src/core/client.ts

// 注: 実際の実装では ChatCompressed イベントは存在します(ChatCompressing ではない)
// buildCompressionPrompt メソッドは存在せず、プロンプトは直接記述されています

async tryCompressChat(): Promise<ChatCompressionInfo | null> {
  try {
    // 1. 現在のトークン数を確認
    const countResult = await this.getContentGenerator().countTokens({
      generateContentRequest: {
        contents: this.geminiChat.getHistory(),
        systemInstruction: this.systemInstruction,
        model: this.model,
      },
    });
    const originalTokenCount = countResult.totalTokens;
    const limit = tokenLimit(this.model);
    
    if (originalTokenCount <= limit * 0.95) {
      return null; // 圧縮不要
    }
    
    // 2. 圧縮リクエストを送信(プロンプトはインラインで定義)
    const compressionPrompt = `Please provide a comprehensive summary of our conversation so far...`;
    const compressedHistory = await this.requestCompression(compressionPrompt);
    
    // 3. 履歴を置き換え
    this.geminiChat.replaceHistory(compressedHistory);
    
    // 4. 新しいトークン数を確認
    const newCountResult = await this.getContentGenerator().countTokens({
      generateContentRequest: {
        contents: this.geminiChat.getHistory(),
        systemInstruction: this.systemInstruction,
        model: this.model,
      },
    });
    const newTokenCount = newCountResult.totalTokens;
    
    // 5. ChatCompressionInfo を返す(yield ではなく return)
    return {
      originalTokenCount,
      newTokenCount,
    };
  } catch (error) {
    this.handleCompressionError(error);
    return null;
  }
}

// 注: ChatCompressed イベントは sendMessageStream メソッド内で発行される
// 圧縮後の情報はジェネレータで yield される

履歴管理とメッセージタイプ

HistoryItem型定義(packages/cli/src/ui/types.ts

// 注: 実際の実装では、MessageType と HistoryItem の構造は
// セクション1で示したものとは異なり、discriminated union として実装されています

export type HistoryItem = HistoryItemWithoutId & { id: number };

export type HistoryItemWithoutId =
  | HistoryItemUser
  | HistoryItemGemini
  | HistoryItemGeminiContent
  | HistoryItemInfo
  | HistoryItemError
  | HistoryItemToolGroup
  | HistoryItemCompression
  | HistoryItemShellCommand
  | HistoryItemAbout
  | HistoryItemStats
  | HistoryItemQuit;

// 各タイプの定義例
export interface HistoryItemUser {
  type: 'user';
  text: string;
  atFiles?: AtFile[];
}

export interface HistoryItemGemini {
  type: 'gemini';
  text: string;
}

// 注: id は number 型で、timestamp、parts、metadata フィールドは存在しません
// 各履歴アイテムタイプは固有の構造を持ちます

コンテキスト最適化戦略

  1. スマート分割: 大きなメッセージを安全な境界で分割
  2. 遅延読み込み: ツール結果の詳細は必要時のみ展開
  3. プリエンプティブ圧縮: 90%時点で圧縮を提案
  4. 選択的保持: 重要なコンテキストを優先的に保持
// packages/cli/src/ui/utils/markdownUtilities.ts
// 注: 実際の実装では MAX_CHUNK_SIZE 定数は存在せず、
// より高度なコードブロック整合性チェックが実装されています

export function findLastSafeSplitPoint(markdown: string, maxLength = 5000): number {
  if (markdown.length <= maxLength) {
    return markdown.length;
  }

  // コードブロックの整合性を最優先で維持
  const codeBlockMatches = Array.from(markdown.matchAll(/```[\s\S]*?```/g));
  for (const match of codeBlockMatches) {
    const blockEnd = match.index! + match[0].length;
    if (blockEnd > maxLength) {
      // コードブロックが maxLength を超える場合、その前で分割
      if (match.index! > 0) {
        return match.index!;
      }
      break;
    }
  }

  // 段落境界での分割を試みる
  const lastDoubleNewline = markdown.lastIndexOf('\n\n', maxLength);
  if (lastDoubleNewline > maxLength * 0.5) {
    return lastDoubleNewline + 2;
  }

  // 単一改行での分割
  const lastNewline = markdown.lastIndexOf('\n', maxLength);
  if (lastNewline > maxLength * 0.7) {
    return lastNewline + 1;
  }

  // フォールバック: 最大長で分割
  return maxLength;
}

8. シェルコマンド実行とサンドボックス

シェルツールの実装

ShellTool クラス(packages/core/src/tools/shell.ts

// 注: 実際の実装では、サンドボックス機能は ShellTool 内部ではなく、
// CLI レベルで実装されています。ShellTool は通常のシェルコマンドを実行するだけです。

export class ShellTool extends BaseTool<ShellToolParams, ToolResult> {
  static readonly Name = 'run_shell_command';
  private whitelist: Set<string> = new Set(); // 承認済みコマンド
  
  async execute(
    params: ShellToolParams,
    abortSignal: AbortSignal,
    updateOutput?: (output: string) => void,
  ): Promise<ToolResult> {
    // 1. パラメータ検証
    const validationError = this.validateToolParams(params);
    if (validationError) {
      throw new Error(validationError);
    }
    
    // 2. 作業ディレクトリの設定(サンドボックス関連のコードは存在しない)
    const cwd = params.directory 
      ? path.resolve(this.config.getTargetDir(), params.directory)
      : this.config.getTargetDir();
    
    // 3. プロセスの起動(OutputBuffer クラスは存在しない)
    let output = '';
    const appendOutput = (data: Buffer | string) => {
      const text = data.toString();
      output += text;
      if (updateOutput) {
        updateOutput(output);
      }
    };
    
    try {
      const result = await this.executeCommand({
        command: params.command,
        cwd,
        signal: abortSignal,
        onStdout: appendOutput,
        onStderr: appendOutput,
      });
      
      return {
        llmContent: `Command executed with exit code ${result.exitCode}`,
        returnDisplay: output,
      };
    } catch (error) {
      // エラーハンドリング
      throw error;
    }
  }
  
  private async executeCommand(options: ExecuteOptions): Promise<ExecuteResult> {
    return new Promise((resolve, reject) => {
      // Windows と Unix でコマンド実行方法が異なる
      const isWindows = os.platform() === 'win32';
      const shell = isWindows 
        ? spawn('cmd.exe', ['/c', options.command], {
            cwd: options.cwd,
            windowsHide: true,
          })
        : spawn('bash', ['-c', options.command], {
            cwd: options.cwd,
            detached: true, // プロセスグループ作成
          });
      
      // 出力ハンドリング
      shell.stdout?.on('data', options.onStdout);
      shell.stderr?.on('data', options.onStderr);
      
      // シグナルハンドリング
      options.signal.addEventListener('abort', () => {
        if (isWindows) {
          // Windows では taskkill を使用
          spawn('taskkill', ['/pid', shell.pid!.toString(), '/f', '/t']);
        } else {
          // Unix系ではプロセスグループ全体を終了
          process.kill(-shell.pid!, 'SIGTERM');
        }
      });
      
      shell.on('exit', (code) => {
        resolve({ exitCode: code || 0 });
      });
      
      shell.on('error', reject);
    });
  }
}

サンドボックスの実装

重要: 実際の実装では、サンドボックス機能は ShellTool 内部ではなく、CLI レベルで実装されています。

実際のサンドボックス実装(packages/cli/src/utils/sandbox.ts

// CLI起動時にプロセス全体をサンドボックス化する
export async function start_sandbox(
  config: SandboxConfig,
  nodeArgs: string[] = [],
) {
  const containerWorkdir = '/home/node/app';
  const workdir = fs.realpathSync(process.cwd());
  
  // Docker/Podman コンテナ内で Gemini CLI 全体を実行
  const args = ['run', '-i', '--rm', '--init', '--workdir', containerWorkdir];
  
  // ボリュームマウント
  args.push('--volume', `${workdir}:${containerWorkdir}`);
  
  // 環境変数の転送(個別に指定)
  if (process.env.GEMINI_API_KEY) {
    args.push('--env', `GEMINI_API_KEY=${process.env.GEMINI_API_KEY}`);
  }
  if (process.env.GOOGLE_API_KEY) {
    args.push('--env', `GOOGLE_API_KEY=${process.env.GOOGLE_API_KEY}`);
  }
  // その他の必要な環境変数も同様に転送
  
  // ネットワーク設定(プロキシサポート)
  if (process.env.GEMINI_SANDBOX_PROXY_COMMAND) {
    // プロキシコンテナと内部ネットワークで接続
    args.push('--network', 'container:gemini-proxy');
  }
  
  // ユーザー権限管理(Linux環境)
  if (shouldUseCurrentUserInSandbox()) {
    args.push('--user', `${process.getuid()}:${process.getgid()}`);
  }
  
  // イメージとコマンド
  args.push(config.image, 'node', ...nodeArgs, '/app/bin/gemini.js');
  
  // コンテナ起動
  spawn(config.command, args, { stdio: 'inherit' });
}

// macOS Seatbelt サンドボックス
export async function startMacOSSandbox() {
  const profileFile = await createSandboxProfile();
  const args = [
    '-D', `TARGET_DIR=${fs.realpathSync(process.cwd())}`,
    '-f', profileFile,
    'sh', '-c',
    `exec node ${process.argv.slice(1).join(' ')}`,
  ];
  
  spawn('sandbox-exec', args, { stdio: 'inherit' });
}

SandboxConfig インターフェース(packages/core/src/types.ts

// 実際の SandboxConfig はよりシンプル
export interface SandboxConfig {
  command: 'docker' | 'podman' | 'sandbox-exec';
  image: string; // コンテナイメージ(docker/podman用)

サンドボックスの種類と特徴

タイプ セキュリティレベル パフォーマンス 使用場面
Docker 高(完全隔離) プロダクション環境
Podman 高(ルートレス) セキュリティ重視
macOS Sandbox 中(部分隔離) macOS開発環境
None 低(直接実行) 最高 信頼できる環境

カスタムDockerイメージ

# 実際のDockerfile(プロジェクトルート)
FROM docker.io/library/node:20-slim

# 最小限のパッケージセット
RUN apt-get update && apt-get install -y --no-install-recommends \
  python3 make g++ man-db curl dnsutils less jq bc gh git \
  unzip rsync ripgrep procps psmisc lsof socat ca-certificates \
  && apt-get clean && rm -rf /var/lib/apt/lists/*

# 非rootユーザー(node)で実行
USER node
WORKDIR /home/node

# gemini-cliをインストール
COPY packages/cli/dist/google-gemini-cli-*.tgz /tmp/
RUN npm install -g /tmp/google-gemini-cli-*.tgz && \
    rm /tmp/google-gemini-cli-*.tgz

# 環境設定
ENV NODE_ENV=production
ENTRYPOINT ["gemini"]

環境変数の管理

主要な環境変数

環境変数 説明 必須
GEMINI_API_KEY Gemini APIキー export GEMINI_API_KEY="YOUR_API_KEY"
GEMINI_MODEL デフォルトモデルの指定 export GEMINI_MODEL="gemini-2.5-pro"
GOOGLE_API_KEY Google Cloud APIキー(Vertex AI Express) export GOOGLE_API_KEY="YOUR_KEY"
GOOGLE_CLOUD_PROJECT Google CloudプロジェクトID export GOOGLE_CLOUD_PROJECT="project-id"
GOOGLE_CLOUD_LOCATION Google Cloudリージョン export GOOGLE_CLOUD_LOCATION="us-central1"
GOOGLE_APPLICATION_CREDENTIALS サービスアカウント認証情報パス export GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json"
GEMINI_SANDBOX サンドボックスモード export GEMINI_SANDBOX=docker
SEATBELT_PROFILE macOSサンドボックスプロファイル export SEATBELT_PROFILE=strict
DEBUG デバッグモード有効化(--debugフラグも使用可) export DEBUG=true
NO_COLOR カラー出力無効化 export NO_COLOR=1
CLI_TITLE CLIタイトルのカスタマイズ(注: 実装に見つからない) export CLI_TITLE="My Gemini CLI"
GEMINI_SANDBOX_PROXY_COMMAND カスタムプロキシコマンド export GEMINI_SANDBOX_PROXY_COMMAND="proxy-command"

: 実装では明示的な SAFE_ENV_VARS ホワイトリストは存在せず、必要な環境変数は個別に転送されます。


9. 認証とセキュリティ

認証方式

  1. Google OAuth認証

    // 個人Googleアカウントでのログイン
    // レート制限: 1分60リクエスト、1日1000リクエスト
    // Flash modelへの自動フォールバック機能付き
  2. Gemini APIキー

    // 環境変数: GEMINI_API_KEY
    // より高いレート制限
    // モデル選択の自由度
  3. Vertex AI

    // 環境変数: GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION
    // エンタープライズ向け
    // 高度なセキュリティとコンプライアンス

セキュリティ機能

  • ツール実行の承認: 変更操作は必ず確認
  • コマンドのホワイトリスト: 繰り返し実行の許可
  • ディレクトリ検証: 絶対パスの禁止
  • プロセス管理: 適切なクリーンアップ

10. MCP(Model Context Protocol)統合

MCPサーバーの接続方式

  1. Stdio(標準入出力)

    // サブプロセスとして起動
    const transport = new StdioServerTransport({
      command: 'mcp-server',
      args: ['--mode', 'stdio']
    });
  2. SSE(Server-Sent Events)

    // HTTPベースの接続
    // 注: 実装では SSEClientTransport として実装
    const transport = new SSEClientTransport({
      url: 'http://localhost:3000/sse'
    });
  3. StreamableHTTP(ストリーミングHTTPトランスポート)

    // バイナリストリーミング対応のHTTPトランスポート
    const transport = new StreamableHTTPClientTransport({
      url: 'http://localhost:3000/rpc',
      requestOptions: {
        headers: {
          'Authorization': 'Bearer token'
        }
      }
    });

MCPサーバーの状態管理

// MCPサーバーのステータス管理(packages/core/src/tools/mcp-server-status.ts)
// 注: MCPServerStatus は enum として定義されています
export enum MCPServerStatus {
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
  DISCONNECTED = 'disconnected',
  ERROR = 'error',
}

// MCPServerManager クラスは存在せず、mcp-client.ts 内の
// モジュールスコープ変数と関数で状態管理されています:

// モジュールレベルの状態管理
const mcpServerStatusesInternal = new Map<string, {
  name: string;
  status: MCPServerStatus;
  error?: string;
  toolCount?: number;
  lastConnected?: Date;
}>();

const mcpDiscoveryState: MCPDiscoveryState = {
  inProgress: false,
  error: undefined,
};

// エクスポートされた状態管理関数
export function updateMCPServerStatus(
  name: string,
  status: Partial<{
    status: MCPServerStatus;
    error?: string;
    toolCount?: number;
    lastConnected?: Date;
  }>,
): void {
  const current = mcpServerStatusesInternal.get(name) || {
    name,
    status: MCPServerStatus.DISCONNECTED
  };
  mcpServerStatusesInternal.set(name, { ...current, ...status });
  // イベントエミッタで変更を通知
  emitMCPStatusChangedEvent();
}

export function getMCPServerStatus(name: string) {
  return mcpServerStatusesInternal.get(name);
}

export function getAllMCPServerStatuses() {
  return Array.from(mcpServerStatusesInternal.values());
}

ツール名の管理

// 注: sanitizeToolName と resolveNameConflict という名前の関数は実装に存在しません。
// 実際の実装では、MCPツールの名前管理は mcp-tool.ts 内で以下のように処理されます:

// MCPツールクラス内での名前生成
export class MCPTool extends BaseTool<any, ToolResult> {
  constructor(
    private readonly serverName: string,
    private readonly toolName: string,
    private readonly toolConfig: ToolConfig,
    config: Config,
  ) {
    super(config);
  }
  
  // 内部API名の生成(server名とtool名を組み合わせ)
  get name(): string {
    return `${this.serverName}_${this.toolName}`;
  }
  
  // 表示名はツール設定から取得
  get displayName(): string {
    return this.toolConfig.name || this.toolName;
  }
}

// ツール登録時の重複チェックは ToolRegistry で実施

11. メモリ管理機能

GEMINI.mdファイルの仕組み

  1. グローバルメモリ: ~/.gemini/GEMINI.md
  2. プロジェクトメモリ: 作業ディレクトリのGEMINI.md
  3. カスタムメモリ: 設定で指定した追加ファイル

メモリの自動更新

// メモリツールの実行後に自動リフレッシュ
if (toolName === 'save_memory' && result.status === 'success') {
  await refreshMemory();
  // システムプロンプトを再構築
  rebuildSystemPrompt();
}

メモリの検索と読み込み

// 親ディレクトリを遡って検索
async function discoverMemoryFiles(startDir: string): Promise<string[]> {
  const memoryFiles = [];
  let currentDir = startDir;
  
  while (currentDir !== '/') {
    const geminiMdPath = path.join(currentDir, 'GEMINI.md');
    if (await fileExists(geminiMdPath)) {
      memoryFiles.push(geminiMdPath);
    }
    currentDir = path.dirname(currentDir);
  }
  
  return memoryFiles;
}

12. テレメトリとモニタリング

OpenTelemetry統合

テレメトリ実装(packages/core/src/telemetry/sdk.ts

Gemini CLIでは、OpenTelemetryの標準的な NodeSDK を使用してテレメトリを実装しています。

// 注: 実際のファイル名は sdk.ts で、telemetry.ts ではありません
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { 
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

// テレメトリの初期化
export function initializeTelemetry(config: TelemetryConfig): NodeSDK {
  const resource = new Resource({
    [ATTR_SERVICE_NAME]: 'gemini-cli',
    [ATTR_SERVICE_VERSION]: config.version,
    // 注: SERVICE_INSTANCE_ID は実装で使用されていません
  });

  // OpenTelemetry Node SDKの設定
  const sdk = new NodeSDK({
    resource,
    traceExporter: new OTLPTraceExporter({
      url: `${config.otlpEndpoint}/v1/traces`,
      headers: config.headers,
    }),
    metricReader: new PeriodicExportingMetricReader({
      exporter: new OTLPMetricExporter({
        url: `${config.otlpEndpoint}/v1/metrics`,
        headers: config.headers,
      }),
      exportIntervalMillis: 10000,
    }),
    instrumentations: [
      // 自動計装(HTTP、gRPCなど)
      getNodeAutoInstrumentations(),
    ],
  });

  return sdk;
}

// メトリクス定義
// 注: 実際の関数名は initializeMetrics で、createMetrics ではありません
export function initializeMetrics(meter: Meter): TelemetryMetrics {
  return {
    // 実際のメトリクス名は文書と異なります
    toolDuration: meter.createHistogram('gemini_cli.tool.duration', {
      description: 'Duration of tool execution',
      unit: 'ms',
    }),
    
    toolErrors: meter.createCounter('gemini_cli.tool.errors', {
      description: 'Number of tool errors',
    }),
    
    apiLatency: meter.createHistogram('gemini_cli.api.latency', {
      description: 'API call latency',
      unit: 'ms',
    }),
    
    apiErrors: meter.createCounter('gemini_cli.api.errors', {
      description: 'Number of API errors',
    }),
    
    turnTokens: meter.createHistogram('gemini_cli.turn.tokens', {
      description: 'Token usage per turn',
    }),
    
    // セッション全体のメトリクス
    sessionDuration: meter.createHistogram('gemini_cli.session.duration', {
      description: 'Duration of interactive session',
      unit: 's',
    }),
  };
}

// トレース作成のヘルパー関数
export function startSpan(name: string, attributes?: Attributes): Span {
  return trace.getTracer('gemini-cli').startSpan(name, { attributes });
}

// グレースフルシャットダウン
export async function shutdownTelemetry(sdk: NodeSDK): Promise<void> {
  await sdk.shutdown();
}

使用統計の収集

// packages/cli/src/ui/contexts/SessionContext.tsx
// 注: 実際の実装では SessionStats ではなく CumulativeStats が使用されています

export interface CumulativeStats {
  turnCount: number;                // turnsCount ではなく turnCount
  promptTokenCount: number;         // プロンプトトークン
  candidatesTokenCount: number;     // 生成トークン
  totalTokenCount: number;          // 合計トークン
  cachedContentTokenCount: number;  // キャッシュトークン
  toolUsePromptTokenCount: number;  // ツール使用プロンプトトークン
  thoughtsTokenCount: number;       // 思考トークン
  apiTimeMs: number;                // API実行時間(ミリ秒)
}

interface SessionStatsState {
  sessionStartTime: Date;
  cumulative: CumulativeStats;      // セッション全体の累積
  currentTurn: CumulativeStats;     // 現在のターンの統計
  currentResponse: CumulativeStats; // 現在のレスポンスの統計
}

export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [stats, setStats] = useState<SessionStatsState>({
    sessionStartTime: new Date(),
    cumulative: {
      turnCount: 0,
      promptTokenCount: 0,
      candidatesTokenCount: 0,
      totalTokenCount: 0,
      cachedContentTokenCount: 0,
      toolUsePromptTokenCount: 0,
      thoughtsTokenCount: 0,
      apiTimeMs: 0,
    },
    currentTurn: { /* 同じ構造 */ },
    currentResponse: { /* 同じ構造 */ },
  });
  
  // 新しいターンの開始
  const startNewTurn = useCallback(() => {
    setStats((prevState) => ({
      ...prevState,
      cumulative: {
        ...prevState.cumulative,
        turnCount: prevState.cumulative.turnCount + 1,
      },
      currentTurn: {
        turnCount: 0, // リセット
        promptTokenCount: 0,
        candidatesTokenCount: 0,
        totalTokenCount: 0,
        cachedContentTokenCount: 0,
        toolUsePromptTokenCount: 0,
        thoughtsTokenCount: 0,
        apiTimeMs: 0,
      },
    }));
  }, []);
  
  // 使用量の追加(addTokens ヘルパー関数を使用)
  const addUsage = useCallback((
    metadata: GenerateContentResponseUsageMetadata & { apiTimeMs?: number }
  ) => {
    setStats((prevState) => {
      const newCumulative = { ...prevState.cumulative };
      const newCurrentTurn = { ...prevState.currentTurn };
      const newCurrentResponse = { /* 新規作成 */ };
      
      // addTokens ヘルパーですべての統計を更新
      addTokens(newCurrentTurn, metadata);
      addTokens(newCumulative, metadata);
      addTokens(newCurrentResponse, metadata);
      
      return {
        ...prevState,
        cumulative: newCumulative,
        currentTurn: newCurrentTurn,
        currentResponse: newCurrentResponse,
      };
    });
  }, []);
  
  return (
    <SessionStatsContext.Provider value={{ stats, startNewTurn, addUsage }}>
      {children}
    </SessionStatsContext.Provider>
  );
};

パフォーマンスモニタリング

// packages/core/src/telemetry/performance.ts
export class PerformanceMonitor {
  private spans = new Map<string, Span>();
  
  startOperation(name: string, attributes?: Record<string, any>): void {
    const span = telemetry.startSpan(name, attributes);
    this.spans.set(name, span);
  }
  
  endOperation(name: string, status?: SpanStatus): void {
    const span = this.spans.get(name);
    if (span) {
      if (status) {
        span.setStatus(status);
      }
      span.end();
      this.spans.delete(name);
    }
  }
  
  async measureAsync<T>(
    name: string,
    operation: () => Promise<T>,
    attributes?: Record<string, any>
  ): Promise<T> {
    this.startOperation(name, attributes);
    try {
      const result = await operation();
      this.endOperation(name, { code: SpanStatusCode.OK });
      return result;
    } catch (error) {
      this.endOperation(name, {
        code: SpanStatusCode.ERROR,
        message: error.message,
      });
      throw error;
    }
  }
}

13. 開発のベストプラクティス

コード構造とパターン

1. TypeScriptの活用

// 厳密な型定義を使用
export interface ToolParams {
  file_path: string;
  content: string;
}

// ユニオン型で状態を表現
type ToolStatus = 
  | 'validating'
  | 'awaiting_approval'
  | 'scheduled'
  | 'executing'
  | 'success'
  | 'error'
  | 'cancelled';

// 型ガードの使用
function isSuccessResult(result: ToolResult): result is SuccessResult {
  return result.status === 'success';
}

2. エラーハンドリング

// カスタムエラークラス
export class ToolExecutionError extends Error {
  constructor(
    message: string,
    public readonly toolName: string,
    public readonly originalError?: unknown
  ) {
    super(message);
    this.name = 'ToolExecutionError';
  }
}

// エラーハンドリングパターン
async function executeWithRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  let lastError: Error;
  
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;
      
      if (i < maxRetries) {
        await delay(Math.pow(2, i) * 1000); // Exponential backoff
      }
    }
  }
  
  throw lastError!;
}

3. React/Inkのベストプラクティス

// メモ化を活用
const MemoizedMessage = React.memo<MessageProps>(({ message }) => {
  const theme = useTheme();
  
  return (
    <Box flexDirection="column">
      <Text color={theme.colors.muted}>
        {formatTimestamp(message.timestamp)}
      </Text>
      <Text>{message.content}</Text>
    </Box>
  );
}, (prevProps, nextProps) => {
  // カスタム比較関数
  return prevProps.message.id === nextProps.message.id;
});

// カスタムフックの作成
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

テスト戦略

1. ユニットテスト

// packages/core/src/tools/__tests__/edit.test.ts
describe('EditTool', () => {
  let editTool: EditTool;
  let mockConfig: Config;
  
  beforeEach(() => {
    vi.resetAllMocks();
    mockConfig = createMockConfig();
    editTool = new EditTool(mockConfig);
  });
  
  describe('validateToolParams', () => {
    it('should reject relative paths', () => {
      const result = editTool.validateToolParams({
        file_path: './relative/path.ts',
        old_string: 'old',
        new_string: 'new',
      });
      
      expect(result).toBe('File path must be absolute');
    });
    
    it('should accept absolute paths within root', () => {
      const result = editTool.validateToolParams({
        file_path: '/project/src/file.ts',
        old_string: 'old',
        new_string: 'new',
      });
      
      expect(result).toBeNull();
    });
  });
});

2. 統合テスト

// integration-tests/file-system.test.js
test('file editing workflow', async () => {
  const testFile = path.join(SANDBOX_DIR, 'test.txt');
  
  // ファイル作成
  await runTool('write_file', {
    file_path: testFile,
    content: 'Hello World',
  });
  
  // 編集
  await runTool('replace', {
    file_path: testFile,
    old_string: 'World',
    new_string: 'Gemini',
  });
  
  // 確認
  const result = await runTool('read_file', {
    file_path: testFile,
  });
  
  expect(result.result).toContain('Hello Gemini');
});

セキュリティベストプラクティス

  1. 環境変数の管理

    • APIキーは環境変数で管理
    • .envファイルは.gitignoreに追加
    • 機密情報のログ出力を避ける
  2. 入力検証

    • すべてのユーザー入力を検証
    • パス・トラバーサル攻撃の防止
    • コマンドインジェクションの防止
  3. 最小権限の原則

    • 必要最小限の権限で実行
    • サンドボックスをデフォルトで有効化
    • 危険な操作には明示的な承認を要求

14. CLIの使用方法と設定

コマンドラインオプション

オプション 短縮形 説明
--model -m 使用するGeminiモデルを指定 npm start -- --model gemini-1.5-pro-latest
--prompt -p 非対話モードでプロンプトを直接渡す npm start -- -p "Hello"
--sandbox -s サンドボックスモードを有効化 npm start -- -s
--sandbox-image サンドボックスイメージのURI npm start -- --sandbox-image custom-image
--debug -d デバッグモードを有効化 npm start -- -d
--all_files -a 現在のディレクトリの全ファイルをコンテキストに含める npm start -- -a
--help -h ヘルプ情報を表示 npm start -- -h
--show_memory_usage メモリ使用量を表示 npm start -- --show_memory_usage
--yolo すべてのツール呼び出しを自動承認 npm start -- --yolo
--telemetry テレメトリを有効化 npm start -- --telemetry
--telemetry-target テレメトリ送信先を設定 npm start -- --telemetry-target local
--telemetry-otlp-endpoint OTLPエンドポイントを設定 npm start -- --telemetry-otlp-endpoint http://localhost:4317
--telemetry-log-prompts プロンプトのログを有効化 npm start -- --telemetry-log-prompts
--checkpointing チェックポイント機能を有効化 npm start -- --checkpointing
--version バージョンを表示 npm start -- --version

設定ファイル(settings.json)

主要な設定項目

{
  // UI関連
  "theme": "GitHub",
  "preferredEditor": "vscode",
  
  // コンテキストファイル
  "contextFileName": "GEMINI.md",
  
  // ファイルフィルタリング
  "fileFiltering": {
    "respectGitIgnore": true,
    "enableRecursiveFileSearch": true
  },
  
  // ツール設定
  "coreTools": ["read_file", "write_file", "replace"],
  "excludeTools": ["run_shell_command"],
  "autoAccept": false,
  
  // サンドボックス
  "sandbox": "docker",
  
  // カスタムツール
  "toolDiscoveryCommand": "bin/get_tools",
  "toolCallCommand": "bin/call_tool",
  
  // MCPサーバー
  "mcpServers": {
    "myServer": {
      "command": "node",
      "args": ["mcp_server.js"],
      "env": {
        "API_KEY": "$MY_API_TOKEN"
      }
    }
  },
  
  // テレメトリ
  "telemetry": {
    "enabled": true,
    "target": "local",
    "otlpEndpoint": "http://localhost:4317",
    "logPrompts": false
  },
  
  // その他
  "usageStatisticsEnabled": true,
  "checkpointing": {
    "enabled": false
  },
  "bugCommand": {
    "urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?title={title}&body={body}"
  }
}

設定の優先順位

  1. コマンドラインオプション(最優先)
  2. 環境変数
  3. ローカル設定(.gemini/settings.json
  4. グローバル設定(~/.gemini/settings.json
  5. デフォルト値

開発コマンド

# 依存関係のインストール
npm install

# ビルド
npm run build              # すべてをビルド
npm run build:packages     # パッケージのみビルド

# テスト
npm test                   # ユニットテスト
npm run test:e2e          # 統合テスト
npm run test:ci           # カバレッジ付きテスト

# 品質チェック
npm run lint              # リンティング
npm run lint:fix          # 自動修正
npm run format            # コードフォーマット
npm run typecheck         # 型チェック
npm run preflight         # 全チェック実行

# デバッグ
npm run debug             # デバッグモードで起動
DEV=true npm start        # React DevTools接続可能

まとめ

Gemini CLIは、以下の特徴を持つ高度なAIアシスタントツールです。

  1. リアルタイムストリーミング: 非同期ジェネレーターベースの効率的な実装
  2. 拡張可能なツールシステム: 明確なインターフェースと状態管理
  3. セキュアな実行環境: サンドボックスと承認フロー
  4. スマートなコンテキスト管理: 自動圧縮とトークン最適化
  5. 柔軟な統合: MCPプロトコルによる外部ツール連携
  6. 包括的なモニタリング: OpenTelemetryによる観測性
  7. 開発者フレンドリー: 明確なアーキテクチャとベストプラクティス

これらの機能は、モジュラーで保守しやすいアーキテクチャで実装されており、将来の拡張にも対応できる設計となっています。開発者は、このドキュメントを参考に、Gemini CLIの内部動作を理解し、新機能の追加や既存機能の改善を効率的に行うことができます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment