Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion mempalace/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- ChatGPT conversations.json
- Claude Code JSONL
- OpenAI Codex CLI JSONL
- Gemini CLI JSON
- Slack JSON export
- Plain text (pass through for paragraph chunking)

Expand Down Expand Up @@ -71,7 +72,7 @@ def _try_normalize_json(content: str) -> Optional[str]:
except json.JSONDecodeError:
return None

for parser in (_try_claude_ai_json, _try_chatgpt_json, _try_slack_json):
for parser in (_try_claude_ai_json, _try_chatgpt_json, _try_gemini_json, _try_slack_json):
normalized = parser(data)
if normalized:
return normalized
Expand Down Expand Up @@ -237,6 +238,51 @@ def _try_chatgpt_json(data) -> Optional[str]:
return None


def _try_gemini_json(data) -> Optional[str]:
"""Gemini CLI session JSON (~/.gemini/tmp/{hash}/chats/session-*.json).

Sessions are single JSON files with a messages array. User messages have
type "user" with content as a list of {text: "..."} blocks. Assistant
messages have type "gemini" with content as a plain string.
"""
if not isinstance(data, dict):
return None
if "sessionId" not in data or "messages" not in data:
return None
messages_list = data.get("messages", [])
if not isinstance(messages_list, list):
return None

messages = []
for item in messages_list:
if not isinstance(item, dict):
continue
msg_type = item.get("type", "")
content = item.get("content", "")
# Gemini user content is [{"text": "..."}] (no "type" key);
# assistant content is a plain string.
if isinstance(content, str):
text = content.strip()
elif isinstance(content, list):
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and "text" in block:
parts.append(block["text"])
text = " ".join(parts).strip()
else:
text = ""
if msg_type == "user" and text:
messages.append(("user", text))
elif msg_type == "gemini" and text:
messages.append(("assistant", text))

if len(messages) >= 2:
return _messages_to_transcript(messages)
return None


def _try_slack_json(data) -> Optional[str]:
"""
Slack channel export: [{"type": "message", "user": "...", "text": "..."}]
Expand Down