context-forge-cli 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.dist-info → context_forge_cli-0.2.2.dist-info}/METADATA +3 -3
- {context_forge_cli-0.2.0.dist-info → context_forge_cli-0.2.2.dist-info}/RECORD +9 -9
- contextforge/adapters/altimate_code.py +2 -1
- contextforge/adapters/claude_code.py +5 -28
- contextforge/adapters/claude_desktop.py +13 -4
- contextforge/adapters/codex.py +24 -2
- contextforge/adapters/gemini.py +135 -137
- {context_forge_cli-0.2.0.dist-info → context_forge_cli-0.2.2.dist-info}/WHEEL +0 -0
- {context_forge_cli-0.2.0.dist-info → context_forge_cli-0.2.2.dist-info}/entry_points.txt +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.2
|
|
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
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
contextforge/__init__.py,sha256=L5NnW7jpnDQrAhnqTWSzW-bk2Bp8TXpFK1-SmQXI2QE,104
|
|
2
2
|
contextforge/cli.py,sha256=OIHjV_pL4fKCvnn-2bQwza8NOUplptSfASPg4yzQmjA,18685
|
|
3
3
|
contextforge/adapters/__init__.py,sha256=HGIFdC_tNpX_QOPrePwU5cEVBhp3Hsh6Sihwb23HdTk,171
|
|
4
|
-
contextforge/adapters/altimate_code.py,sha256=
|
|
4
|
+
contextforge/adapters/altimate_code.py,sha256=_buMKoFP8YSqtElPHwmsJicse85OydwoX1t94JC8BDI,6050
|
|
5
5
|
contextforge/adapters/base.py,sha256=q3_pn0XoDV_r2GkuSzuRof0SSqynHqiaCAe4piq3tGs,1355
|
|
6
|
-
contextforge/adapters/claude_code.py,sha256=
|
|
7
|
-
contextforge/adapters/claude_desktop.py,sha256=
|
|
8
|
-
contextforge/adapters/codex.py,sha256=
|
|
9
|
-
contextforge/adapters/gemini.py,sha256=
|
|
6
|
+
contextforge/adapters/claude_code.py,sha256=Oes23vjRRv7Js0BaZvScZNDpyaItlwPEWKwsyL6Zj60,14453
|
|
7
|
+
contextforge/adapters/claude_desktop.py,sha256=P0fI3Tg00bX8MifV09I1iRPqk-6m5w1kpllCW7Bc2hg,12238
|
|
8
|
+
contextforge/adapters/codex.py,sha256=yj0Sdzo8yrkQHSqfCxq-NxXuaiaUOSlnZOTIAOQw-gU,8776
|
|
9
|
+
contextforge/adapters/gemini.py,sha256=757prqt5_AILxFm53CEfhlqjFKlxVs-YmLBImt8Aj1M,6844
|
|
10
10
|
contextforge/adapters/registry.py,sha256=kbasRJE4WBJ8qw7VGENgjp8NDS3jyIHo8wpGxjk9srs,1177
|
|
11
11
|
contextforge/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
contextforge/core/analytics.py,sha256=qffm7oqfmosU6XPUftbf68AOvDrylrxBR4rJyLhvoXU,7056
|
|
@@ -32,7 +32,7 @@ contextforge/tui/widgets/transfer_panel.py,sha256=Qrgkt1-MpzRHWuwNyu2ErM26IRl0ya
|
|
|
32
32
|
contextforge/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
33
|
contextforge/utils/display.py,sha256=bagZgpZ6WGki2THNHA8lfg9ITqM8W0q_M6Os-pgwFHk,3111
|
|
34
34
|
contextforge/utils/tokens.py,sha256=UVGz-vvmCIxMgnRPn1T53v2ZdKMmxKhoJcc1qbmjpDQ,414
|
|
35
|
-
context_forge_cli-0.2.
|
|
36
|
-
context_forge_cli-0.2.
|
|
37
|
-
context_forge_cli-0.2.
|
|
38
|
-
context_forge_cli-0.2.
|
|
35
|
+
context_forge_cli-0.2.2.dist-info/METADATA,sha256=PixJa-FTRNZSUIxXHuah7o_1Z2QlzaSNkUqGyoe-5-g,12831
|
|
36
|
+
context_forge_cli-0.2.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
37
|
+
context_forge_cli-0.2.2.dist-info/entry_points.txt,sha256=nPN0PWb85KCPXqFp4opmBsNejdCHyl7rK_cREsu4r7A,44
|
|
38
|
+
context_forge_cli-0.2.2.dist-info/RECORD,,
|
|
@@ -117,6 +117,7 @@ class AltimateCodeAdapter(ToolAdapter):
|
|
|
117
117
|
role=current_role,
|
|
118
118
|
content=content,
|
|
119
119
|
timestamp=current_ts,
|
|
120
|
+
token_count=_count_tokens(content),
|
|
120
121
|
)
|
|
121
122
|
)
|
|
122
123
|
current_role = None
|
|
@@ -157,7 +158,7 @@ class AltimateCodeAdapter(ToolAdapter):
|
|
|
157
158
|
messages = self.load_messages(session_id)
|
|
158
159
|
total = 0
|
|
159
160
|
for msg in messages:
|
|
160
|
-
total += _count_tokens(msg.content)
|
|
161
|
+
total += msg.token_count or _count_tokens(msg.content)
|
|
161
162
|
return total
|
|
162
163
|
|
|
163
164
|
def build_inject_command(
|
|
@@ -333,6 +333,7 @@ class ClaudeCodeAdapter(ToolAdapter):
|
|
|
333
333
|
role="user" if role == "user" else "assistant",
|
|
334
334
|
content=content,
|
|
335
335
|
timestamp=ts,
|
|
336
|
+
token_count=_count_tokens(content),
|
|
336
337
|
)
|
|
337
338
|
)
|
|
338
339
|
|
|
@@ -340,35 +341,11 @@ class ClaudeCodeAdapter(ToolAdapter):
|
|
|
340
341
|
|
|
341
342
|
def _count_session_tokens(self, jsonl_path: Path) -> int:
|
|
342
343
|
"""Count total tokens in a session JSONL file."""
|
|
344
|
+
session_id = jsonl_path.stem
|
|
345
|
+
messages = self.load_messages(session_id)
|
|
343
346
|
total = 0
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
for line in f:
|
|
347
|
-
line = line.strip()
|
|
348
|
-
if not line:
|
|
349
|
-
continue
|
|
350
|
-
try:
|
|
351
|
-
entry = json.loads(line)
|
|
352
|
-
except json.JSONDecodeError:
|
|
353
|
-
continue
|
|
354
|
-
|
|
355
|
-
entry_type = entry.get("type", "")
|
|
356
|
-
if entry_type not in ("user", "assistant"):
|
|
357
|
-
continue
|
|
358
|
-
|
|
359
|
-
msg_data = entry.get("message", {})
|
|
360
|
-
content_raw = msg_data.get("content", "")
|
|
361
|
-
|
|
362
|
-
# Skip tool-result-only arrays (not actual user/assistant content)
|
|
363
|
-
if isinstance(content_raw, list):
|
|
364
|
-
if not any(block.get("type") == "text" for block in content_raw if isinstance(block, dict)):
|
|
365
|
-
continue
|
|
366
|
-
|
|
367
|
-
content = _parse_content(content_raw)
|
|
368
|
-
if content:
|
|
369
|
-
total += _count_tokens(content)
|
|
370
|
-
except Exception:
|
|
371
|
-
pass
|
|
347
|
+
for msg in messages:
|
|
348
|
+
total += msg.token_count or _count_tokens(msg.content)
|
|
372
349
|
return total
|
|
373
350
|
|
|
374
351
|
def build_inject_command(
|
|
@@ -240,8 +240,12 @@ class ClaudeDesktopAdapter(ToolAdapter):
|
|
|
240
240
|
pass
|
|
241
241
|
|
|
242
242
|
messages.append(
|
|
243
|
-
Message(
|
|
244
|
-
|
|
243
|
+
Message(
|
|
244
|
+
role="user" if role == "user" else "assistant",
|
|
245
|
+
content=content,
|
|
246
|
+
timestamp=ts,
|
|
247
|
+
token_count=_count_tokens(content),
|
|
248
|
+
)
|
|
245
249
|
)
|
|
246
250
|
return messages
|
|
247
251
|
|
|
@@ -279,7 +283,12 @@ class ClaudeDesktopAdapter(ToolAdapter):
|
|
|
279
283
|
except (ValueError, AttributeError):
|
|
280
284
|
pass
|
|
281
285
|
|
|
282
|
-
messages.append(Message(
|
|
286
|
+
messages.append(Message(
|
|
287
|
+
role=role,
|
|
288
|
+
content=content,
|
|
289
|
+
timestamp=ts,
|
|
290
|
+
token_count=_count_tokens(content),
|
|
291
|
+
))
|
|
283
292
|
return messages
|
|
284
293
|
|
|
285
294
|
def _count_session_tokens(self, session_id: str) -> int:
|
|
@@ -287,7 +296,7 @@ class ClaudeDesktopAdapter(ToolAdapter):
|
|
|
287
296
|
messages = self.load_messages(session_id)
|
|
288
297
|
total = 0
|
|
289
298
|
for msg in messages:
|
|
290
|
-
total += _count_tokens(msg.content)
|
|
299
|
+
total += msg.token_count or _count_tokens(msg.content)
|
|
291
300
|
return total
|
|
292
301
|
|
|
293
302
|
def build_inject_command(
|
contextforge/adapters/codex.py
CHANGED
|
@@ -7,6 +7,8 @@ import sqlite3
|
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
import tiktoken
|
|
11
|
+
|
|
10
12
|
from contextforge.adapters.base import ToolAdapter
|
|
11
13
|
from contextforge.models.session import Message, Session
|
|
12
14
|
|
|
@@ -29,6 +31,16 @@ _CODEX_SESSIONS_DIR = Path.home() / ".codex" / "sessions"
|
|
|
29
31
|
_SESSION_INDEX = Path.home() / ".codex" / "session_index.jsonl"
|
|
30
32
|
|
|
31
33
|
|
|
34
|
+
def _count_tokens(text: str) -> int:
|
|
35
|
+
"""Count tokens in text using tiktoken for Claude models."""
|
|
36
|
+
try:
|
|
37
|
+
enc = tiktoken.encoding_for_model("claude-3-5-sonnet-20241022")
|
|
38
|
+
return len(enc.encode(text))
|
|
39
|
+
except Exception:
|
|
40
|
+
# Fallback: rough estimate (~4 chars per token)
|
|
41
|
+
return len(text) // 4
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
class CodexAdapter(ToolAdapter):
|
|
33
45
|
tool_name = "codex"
|
|
34
46
|
default_paths = [_CODEX_DB, _CODEX_SESSIONS_DIR]
|
|
@@ -193,11 +205,21 @@ class CodexAdapter(ToolAdapter):
|
|
|
193
205
|
if msg_type == "user_message":
|
|
194
206
|
text = payload.get("message", "")
|
|
195
207
|
if text:
|
|
196
|
-
|
|
208
|
+
text_str = str(text)
|
|
209
|
+
messages.append(Message(
|
|
210
|
+
role="user",
|
|
211
|
+
content=text_str,
|
|
212
|
+
token_count=_count_tokens(text_str),
|
|
213
|
+
))
|
|
197
214
|
elif msg_type == "agent_message":
|
|
198
215
|
text = payload.get("message", "")
|
|
199
216
|
if text:
|
|
200
|
-
|
|
217
|
+
text_str = str(text)
|
|
218
|
+
messages.append(Message(
|
|
219
|
+
role="assistant",
|
|
220
|
+
content=text_str,
|
|
221
|
+
token_count=_count_tokens(text_str),
|
|
222
|
+
))
|
|
201
223
|
|
|
202
224
|
return messages
|
|
203
225
|
|
contextforge/adapters/gemini.py
CHANGED
|
@@ -6,105 +6,95 @@ import shlex
|
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
import tiktoken
|
|
10
|
+
|
|
9
11
|
from contextforge.adapters.base import ToolAdapter
|
|
10
12
|
from contextforge.models.session import Message, Session
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
_GEMINI_TMP_BASE = Path.home() / ".gemini" / "tmp"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _count_tokens(text: str) -> int:
|
|
18
|
+
"""Count tokens in text using tiktoken for Claude models."""
|
|
19
|
+
try:
|
|
20
|
+
enc = tiktoken.encoding_for_model("claude-3-5-sonnet-20241022")
|
|
21
|
+
return len(enc.encode(text))
|
|
22
|
+
except Exception:
|
|
23
|
+
# Fallback: rough estimate (~4 chars per token)
|
|
24
|
+
return len(text) // 4
|
|
14
25
|
|
|
15
26
|
|
|
16
27
|
class GeminiAdapter(ToolAdapter):
|
|
17
28
|
tool_name = "gemini"
|
|
18
|
-
default_paths = [
|
|
29
|
+
default_paths = [_GEMINI_TMP_BASE]
|
|
19
30
|
|
|
20
31
|
def discover_sessions(self) -> list[Session]:
|
|
21
|
-
"""Discover Gemini sessions from
|
|
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."""
|
|
32
|
+
"""Discover Gemini sessions from ~/.gemini/tmp/<project_hash>/chats/."""
|
|
36
33
|
sessions: list[Session] = []
|
|
37
34
|
|
|
38
|
-
if not
|
|
35
|
+
if not _GEMINI_TMP_BASE.exists():
|
|
39
36
|
return sessions
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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()
|
|
38
|
+
# Scan all project hash directories
|
|
39
|
+
for project_dir in _GEMINI_TMP_BASE.iterdir():
|
|
40
|
+
if not project_dir.is_dir():
|
|
41
|
+
continue
|
|
82
42
|
|
|
83
|
-
|
|
84
|
-
|
|
43
|
+
chats_dir = project_dir / "chats"
|
|
44
|
+
if not chats_dir.exists():
|
|
45
|
+
continue
|
|
85
46
|
|
|
86
|
-
|
|
87
|
-
for
|
|
88
|
-
line = line.strip()
|
|
89
|
-
if not line:
|
|
90
|
-
continue
|
|
47
|
+
# Scan for session JSON files in format: session-<timestamp>-<hash>.json
|
|
48
|
+
for session_file in sorted(chats_dir.glob("session-*.json")):
|
|
91
49
|
try:
|
|
92
|
-
|
|
93
|
-
|
|
50
|
+
with session_file.open() as f:
|
|
51
|
+
data = json.load(f)
|
|
52
|
+
except (json.JSONDecodeError, OSError):
|
|
94
53
|
continue
|
|
95
54
|
|
|
96
|
-
session_id =
|
|
97
|
-
if not session_id
|
|
55
|
+
session_id = data.get("sessionId")
|
|
56
|
+
if not session_id:
|
|
98
57
|
continue
|
|
99
|
-
seen.add(session_id)
|
|
100
58
|
|
|
101
|
-
|
|
102
|
-
|
|
59
|
+
# Extract timestamps
|
|
60
|
+
start_time_str = data.get("startTime")
|
|
61
|
+
last_updated_str = data.get("lastUpdated")
|
|
103
62
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
63
|
+
created_at = None
|
|
64
|
+
updated_at = None
|
|
65
|
+
|
|
66
|
+
if start_time_str:
|
|
67
|
+
try:
|
|
68
|
+
created_at = datetime.fromisoformat(
|
|
69
|
+
start_time_str.replace("Z", "+00:00")
|
|
70
|
+
)
|
|
71
|
+
except (ValueError, AttributeError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
if last_updated_str:
|
|
75
|
+
try:
|
|
76
|
+
updated_at = datetime.fromisoformat(
|
|
77
|
+
last_updated_str.replace("Z", "+00:00")
|
|
78
|
+
)
|
|
79
|
+
except (ValueError, AttributeError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
if created_at is None:
|
|
83
|
+
created_at = datetime.now(timezone.utc)
|
|
84
|
+
if updated_at is None:
|
|
85
|
+
updated_at = created_at
|
|
86
|
+
|
|
87
|
+
# Extract title from first user message
|
|
88
|
+
title = None
|
|
89
|
+
messages = data.get("messages", [])
|
|
90
|
+
for msg in messages:
|
|
91
|
+
if msg.get("type") == "user":
|
|
92
|
+
content = msg.get("content", [])
|
|
93
|
+
if isinstance(content, list) and content:
|
|
94
|
+
text = content[0].get("text", "")
|
|
95
|
+
if text:
|
|
96
|
+
title = text[:100]
|
|
97
|
+
break
|
|
108
98
|
|
|
109
99
|
sessions.append(
|
|
110
100
|
Session(
|
|
@@ -112,9 +102,9 @@ class GeminiAdapter(ToolAdapter):
|
|
|
112
102
|
tool=self.tool_name,
|
|
113
103
|
title=title,
|
|
114
104
|
cwd=None,
|
|
115
|
-
created_at=
|
|
116
|
-
updated_at=
|
|
117
|
-
raw_path=
|
|
105
|
+
created_at=created_at,
|
|
106
|
+
updated_at=updated_at,
|
|
107
|
+
raw_path=str(session_file),
|
|
118
108
|
status="unknown",
|
|
119
109
|
)
|
|
120
110
|
)
|
|
@@ -122,61 +112,72 @@ class GeminiAdapter(ToolAdapter):
|
|
|
122
112
|
return sessions
|
|
123
113
|
|
|
124
114
|
def load_messages(self, session_id: str) -> list[Message]:
|
|
125
|
-
"""Load messages for a given session ID."""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
115
|
+
"""Load messages for a given session ID by scanning all projects."""
|
|
116
|
+
if not _GEMINI_TMP_BASE.exists():
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
# Brute-force search across all projects to find the session
|
|
120
|
+
for project_dir in _GEMINI_TMP_BASE.iterdir():
|
|
121
|
+
if not project_dir.is_dir():
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
chats_dir = project_dir / "chats"
|
|
125
|
+
if not chats_dir.exists():
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
for session_file in chats_dir.glob("session-*.json"):
|
|
129
|
+
try:
|
|
130
|
+
with session_file.open() as f:
|
|
131
|
+
data = json.load(f)
|
|
132
|
+
except (json.JSONDecodeError, OSError):
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if data.get("sessionId") == session_id:
|
|
136
|
+
return self._parse_session_data(data)
|
|
131
137
|
|
|
132
138
|
return []
|
|
133
139
|
|
|
134
|
-
def
|
|
135
|
-
"""Parse
|
|
140
|
+
def _parse_session_data(self, data: dict) -> list[Message]:
|
|
141
|
+
"""Parse Gemini session JSON object into Message list."""
|
|
136
142
|
messages: list[Message] = []
|
|
137
143
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
timestamp=ts,
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
except Exception:
|
|
179
|
-
pass
|
|
144
|
+
for msg in data.get("messages", []):
|
|
145
|
+
msg_type = msg.get("type", "")
|
|
146
|
+
if msg_type not in ("user", "gemini"):
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Extract content
|
|
150
|
+
content_raw = msg.get("content", "")
|
|
151
|
+
if isinstance(content_raw, list):
|
|
152
|
+
# User messages: content is list of {text: ...}
|
|
153
|
+
parts = []
|
|
154
|
+
for item in content_raw:
|
|
155
|
+
if isinstance(item, dict) and "text" in item:
|
|
156
|
+
parts.append(item["text"])
|
|
157
|
+
content = " ".join(parts)
|
|
158
|
+
else:
|
|
159
|
+
# Gemini messages: content is a string
|
|
160
|
+
content = str(content_raw)
|
|
161
|
+
|
|
162
|
+
if not content or not content.strip():
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
ts_str = msg.get("timestamp")
|
|
166
|
+
ts = None
|
|
167
|
+
if ts_str:
|
|
168
|
+
try:
|
|
169
|
+
ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
170
|
+
except (ValueError, AttributeError):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
messages.append(
|
|
174
|
+
Message(
|
|
175
|
+
role="user" if msg_type == "user" else "assistant",
|
|
176
|
+
content=content,
|
|
177
|
+
timestamp=ts,
|
|
178
|
+
token_count=_count_tokens(content),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
180
181
|
|
|
181
182
|
return messages
|
|
182
183
|
|
|
@@ -191,13 +192,10 @@ class GeminiAdapter(ToolAdapter):
|
|
|
191
192
|
safe_ctx = shlex.quote(context)
|
|
192
193
|
|
|
193
194
|
if method == "resume" and target_session_id:
|
|
194
|
-
return f"gemini resume {target_session_id}
|
|
195
|
-
|
|
196
|
-
if method == "fork" and target_session_id:
|
|
197
|
-
return f"gemini fork {target_session_id} {safe_ctx}"
|
|
195
|
+
return f"gemini /resume {target_session_id}"
|
|
198
196
|
|
|
199
|
-
# Default: new session
|
|
200
|
-
cmd = f"gemini
|
|
197
|
+
# Default: new session with context injected via system prompt
|
|
198
|
+
cmd = f"gemini {safe_ctx}"
|
|
201
199
|
if cwd:
|
|
202
200
|
cmd = f"cd {shlex.quote(cwd)} && {cmd}"
|
|
203
201
|
return cmd
|
|
File without changes
|
|
File without changes
|