開発者の思考プロセスは、今やターミナル上の AI との対話の中にこそ最も色濃く反映される。しかし、ターミナルは「フロー(流れ)」の場であり、知識はコマンドとともに流れて消えていく。
一方で Obsidian などのノートアプリは「ストック(蓄積)」の場である。
このシステムの本質は、「フローの場」であるターミナルから一歩も出ることなく、自動的に「ストックの場」へ知識を永続化させるパイプラインの構築にある。
元記事の Claude Code はログを単純なテキストとして追記する形式だったが、Gemini CLI はより現代的かつ複雑な JSON 構造を持つ。さらに、マルチセッション(複数タブでの同時作業)に対応するため、独自の監視アーキテクチャを採用した。
Gemini CLI は会話のたびに、プロジェクト固有のディレクトリに JSON ログを「上書き保存」する。しかも、タブ(プロジェクト)を変えるとログファイル自体が変わる。
ログが追記型ではないため、「どこまで読んだか」を行数で管理することはできない。代わりに、メッセージ固有の UUIDを利用する。
~/.gemini/archiver_states/<SESSION_ID>.state を作成し、最後に読み込んだメッセージ UUID を記録する。jq を駆使し、「記録済み UUID より後ろにあるメッセージ」だけをスライスして抽出する。このロジックにより、「どのタブで」「どの順番で」会話しても、重複なく、漏れなく、Obsidian に集約される。
Obsidian のデイリーノート(yyyy/mm/dd/📓...gemini-log.md)へ集約される。複数の文脈が一本のタイムラインに統合されることで、その日の思考の流れが「時間軸」で可視化される。
gemini_archiver.sh)#!/bin/bash
# gemini_archiver.sh
# Monitors Gemini CLI chat logs and archives them to Obsidian daily notes.
# Multi-session support: Tracks each session independently.
GEMINI_TMP_BASE="$HOME/.gemini/tmp"
OBSIDIAN_ROOT="$HOME/dotfiles/zettelkasten"
STATE_DIR="$HOME/.gemini/archiver_states"
# Ensure jq is available
if ! command -v jq > /dev/null; then
echo "Error: jq is required but not installed." >&2
exit 1
fi
get_date_vars() {
YYYY=$(date +%Y)
MM=$(date +%m)
DD=$(date +%d)
DATE_STR="${YYYY}${MM}${DD}"
}
process_session() {
local session_file="$1"
# Extract Session UUID from JSON content
local session_id=$(jq -r '.sessionId // empty' "$session_file" 2>/dev/null)
if [ -z "$session_id" ]; then
return
fi
local state_file="$STATE_DIR/${session_id}.state"
local last_id=""
if [ -f "$state_file" ]; then
last_id=$(cat "$state_file")
fi
# Check if session file has messages
local has_messages=$(jq '.messages | length' "$session_file" 2>/dev/null)
if [ -z "$has_messages" ] || [ "$has_messages" -eq 0 ]; then
return
fi
# Construct jq filter
local jq_filter
if [ -z "$last_id" ]; then
# New session tracking: Get ALL messages
jq_filter='.messages[] | select(.type == "user" or .type == "gemini")'
else
# Existing session: Get messages AFTER last_id
# Safety: If last_id is missing (null index), output empty to avoid duplication
jq_filter='.messages as $m | ($m | to_entries | map(select(.value.id == "'"$last_id"'")) | .[0].key) as $idx | if $idx == null then empty else $m[$idx+1:][] end | select(.type == "user" or .type == "gemini")'
fi
local tmp_output=$(mktemp)
# Run jq
jq -r "$jq_filter | \"**\" + (.type | ascii_upcase) + \"**: \" + .content + \"\n\"" "$session_file" > "$tmp_output"
if [ -s "$tmp_output" ]; then
get_date_vars
local target_dir="$OBSIDIAN_ROOT/dagnetz/$YYYY/$MM/$DD"
local target_file="$target_dir/📓${DATE_STR}_gemini-log.md"
mkdir -p "$target_dir"
if [ ! -f "$target_file" ]; then
echo "# $YYYY-$MM-$DD Gemini Log" > "$target_file"
fi
# Append newline and content
echo "" >> "$target_file"
cat "$tmp_output" >> "$target_file"
# Update state file with the NEW last ID
local new_last_id=$(jq -r '.messages[] | select(.type == "user" or .type == "gemini") | .id' "$session_file" | tail -1)
if [ -n "$new_last_id" ]; then
echo "$new_last_id" > "$state_file"
fi
fi
rm "$tmp_output"
}
# Main Loop
while true; do
# Find ALL session files modified in the last 60 minutes
find "$GEMINI_TMP_BASE" -type f -path "*/chats/session-*.json" -mmin -60 2>/dev/null | while read -r session_file; do
process_session "$session_file"
done
sleep 5
done
sessionId を読み取り、それに対応する .state ファイルを動的に参照する。
これにより、1つのスクリプトプロセスで、N個の同時進行セッションを完璧に捌くことができる。
システムが意識されるようでは不完全である。ターミナルを開いた瞬間に、空気のように裏で動き出す必要がある。
.bashrc や .zshrc に以下を仕込むことで、特別なデーモン設定(systemd等)すら不要になる。
# 既に動いていなければ、裏でそっと起動する
if ! pgrep -f "gemini_archiver.sh" > /dev/null; then
~/dotfiles/shellscripts/gemini_archiver.sh > /dev/null 2>&1 &
fi
pgrep) により、タブを100個開いても監視プロセスは常に「1つ」に保たれる。リソース消費は最小限である。
このシステムを導入することで、以下のパラダイムシフトが発生する。
[[📓20260112_gemini-log]])ことで、その日の思考の文脈をいつでも参照できるようになる。