context-forge-cli 0.2.0__tar.gz → 0.2.1__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.
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/PKG-INFO +3 -3
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/README.md +2 -2
- context_forge_cli-0.2.1/contextforge/adapters/gemini.py +188 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/pyproject.toml +1 -1
- context_forge_cli-0.2.0/contextforge/adapters/gemini.py +0 -203
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/.contextforge.toml +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/.github/workflows/publish.yml +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/.gitignore +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/.python-version +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/AGENTS.md +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/PLAN.md +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/altimate_code.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/base.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/claude_code.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/claude_desktop.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/codex.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/adapters/registry.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/cli.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/analytics.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/compactor.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/db.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/injector.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/scanner.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/summarizer.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/core/token_analyzer.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/models/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/models/config.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/models/session.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/app.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/styles.tcss +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/session_detail.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/session_table.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/stats_panel.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/status_bar.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/tokens_panel.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/transfer_panel.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/utils/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/utils/display.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/utils/tokens.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/adapters/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/adapters/test_claude_code.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/core/__init__.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/core/test_compactor.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/core/test_db.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/core/test_injector.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/core/test_summarizer.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/core/test_token_analyzer.py +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/fixtures/claude_session_sample.jsonl +0 -0
- {context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: context-forge-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -37,7 +37,7 @@ or across different tools.
|
|
|
37
37
|
|
|
38
38
|
## What it does
|
|
39
39
|
|
|
40
|
-
- **Discovers sessions** from Claude Code (`~/.claude/projects/`), Claude Desktop, Codex (`~/.codex/state_5.sqlite`), Gemini (`~/.gemini/
|
|
40
|
+
- **Discovers sessions** from Claude Code (`~/.claude/projects/`), Claude Desktop, Codex (`~/.codex/state_5.sqlite`), Gemini (`~/.gemini/tmp/<project_hash>/chats/`), 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.)
|
|
@@ -167,7 +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/
|
|
170
|
+
| Gemini | `~/.gemini/tmp/<project_hash>/chats/` JSON | New session / `/resume` |
|
|
171
171
|
| altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
|
|
172
172
|
|
|
173
173
|
## Session Summaries
|
|
@@ -9,7 +9,7 @@ or across different tools.
|
|
|
9
9
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
12
|
-
- **Discovers sessions** from Claude Code (`~/.claude/projects/`), Claude Desktop, Codex (`~/.codex/state_5.sqlite`), Gemini (`~/.gemini/
|
|
12
|
+
- **Discovers sessions** from Claude Code (`~/.claude/projects/`), Claude Desktop, Codex (`~/.codex/state_5.sqlite`), Gemini (`~/.gemini/tmp/<project_hash>/chats/`), 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.)
|
|
@@ -139,7 +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/
|
|
142
|
+
| Gemini | `~/.gemini/tmp/<project_hash>/chats/` JSON | New session / `/resume` |
|
|
143
143
|
| altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
|
|
144
144
|
|
|
145
145
|
## Session Summaries
|
|
@@ -0,0 +1,188 @@
|
|
|
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_TMP_BASE = Path.home() / ".gemini" / "tmp"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GeminiAdapter(ToolAdapter):
|
|
16
|
+
tool_name = "gemini"
|
|
17
|
+
default_paths = [_GEMINI_TMP_BASE]
|
|
18
|
+
|
|
19
|
+
def discover_sessions(self) -> list[Session]:
|
|
20
|
+
"""Discover Gemini sessions from ~/.gemini/tmp/<project_hash>/chats/."""
|
|
21
|
+
sessions: list[Session] = []
|
|
22
|
+
|
|
23
|
+
if not _GEMINI_TMP_BASE.exists():
|
|
24
|
+
return sessions
|
|
25
|
+
|
|
26
|
+
# Scan all project hash directories
|
|
27
|
+
for project_dir in _GEMINI_TMP_BASE.iterdir():
|
|
28
|
+
if not project_dir.is_dir():
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
chats_dir = project_dir / "chats"
|
|
32
|
+
if not chats_dir.exists():
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# Scan for session JSON files in format: session-<timestamp>-<hash>.json
|
|
36
|
+
for session_file in sorted(chats_dir.glob("session-*.json")):
|
|
37
|
+
try:
|
|
38
|
+
with session_file.open() as f:
|
|
39
|
+
data = json.load(f)
|
|
40
|
+
except (json.JSONDecodeError, OSError):
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
session_id = data.get("sessionId")
|
|
44
|
+
if not session_id:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# Extract timestamps
|
|
48
|
+
start_time_str = data.get("startTime")
|
|
49
|
+
last_updated_str = data.get("lastUpdated")
|
|
50
|
+
|
|
51
|
+
created_at = None
|
|
52
|
+
updated_at = None
|
|
53
|
+
|
|
54
|
+
if start_time_str:
|
|
55
|
+
try:
|
|
56
|
+
created_at = datetime.fromisoformat(
|
|
57
|
+
start_time_str.replace("Z", "+00:00")
|
|
58
|
+
)
|
|
59
|
+
except (ValueError, AttributeError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
if last_updated_str:
|
|
63
|
+
try:
|
|
64
|
+
updated_at = datetime.fromisoformat(
|
|
65
|
+
last_updated_str.replace("Z", "+00:00")
|
|
66
|
+
)
|
|
67
|
+
except (ValueError, AttributeError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
if created_at is None:
|
|
71
|
+
created_at = datetime.now(timezone.utc)
|
|
72
|
+
if updated_at is None:
|
|
73
|
+
updated_at = created_at
|
|
74
|
+
|
|
75
|
+
# Extract title from first user message
|
|
76
|
+
title = None
|
|
77
|
+
messages = data.get("messages", [])
|
|
78
|
+
for msg in messages:
|
|
79
|
+
if msg.get("type") == "user":
|
|
80
|
+
content = msg.get("content", [])
|
|
81
|
+
if isinstance(content, list) and content:
|
|
82
|
+
text = content[0].get("text", "")
|
|
83
|
+
if text:
|
|
84
|
+
title = text[:100]
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
sessions.append(
|
|
88
|
+
Session(
|
|
89
|
+
id=session_id,
|
|
90
|
+
tool=self.tool_name,
|
|
91
|
+
title=title,
|
|
92
|
+
cwd=None,
|
|
93
|
+
created_at=created_at,
|
|
94
|
+
updated_at=updated_at,
|
|
95
|
+
raw_path=str(session_file),
|
|
96
|
+
status="unknown",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return sessions
|
|
101
|
+
|
|
102
|
+
def load_messages(self, session_id: str) -> list[Message]:
|
|
103
|
+
"""Load messages for a given session ID by scanning all projects."""
|
|
104
|
+
if not _GEMINI_TMP_BASE.exists():
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
# Brute-force search across all projects to find the session
|
|
108
|
+
for project_dir in _GEMINI_TMP_BASE.iterdir():
|
|
109
|
+
if not project_dir.is_dir():
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
chats_dir = project_dir / "chats"
|
|
113
|
+
if not chats_dir.exists():
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
for session_file in chats_dir.glob("session-*.json"):
|
|
117
|
+
try:
|
|
118
|
+
with session_file.open() as f:
|
|
119
|
+
data = json.load(f)
|
|
120
|
+
except (json.JSONDecodeError, OSError):
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if data.get("sessionId") == session_id:
|
|
124
|
+
return self._parse_session_data(data)
|
|
125
|
+
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
def _parse_session_data(self, data: dict) -> list[Message]:
|
|
129
|
+
"""Parse Gemini session JSON object into Message list."""
|
|
130
|
+
messages: list[Message] = []
|
|
131
|
+
|
|
132
|
+
for msg in data.get("messages", []):
|
|
133
|
+
msg_type = msg.get("type", "")
|
|
134
|
+
if msg_type not in ("user", "gemini"):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Extract content
|
|
138
|
+
content_raw = msg.get("content", "")
|
|
139
|
+
if isinstance(content_raw, list):
|
|
140
|
+
# User messages: content is list of {text: ...}
|
|
141
|
+
parts = []
|
|
142
|
+
for item in content_raw:
|
|
143
|
+
if isinstance(item, dict) and "text" in item:
|
|
144
|
+
parts.append(item["text"])
|
|
145
|
+
content = " ".join(parts)
|
|
146
|
+
else:
|
|
147
|
+
# Gemini messages: content is a string
|
|
148
|
+
content = str(content_raw)
|
|
149
|
+
|
|
150
|
+
if not content or not content.strip():
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
ts_str = msg.get("timestamp")
|
|
154
|
+
ts = None
|
|
155
|
+
if ts_str:
|
|
156
|
+
try:
|
|
157
|
+
ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
158
|
+
except (ValueError, AttributeError):
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
messages.append(
|
|
162
|
+
Message(
|
|
163
|
+
role="user" if msg_type == "user" else "assistant",
|
|
164
|
+
content=content,
|
|
165
|
+
timestamp=ts,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return messages
|
|
170
|
+
|
|
171
|
+
def build_inject_command(
|
|
172
|
+
self,
|
|
173
|
+
context: str,
|
|
174
|
+
target_session_id: str | None = None,
|
|
175
|
+
cwd: str | None = None,
|
|
176
|
+
method: str = "system_prompt",
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Build a shell command to start/resume a Gemini session with context."""
|
|
179
|
+
safe_ctx = shlex.quote(context)
|
|
180
|
+
|
|
181
|
+
if method == "resume" and target_session_id:
|
|
182
|
+
return f"gemini /resume {target_session_id}"
|
|
183
|
+
|
|
184
|
+
# Default: new session with context injected via system prompt
|
|
185
|
+
cmd = f"gemini {safe_ctx}"
|
|
186
|
+
if cwd:
|
|
187
|
+
cmd = f"cd {shlex.quote(cwd)} && {cmd}"
|
|
188
|
+
return cmd
|
|
@@ -1,203 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/session_detail.py
RENAMED
|
File without changes
|
{context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/session_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/tokens_panel.py
RENAMED
|
File without changes
|
{context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/contextforge/tui/widgets/transfer_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.2.0 → context_forge_cli-0.2.1}/tests/fixtures/claude_session_sample.jsonl
RENAMED
|
File without changes
|
|
File without changes
|