Skip to content

Instantly share code, notes, and snippets.

@zinknovo
Created March 15, 2026 22:37
Show Gist options
  • Select an option

  • Save zinknovo/5fbd2eddcbf7bc5dd89c0dee59b05eb7 to your computer and use it in GitHub Desktop.

Select an option

Save zinknovo/5fbd2eddcbf7bc5dd89c0dee59b05eb7 to your computer and use it in GitHub Desktop.
Gemini CLI wrapper with session naming and cross-project listing
#!/usr/bin/env bash
# gem - Gemini CLI wrapper with session naming
# Usage:
# gem Start a new Gemini session
# gem ls List sessions with names (current project)
# gem ls -a List sessions across all projects
# gem r <name|index> Resume a session by name or index
# gem rn <index> <name> Rename a session
# gem rm <name|index> Delete a session
set -euo pipefail
# Auto-check compatibility on version change
_check_version() {
local compat_dir="$HOME/.local/share/cli-compat"
mkdir -p "$compat_dir"
local ver_file="$compat_dir/gemini.ver"
local cur_ver
cur_ver=$(gemini --version 2>/dev/null | head -1 || echo "unknown")
local last_ver=""
[ -f "$ver_file" ] && last_ver=$(cat "$ver_file")
if [ "$cur_ver" != "$last_ver" ]; then
echo "$cur_ver" > "$ver_file"
[ -n "$last_ver" ] && echo -e "\033[0;33mGemini CLI updated: $last_ver → $cur_ver\033[0m" && check-cli-compat 2>/dev/null || true
fi
}
_check_version &
NAMES_DIR="$HOME/.gemini/session-names"
mkdir -p "$NAMES_DIR"
# Get project hash for current directory (matches Gemini's logic)
get_project_dir() {
local dir
for dir in "$HOME/.gemini/tmp"/*/chats; do
[ -d "$dir" ] && echo "$(dirname "$dir")" && return
done
}
# Find the chats dir for the current working directory
find_chats_dir() {
local found=""
local cwd="$PWD"
# Gemini uses project-specific dirs under ~/.gemini/tmp/
# We check which project dir's sessions reference our cwd
for dir in "$HOME/.gemini/tmp"/*/chats; do
[ -d "$dir" ] && found="$dir" # fallback to last found
# Check if any session in this dir matches our project
local sample
sample=$(ls -t "$dir"/*.json 2>/dev/null | head -1)
if [ -n "$sample" ]; then
local hash
hash=$(python3 -c "import json; print(json.load(open('$sample')).get('projectHash',''))" 2>/dev/null)
local dirname
dirname=$(basename "$(dirname "$dir")")
# Match by directory name heuristic
local basename_cwd
basename_cwd=$(basename "$cwd")
if [[ "$dirname" == "$basename_cwd"* ]] || [[ "$dirname" == "$hash"* ]]; then
echo "$dir"
return
fi
fi
done
# If no match, check all
echo ""
}
# Get names file for a project
names_file() {
local project_id="${1:-default}"
echo "$NAMES_DIR/$project_id.json"
}
# Read name for a session
get_name() {
local project_id="$1" session_id="$2"
local nf
nf=$(names_file "$project_id")
[ -f "$nf" ] && python3 -c "
import json, sys
nf, sid = sys.argv[1], sys.argv[2]
d = json.load(open(nf))
print(d.get(sid, ''))
" "$nf" "$session_id" 2>/dev/null || echo ""
}
# Set name for a session
set_name() {
local project_id="$1" session_id="$2" name="$3"
local nf
nf=$(names_file "$project_id")
python3 -c "
import json, os, sys
nf, sid, name = sys.argv[1], sys.argv[2], sys.argv[3]
d = {}
if os.path.exists(nf):
d = json.load(open(nf))
d[sid] = name
json.dump(d, open(nf, 'w'), indent=2, ensure_ascii=False)
" "$nf" "$session_id" "$name"
}
# Remove name for a session
rm_name() {
local project_id="$1" session_id="$2"
local nf
nf=$(names_file "$project_id")
[ -f "$nf" ] && python3 -c "
import json, os, sys
nf, sid = sys.argv[1], sys.argv[2]
d = json.load(open(nf))
d.pop(sid, None)
json.dump(d, open(nf, 'w'), indent=2, ensure_ascii=False)
" "$nf" "$session_id" 2>/dev/null
}
# List sessions with names
cmd_list() {
local all_projects=false
[ "${1:-}" = "-a" ] && all_projects=true
python3 - "$all_projects" <<'PYEOF'
import json, os, sys, glob
from datetime import datetime, timezone
all_projects = sys.argv[1] == "true"
home = os.path.expanduser("~")
names_dir = os.path.join(home, ".gemini", "session-names")
tmp_dir = os.path.join(home, ".gemini", "tmp")
cwd = os.getcwd()
cwd_base = os.path.basename(cwd)
sessions = []
for project_dir in sorted(glob.glob(os.path.join(tmp_dir, "*", "chats"))):
project_id = os.path.basename(os.path.dirname(project_dir))
# Show all projects by default since directory heuristic is unreliable
names_file = os.path.join(names_dir, f"{project_id}.json")
names = {}
if os.path.exists(names_file):
names = json.load(open(names_file))
# Read actual project root path
project_root_file = os.path.join(os.path.dirname(project_dir), ".project_root")
project_root = project_id
if os.path.exists(project_root_file):
project_root = open(project_root_file).read().strip()
for f in sorted(glob.glob(os.path.join(project_dir, "*.json")), reverse=True):
try:
d = json.load(open(f))
sid = d.get("sessionId", "")
summary = d.get("summary", "")
start = d.get("startTime", "")
msgs = len(d.get("messages", []))
custom_name = names.get(sid, "")
# Parse time
time_str = ""
if start:
try:
dt = datetime.fromisoformat(start.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
diff = now - dt
if diff.days > 0:
time_str = f"{diff.days}d ago"
elif diff.seconds > 3600:
time_str = f"{diff.seconds // 3600}h ago"
else:
time_str = f"{diff.seconds // 60}m ago"
except:
time_str = start[:16]
sessions.append({
"project": project_id,
"project_root": project_root,
"sid": sid,
"name": custom_name,
"summary": summary,
"time": time_str,
"msgs": msgs,
})
except:
continue
if not sessions:
print(" No sessions found.")
sys.exit(0)
# Sort: for lsa group by project then by time; for ls just by time
if all_projects:
# Group by project, within each group sort by time desc
from collections import OrderedDict
groups = OrderedDict()
for s in sessions:
groups.setdefault(s["project"], []).append(s)
sessions = []
for proj_sessions in groups.values():
sessions.extend(proj_sessions)
current_project = ""
for i, s in enumerate(sessions, 1):
pr = s['project_root'].replace(home, '~', 1) if s['project_root'].startswith(home) else s['project_root']
if all_projects and s["project"] != current_project:
current_project = s["project"]
print(f" \033[0;33m{pr}\033[0m")
summary_display = s["summary"][:50] if s["summary"] else "(no summary)"
if s["name"]:
summary_display = s["summary"][:40] if s["summary"] else ""
proj = f"\033[0;33m{pr}\033[0m" if not all_projects else ""
line = f" {i:>3}. {s['time']:>8} {s['msgs']:>3} msgs"
if not all_projects:
line += f" {proj}"
if s["name"]:
line += f" \033[1;36m{s['name']}\033[0m"
if summary_display:
line += f" ({summary_display})"
else:
line += f" {summary_display}"
print(line)
PYEOF
}
# Find session by name or index
resolve_session() {
local query="$1"
python3 - "$query" <<'PYEOF' | head -1
import json, os, sys, glob
query = sys.argv[1]
home = os.path.expanduser("~")
names_dir = os.path.join(home, ".gemini", "session-names")
tmp_dir = os.path.join(home, ".gemini", "tmp")
cwd_base = os.path.basename(os.getcwd())
sessions = []
for project_dir in sorted(glob.glob(os.path.join(tmp_dir, "*", "chats"))):
project_id = os.path.basename(os.path.dirname(project_dir))
names_file = os.path.join(names_dir, f"{project_id}.json")
names = {}
if os.path.exists(names_file):
names = json.load(open(names_file))
for f in sorted(glob.glob(os.path.join(project_dir, "*.json")), reverse=True):
try:
d = json.load(open(f))
sid = d.get("sessionId", "")
custom_name = names.get(sid, "")
sessions.append({"sid": sid, "name": custom_name, "project": project_id})
except:
continue
# Try by index
try:
idx = int(query) - 1
if 0 <= idx < len(sessions):
print(sessions[idx]["sid"])
sys.exit(0)
except ValueError:
pass
# Try by name (exact match first, then prefix)
for s in sessions:
if s["name"] == query:
print(s["sid"])
sys.exit(0)
for s in sessions:
if s["name"] and query.lower() in s["name"].lower():
print(s["sid"])
sys.exit(0)
print(f"ERROR: No session found matching '{query}'", file=sys.stderr)
sys.exit(1)
PYEOF
}
# Resolve project id for a session
resolve_project_for_session() {
local session_id="$1"
python3 - "$session_id" <<'PYEOF' | head -1
import json, os, glob, sys
sid = sys.argv[1]
home = os.path.expanduser("~")
for f in glob.glob(os.path.join(home, ".gemini", "tmp", "*", "chats", "*.json")):
try:
d = json.load(open(f))
if d.get("sessionId") == sid:
print(os.path.basename(os.path.dirname(os.path.dirname(f))))
break
except:
continue
PYEOF
}
# Find the newest session created after a given timestamp
find_newest_session_after() {
local ts="$1"
python3 - "$ts" <<'PYEOF' | head -1
import json, os, glob, sys
from datetime import datetime, timezone
ts = sys.argv[1]
home = os.path.expanduser("~")
newest = None
newest_time = ts
for f in glob.glob(os.path.join(home, ".gemini", "tmp", "*", "chats", "*.json")):
try:
d = json.load(open(f))
start = d.get("startTime", "")
if start > ts:
if newest is None or start > newest_time:
newest = d.get("sessionId")
newest_time = start
except:
continue
if newest:
print(newest)
PYEOF
}
# Launch gemini and name the session after exit
launch_and_name() {
local session_name="$1"
shift
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%S")
gemini "$@"
# After exit, find the session and name it
local sid
sid=$(find_newest_session_after "$ts")
if [ -n "$sid" ] && [ -n "$session_name" ]; then
local project_id
project_id=$(resolve_project_for_session "$sid")
if [ -n "$project_id" ]; then
set_name "$project_id" "$sid" "$session_name"
echo "Session saved as: $session_name"
fi
fi
}
# Main
case "${1:-}" in
ls)
cmd_list ""
;;
lsa)
cmd_list "-a"
;;
r|resume)
[ -z "${2:-}" ] && echo "Usage: gem r <name|index>" && exit 1
sid=$(resolve_session "$2")
if [ -n "$sid" ]; then
exec gemini --resume "$sid"
fi
;;
rn|rename)
[ -z "${2:-}" ] || [ -z "${3:-}" ] && echo "Usage: gem rn <index> <name>" && exit 1
target="$2"
shift 2
sid=$(resolve_session "$target")
if [ -n "$sid" ]; then
project_id=$(resolve_project_for_session "$sid")
set_name "$project_id" "$sid" "$*"
echo "Named session as: $*"
fi
;;
rm|delete)
[ -z "${2:-}" ] && echo "Usage: gem rm <name|index>" && exit 1
sid=$(resolve_session "$2")
if [ -n "$sid" ]; then
gemini --delete-session "$sid" 2>/dev/null || true
project_id=$(resolve_project_for_session "$sid")
[ -n "$project_id" ] && rm_name "$project_id" "$sid"
echo "Deleted session: $sid"
fi
;;
clear)
echo -n "This will delete ALL Gemini sessions and names. Continue? [y/N] "
read -r -n 1 confirm
echo
if [[ "$confirm" =~ ^[yY]$ ]]; then
find "$HOME/.gemini/tmp" -path "*/chats/*.json" -delete 2>/dev/null
rm -f "$HOME/.gemini/session-names/"*.json 2>/dev/null
echo "All sessions cleared."
else
echo "Cancelled."
fi
;;
n|new)
[ -z "${2:-}" ] && echo "Usage: gem n <name>" && exit 1
shift
launch_and_name "$*"
;;
help|-h|--help)
echo "gem - Gemini CLI wrapper with session naming"
echo ""
echo "Usage:"
echo " gem Start a new Gemini session"
echo " gem n <name> Start a new session with a name"
echo " gem ls List sessions (current project)"
echo " gem lsa List sessions across all projects"
echo " gem r <name|index> Resume session by name or index"
echo " gem rn <index> <name> Rename a session"
echo " gem rm <name|index> Delete a session"
echo " gem clear Delete all sessions"
echo " gem help Show this help"
;;
"")
echo "Usage: gem <command> [args] (try 'gem help')"
;;
*)
echo "Unknown command: $1 (try 'gem help')"
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment