context-forge-cli 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/PKG-INFO +7 -6
  2. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/README.md +6 -5
  3. context_forge_cli-0.2.0/contextforge/adapters/gemini.py +203 -0
  4. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/registry.py +2 -0
  5. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/pyproject.toml +1 -1
  6. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/.contextforge.toml +0 -0
  7. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/.github/workflows/publish.yml +0 -0
  8. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/.gitignore +0 -0
  9. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/.python-version +0 -0
  10. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/AGENTS.md +0 -0
  11. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/PLAN.md +0 -0
  12. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/__init__.py +0 -0
  13. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/__init__.py +0 -0
  14. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/altimate_code.py +0 -0
  15. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/base.py +0 -0
  16. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/claude_code.py +0 -0
  17. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/claude_desktop.py +0 -0
  18. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/adapters/codex.py +0 -0
  19. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/cli.py +0 -0
  20. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/__init__.py +0 -0
  21. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/analytics.py +0 -0
  22. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/compactor.py +0 -0
  23. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/db.py +0 -0
  24. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/injector.py +0 -0
  25. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/scanner.py +0 -0
  26. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/summarizer.py +0 -0
  27. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/core/token_analyzer.py +0 -0
  28. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/models/__init__.py +0 -0
  29. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/models/config.py +0 -0
  30. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/models/session.py +0 -0
  31. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/__init__.py +0 -0
  32. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/app.py +0 -0
  33. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/styles.tcss +0 -0
  34. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/__init__.py +0 -0
  35. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/session_detail.py +0 -0
  36. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/session_table.py +0 -0
  37. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/stats_panel.py +0 -0
  38. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/status_bar.py +0 -0
  39. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/tokens_panel.py +0 -0
  40. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/tui/widgets/transfer_panel.py +0 -0
  41. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/utils/__init__.py +0 -0
  42. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/utils/display.py +0 -0
  43. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/contextforge/utils/tokens.py +0 -0
  44. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/__init__.py +0 -0
  45. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/adapters/__init__.py +0 -0
  46. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/adapters/test_claude_code.py +0 -0
  47. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/core/__init__.py +0 -0
  48. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/core/test_compactor.py +0 -0
  49. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/core/test_db.py +0 -0
  50. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/core/test_injector.py +0 -0
  51. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/core/test_summarizer.py +0 -0
  52. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/core/test_token_analyzer.py +0 -0
  53. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/tests/fixtures/claude_session_sample.jsonl +0 -0
  54. {context_forge_cli-0.1.0 → context_forge_cli-0.2.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: context-forge-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Session manager and context bridge for agentic CLI tools
5
5
  Project-URL: Homepage, https://github.com/emmver/contextforge
6
6
  Project-URL: Repository, https://github.com/emmver/contextforge
@@ -30,14 +30,14 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  **Session manager and context bridge for agentic CLI tools.**
32
32
 
33
- ContextForge (`cf`) tracks sessions from Claude Code, Codex, altimate-code, and other
33
+ ContextForge (`cf`) tracks sessions from Claude Code, Claude Desktop, Codex, Gemini, altimate-code, and other
34
34
  agentic CLI tools. It generates plain-English summaries of each session and can compact
35
35
  and transfer context from one or more sessions into a new session — within the same tool
36
36
  or across different tools.
37
37
 
38
38
  ## What it does
39
39
 
40
- - **Discovers sessions** from Claude Code (`~/.claude/projects/`), Codex (`~/.codex/state_5.sqlite`), and altimate-code (`~/.local/share/altimate-code/`)
40
+ - **Discovers sessions** from Claude Code (`~/.claude/projects/`), Claude Desktop, Codex (`~/.codex/state_5.sqlite`), Gemini (`~/.gemini/sessions/`), and altimate-code (`~/.local/share/altimate-code/`)
41
41
  - **Generates summaries** of what each session accomplished (with optional Claude API integration)
42
42
  - **Compacts context** intelligently, reducing multi-turn conversations to a token-efficient ContextBundle
43
43
  - **Transfers context** across tools and sessions (Claude Code → Codex, Codex → altimate-code, etc.)
@@ -47,7 +47,7 @@ or across different tools.
47
47
 
48
48
  - **Python 3.13+** (ContextForge requires Python 3.13 or later)
49
49
  - **uv** or **pipx** (for installation as a tool)
50
- - At least one of: Claude Code, Codex, or altimate-code installed and with session history
50
+ - At least one of: Claude Code, Claude Desktop, Codex, Gemini, or altimate-code installed and with session history
51
51
 
52
52
  ## Install
53
53
 
@@ -167,6 +167,7 @@ The method is chosen automatically based on token count and whether a target ses
167
167
  | Claude Code (CLI) | `~/.claude/projects/` JSONL | `--system-prompt` / `--resume` |
168
168
  | Claude Desktop | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | `--system-prompt` |
169
169
  | Codex | `~/.codex/state_5.sqlite` | `resume` / `fork` |
170
+ | Gemini | `~/.gemini/sessions/` JSONL | `exec` / `resume` / `fork` |
170
171
  | altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
171
172
 
172
173
  ## Session Summaries
@@ -261,7 +262,7 @@ A full-screen Textual dashboard with live session browsing, filtering, and analy
261
262
  │ │ Worked on UTASTAR ensemble │
262
263
  │ │ pruning framework... │
263
264
  └──────────────────────────┴─────────────────────────────────┘
264
- Sessions — CC:12 │ Codex:4 │ Alt:3 │ Total:19 │ 129M tok 14:32:01
265
+ Sessions — CC:12 │ Codex:4 │ Gemini:2 │ Alt:3 │ Total:21 │ 129M tok 14:32:01
265
266
  ```
266
267
 
267
268
  ### Key bindings
@@ -298,7 +299,7 @@ Press `/` to open the filter bar above the session list:
298
299
  ### Transfer modal (`t`)
299
300
 
300
301
  Press `t` on any session to open the transfer panel. Choose:
301
- - **Target tool** — Claude Code, Codex, or altimate-code
302
+ - **Target tool** — Claude Code, Claude Desktop, Codex, Gemini, or altimate-code
302
303
  - **Strategy** — `summary_only`, `key_messages`, or `full_recent`
303
304
  - **Preview** — shows the exact shell command (no side effects)
304
305
  - **Execute** — builds the bundle and launches the target tool
@@ -2,14 +2,14 @@
2
2
 
3
3
  **Session manager and context bridge for agentic CLI tools.**
4
4
 
5
- ContextForge (`cf`) tracks sessions from Claude Code, Codex, altimate-code, and other
5
+ ContextForge (`cf`) tracks sessions from Claude Code, Claude Desktop, Codex, Gemini, altimate-code, and other
6
6
  agentic CLI tools. It generates plain-English summaries of each session and can compact
7
7
  and transfer context from one or more sessions into a new session — within the same tool
8
8
  or across different tools.
9
9
 
10
10
  ## What it does
11
11
 
12
- - **Discovers sessions** from Claude Code (`~/.claude/projects/`), Codex (`~/.codex/state_5.sqlite`), and altimate-code (`~/.local/share/altimate-code/`)
12
+ - **Discovers sessions** from Claude Code (`~/.claude/projects/`), Claude Desktop, Codex (`~/.codex/state_5.sqlite`), Gemini (`~/.gemini/sessions/`), and altimate-code (`~/.local/share/altimate-code/`)
13
13
  - **Generates summaries** of what each session accomplished (with optional Claude API integration)
14
14
  - **Compacts context** intelligently, reducing multi-turn conversations to a token-efficient ContextBundle
15
15
  - **Transfers context** across tools and sessions (Claude Code → Codex, Codex → altimate-code, etc.)
@@ -19,7 +19,7 @@ or across different tools.
19
19
 
20
20
  - **Python 3.13+** (ContextForge requires Python 3.13 or later)
21
21
  - **uv** or **pipx** (for installation as a tool)
22
- - At least one of: Claude Code, Codex, or altimate-code installed and with session history
22
+ - At least one of: Claude Code, Claude Desktop, Codex, Gemini, or altimate-code installed and with session history
23
23
 
24
24
  ## Install
25
25
 
@@ -139,6 +139,7 @@ The method is chosen automatically based on token count and whether a target ses
139
139
  | Claude Code (CLI) | `~/.claude/projects/` JSONL | `--system-prompt` / `--resume` |
140
140
  | Claude Desktop | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | `--system-prompt` |
141
141
  | Codex | `~/.codex/state_5.sqlite` | `resume` / `fork` |
142
+ | Gemini | `~/.gemini/sessions/` JSONL | `exec` / `resume` / `fork` |
142
143
  | altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
143
144
 
144
145
  ## Session Summaries
@@ -233,7 +234,7 @@ A full-screen Textual dashboard with live session browsing, filtering, and analy
233
234
  │ │ Worked on UTASTAR ensemble │
234
235
  │ │ pruning framework... │
235
236
  └──────────────────────────┴─────────────────────────────────┘
236
- Sessions — CC:12 │ Codex:4 │ Alt:3 │ Total:19 │ 129M tok 14:32:01
237
+ Sessions — CC:12 │ Codex:4 │ Gemini:2 │ Alt:3 │ Total:21 │ 129M tok 14:32:01
237
238
  ```
238
239
 
239
240
  ### Key bindings
@@ -270,7 +271,7 @@ Press `/` to open the filter bar above the session list:
270
271
  ### Transfer modal (`t`)
271
272
 
272
273
  Press `t` on any session to open the transfer panel. Choose:
273
- - **Target tool** — Claude Code, Codex, or altimate-code
274
+ - **Target tool** — Claude Code, Claude Desktop, Codex, Gemini, or altimate-code
274
275
  - **Strategy** — `summary_only`, `key_messages`, or `full_recent`
275
276
  - **Preview** — shows the exact shell command (no side effects)
276
277
  - **Execute** — builds the bundle and launches the target tool
@@ -0,0 +1,203 @@
1
+ """Adapter for Google Gemini CLI sessions."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import shlex
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from contextforge.adapters.base import ToolAdapter
10
+ from contextforge.models.session import Message, Session
11
+
12
+ _GEMINI_HISTORY_PATH = Path.home() / ".gemini" / "history.jsonl"
13
+ _GEMINI_SESSIONS_DIR = Path.home() / ".gemini" / "sessions"
14
+
15
+
16
+ class GeminiAdapter(ToolAdapter):
17
+ tool_name = "gemini"
18
+ default_paths = [_GEMINI_HISTORY_PATH, _GEMINI_SESSIONS_DIR]
19
+
20
+ def discover_sessions(self) -> list[Session]:
21
+ """Discover Gemini sessions from history and sessions directory."""
22
+ sessions: list[Session] = []
23
+
24
+ # Try to load from sessions directory first
25
+ if _GEMINI_SESSIONS_DIR.exists():
26
+ sessions = self._scan_sessions_dir()
27
+
28
+ # Fall back to history.jsonl if no sessions directory
29
+ if not sessions and _GEMINI_HISTORY_PATH.exists():
30
+ sessions = self._from_history()
31
+
32
+ return sessions
33
+
34
+ def _scan_sessions_dir(self) -> list[Session]:
35
+ """Scan the Gemini sessions directory for session files."""
36
+ sessions: list[Session] = []
37
+
38
+ if not _GEMINI_SESSIONS_DIR.exists():
39
+ return sessions
40
+
41
+ for session_file in sorted(_GEMINI_SESSIONS_DIR.glob("*.jsonl")):
42
+ session_id = session_file.stem
43
+ mtime = session_file.stat().st_mtime
44
+ dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
45
+
46
+ # Try to extract title from first few lines
47
+ title = None
48
+ try:
49
+ with session_file.open() as f:
50
+ for line in f:
51
+ try:
52
+ entry = json.loads(line)
53
+ if entry.get("type") == "user" and not title:
54
+ content = entry.get("message", {}).get("content", "")
55
+ if isinstance(content, str):
56
+ title = content[:100] # First 100 chars as title
57
+ break
58
+ except json.JSONDecodeError:
59
+ pass
60
+ except Exception:
61
+ pass
62
+
63
+ sessions.append(
64
+ Session(
65
+ id=session_id,
66
+ tool=self.tool_name,
67
+ title=title,
68
+ cwd=None,
69
+ created_at=dt,
70
+ updated_at=dt,
71
+ raw_path=str(session_file),
72
+ status="unknown",
73
+ )
74
+ )
75
+
76
+ return sessions
77
+
78
+ def _from_history(self) -> list[Session]:
79
+ """Load sessions from history.jsonl file."""
80
+ sessions: list[Session] = []
81
+ seen: set[str] = set()
82
+
83
+ if not _GEMINI_HISTORY_PATH.exists():
84
+ return sessions
85
+
86
+ with _GEMINI_HISTORY_PATH.open() as f:
87
+ for line in f:
88
+ line = line.strip()
89
+ if not line:
90
+ continue
91
+ try:
92
+ entry = json.loads(line)
93
+ except json.JSONDecodeError:
94
+ continue
95
+
96
+ session_id = entry.get("session_id") or entry.get("sessionId")
97
+ if not session_id or session_id in seen:
98
+ continue
99
+ seen.add(session_id)
100
+
101
+ title = entry.get("title") or entry.get("display")
102
+ ts = entry.get("timestamp")
103
+
104
+ try:
105
+ dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
106
+ except (ValueError, AttributeError, TypeError):
107
+ dt = datetime.now(timezone.utc)
108
+
109
+ sessions.append(
110
+ Session(
111
+ id=session_id,
112
+ tool=self.tool_name,
113
+ title=title,
114
+ cwd=None,
115
+ created_at=dt,
116
+ updated_at=dt,
117
+ raw_path=None,
118
+ status="unknown",
119
+ )
120
+ )
121
+
122
+ return sessions
123
+
124
+ def load_messages(self, session_id: str) -> list[Message]:
125
+ """Load messages for a given session ID."""
126
+ # Try to find session file
127
+ if _GEMINI_SESSIONS_DIR.exists():
128
+ session_file = _GEMINI_SESSIONS_DIR / f"{session_id}.jsonl"
129
+ if session_file.exists():
130
+ return self._parse_session_file(session_file)
131
+
132
+ return []
133
+
134
+ def _parse_session_file(self, path: Path) -> list[Message]:
135
+ """Parse a Gemini session JSONL file."""
136
+ messages: list[Message] = []
137
+
138
+ try:
139
+ with path.open() as f:
140
+ for line in f:
141
+ line = line.strip()
142
+ if not line:
143
+ continue
144
+ try:
145
+ entry = json.loads(line)
146
+ except json.JSONDecodeError:
147
+ continue
148
+
149
+ entry_type = entry.get("type", "")
150
+ if entry_type not in ("user", "assistant"):
151
+ continue
152
+
153
+ msg_data = entry.get("message", {})
154
+ content = msg_data.get("content", "")
155
+
156
+ if not content:
157
+ continue
158
+
159
+ # Handle both string and list content formats
160
+ if isinstance(content, list):
161
+ content = " ".join(str(item) for item in content if item)
162
+
163
+ ts_str = entry.get("timestamp")
164
+ ts = None
165
+ if ts_str:
166
+ try:
167
+ ts = datetime.fromisoformat(str(ts_str).replace("Z", "+00:00"))
168
+ except (ValueError, AttributeError, TypeError):
169
+ pass
170
+
171
+ messages.append(
172
+ Message(
173
+ role="user" if entry_type == "user" else "assistant",
174
+ content=str(content),
175
+ timestamp=ts,
176
+ )
177
+ )
178
+ except Exception:
179
+ pass
180
+
181
+ return messages
182
+
183
+ def build_inject_command(
184
+ self,
185
+ context: str,
186
+ target_session_id: str | None = None,
187
+ cwd: str | None = None,
188
+ method: str = "system_prompt",
189
+ ) -> str:
190
+ """Build a shell command to start/resume a Gemini session with context."""
191
+ safe_ctx = shlex.quote(context)
192
+
193
+ if method == "resume" and target_session_id:
194
+ return f"gemini resume {target_session_id} {safe_ctx}"
195
+
196
+ if method == "fork" and target_session_id:
197
+ return f"gemini fork {target_session_id} {safe_ctx}"
198
+
199
+ # Default: new session
200
+ cmd = f"gemini exec {safe_ctx}"
201
+ if cwd:
202
+ cmd = f"cd {shlex.quote(cwd)} && {cmd}"
203
+ return cmd
@@ -6,12 +6,14 @@ from contextforge.adapters.base import ToolAdapter
6
6
  from contextforge.adapters.claude_code import ClaudeCodeAdapter
7
7
  from contextforge.adapters.claude_desktop import ClaudeDesktopAdapter
8
8
  from contextforge.adapters.codex import CodexAdapter
9
+ from contextforge.adapters.gemini import GeminiAdapter
9
10
 
10
11
  ADAPTERS: dict[str, type[ToolAdapter]] = {
11
12
  "claude_code": ClaudeCodeAdapter,
12
13
  "claude_desktop": ClaudeDesktopAdapter,
13
14
  "codex": CodexAdapter,
14
15
  "altimate_code": AltimateCodeAdapter,
16
+ "gemini": GeminiAdapter,
15
17
  }
16
18
 
17
19
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "context-forge-cli"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Session manager and context bridge for agentic CLI tools"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }