context-forge-cli 0.4.10__tar.gz → 0.4.11__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.4.10 → context_forge_cli-0.4.11}/PKG-INFO +2 -1
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/PLAN.md +1 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/README.md +1 -0
- context_forge_cli-0.4.11/contextforge/adapters/antigravity.py +233 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/registry.py +2 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/session_detail.py +2 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/session_table.py +5 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/stats_panel.py +3 -1
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/status_bar.py +2 -1
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/tokens_panel.py +1 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/transfer_panel.py +1 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/pyproject.toml +1 -1
- context_forge_cli-0.4.11/tests/adapters/test_antigravity.py +195 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/uv.lock +1 -1
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/.contextforge.toml +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/.github/workflows/publish.yml +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/.gitignore +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/.python-version +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/AGENTS.md +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/altimate_code.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/base.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/claude_code.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/claude_desktop.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/codex.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/gemini.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/cli.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/analytics.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/compactor.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/db.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/injector.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/scanner.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/summarizer.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/core/token_analyzer.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/mcp_server.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/models/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/models/config.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/models/session.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/app.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/clipboard.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/styles.tcss +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/utils/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/utils/display.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/utils/tokens.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/adapters/test_claude_code.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/conftest.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/core/__init__.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/core/test_compactor.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/core/test_db.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/core/test_injector.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/core/test_summarizer.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/core/test_token_analyzer.py +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/fixtures/claude_session_sample.jsonl +0 -0
- {context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/tests/test_mcp_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: context-forge-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
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
|
|
@@ -169,6 +169,7 @@ The method is chosen automatically based on token count and whether a target ses
|
|
|
169
169
|
| Claude Desktop | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | `--system-prompt` |
|
|
170
170
|
| Codex | `~/.codex/state_5.sqlite` | `resume` / `fork` |
|
|
171
171
|
| Gemini | `~/.gemini/tmp/<project_hash>/chats/` JSON | New session / `/resume` |
|
|
172
|
+
| Antigravity | `~/.gemini/antigravity-cli/history.jsonl` | `-i` / `--conversation` |
|
|
172
173
|
| altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
|
|
173
174
|
|
|
174
175
|
## Session Summaries
|
|
@@ -87,6 +87,7 @@ Agents and contributors: update this file as you work.
|
|
|
87
87
|
- [ ] Install docs: `uv tool install contextforge`
|
|
88
88
|
- [ ] GitHub Actions CI: `uv run pytest --cov=contextforge`
|
|
89
89
|
- [ ] `open-claw` adapter (if/when CLI interface is documented)
|
|
90
|
+
- [x] Antigravity adapter (`adapters/antigravity.py`)
|
|
90
91
|
- [ ] Session search: `cf ls --search <query>`
|
|
91
92
|
- [ ] Session archiving: `cf archive <id>`
|
|
92
93
|
|
|
@@ -140,6 +140,7 @@ The method is chosen automatically based on token count and whether a target ses
|
|
|
140
140
|
| Claude Desktop | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | `--system-prompt` |
|
|
141
141
|
| Codex | `~/.codex/state_5.sqlite` | `resume` / `fork` |
|
|
142
142
|
| Gemini | `~/.gemini/tmp/<project_hash>/chats/` JSON | New session / `/resume` |
|
|
143
|
+
| Antigravity | `~/.gemini/antigravity-cli/history.jsonl` | `-i` / `--conversation` |
|
|
143
144
|
| altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
|
|
144
145
|
|
|
145
146
|
## Session Summaries
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Adapter for Google Antigravity 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
|
+
import tiktoken
|
|
10
|
+
|
|
11
|
+
from contextforge.adapters.base import ToolAdapter
|
|
12
|
+
from contextforge.models.session import Message, Session
|
|
13
|
+
|
|
14
|
+
_AGY_HISTORY = Path.home() / ".gemini" / "antigravity-cli" / "history.jsonl"
|
|
15
|
+
_AGY_CONVERSATIONS = Path.home() / ".gemini" / "antigravity-cli" / "conversations"
|
|
16
|
+
_AGY_ALT_HISTORY = Path.home() / ".gemini" / "antigravity" / "history.jsonl"
|
|
17
|
+
_AGY_ALT_CONVERSATIONS = Path.home() / ".gemini" / "antigravity" / "conversations"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _count_tokens(text: str) -> int:
|
|
21
|
+
"""Count tokens in text using tiktoken for Claude models."""
|
|
22
|
+
try:
|
|
23
|
+
enc = tiktoken.encoding_for_model("claude-3-5-sonnet-20241022")
|
|
24
|
+
return len(enc.encode(text))
|
|
25
|
+
except Exception:
|
|
26
|
+
# Fallback: rough estimate (~4 chars per token)
|
|
27
|
+
return len(text) // 4
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ms_to_dt(ts: int) -> datetime:
|
|
31
|
+
"""Convert a millisecond timestamp to UTC datetime."""
|
|
32
|
+
return datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AntigravityAdapter(ToolAdapter):
|
|
36
|
+
tool_name = "antigravity"
|
|
37
|
+
default_paths = [_AGY_HISTORY, _AGY_ALT_HISTORY]
|
|
38
|
+
|
|
39
|
+
def _history_path(self) -> Path | None:
|
|
40
|
+
"""Return the first existing history.jsonl path."""
|
|
41
|
+
for p in (_AGY_HISTORY, _AGY_ALT_HISTORY):
|
|
42
|
+
if p.exists():
|
|
43
|
+
return p
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def _conversations_dir(self) -> Path | None:
|
|
47
|
+
"""Return the conversations directory that matches the history path."""
|
|
48
|
+
history = self._history_path()
|
|
49
|
+
if history is not None:
|
|
50
|
+
conversations = history.parent / "conversations"
|
|
51
|
+
if conversations.exists():
|
|
52
|
+
return conversations
|
|
53
|
+
for p in (_AGY_CONVERSATIONS, _AGY_ALT_CONVERSATIONS):
|
|
54
|
+
if p.exists():
|
|
55
|
+
return p
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def discover_sessions(self) -> list[Session]:
|
|
59
|
+
"""Discover Antigravity sessions from history.jsonl.
|
|
60
|
+
|
|
61
|
+
history.jsonl contains one JSON object per user prompt. Prompts that
|
|
62
|
+
belong to the same saved conversation share a ``conversationId``; we
|
|
63
|
+
group those into a single ``Session``.
|
|
64
|
+
"""
|
|
65
|
+
sessions: list[Session] = []
|
|
66
|
+
history_path = self._history_path()
|
|
67
|
+
if history_path is None:
|
|
68
|
+
return sessions
|
|
69
|
+
|
|
70
|
+
# conversationId -> list of history entries
|
|
71
|
+
groups: dict[str, list[dict]] = {}
|
|
72
|
+
|
|
73
|
+
with history_path.open() as f:
|
|
74
|
+
for line in f:
|
|
75
|
+
line = line.strip()
|
|
76
|
+
if not line:
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
entry = json.loads(line)
|
|
80
|
+
except json.JSONDecodeError:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
conversation_id = entry.get("conversationId")
|
|
84
|
+
if not conversation_id:
|
|
85
|
+
# Skip un-persisted one-off prompts.
|
|
86
|
+
continue
|
|
87
|
+
groups.setdefault(conversation_id, []).append(entry)
|
|
88
|
+
|
|
89
|
+
for conversation_id, entries in groups.items():
|
|
90
|
+
if not entries:
|
|
91
|
+
continue
|
|
92
|
+
# Use earliest timestamp as created_at, latest as updated_at.
|
|
93
|
+
timestamps = [e.get("timestamp", 0) for e in entries if e.get("timestamp")]
|
|
94
|
+
created_ts = min(timestamps) if timestamps else 0
|
|
95
|
+
updated_ts = max(timestamps) if timestamps else 0
|
|
96
|
+
|
|
97
|
+
created_at = _ms_to_dt(created_ts) if created_ts else datetime.now(timezone.utc)
|
|
98
|
+
updated_at = _ms_to_dt(updated_ts) if updated_ts else created_at
|
|
99
|
+
|
|
100
|
+
# Title from the first entry's display prompt.
|
|
101
|
+
first_display = entries[0].get("display", "")
|
|
102
|
+
title = first_display[:200] if first_display else None
|
|
103
|
+
cwd = entries[0].get("workspace") or None
|
|
104
|
+
|
|
105
|
+
sessions.append(
|
|
106
|
+
Session(
|
|
107
|
+
id=conversation_id,
|
|
108
|
+
tool=self.tool_name,
|
|
109
|
+
title=title,
|
|
110
|
+
cwd=cwd,
|
|
111
|
+
created_at=created_at,
|
|
112
|
+
updated_at=updated_at,
|
|
113
|
+
raw_path=str(history_path),
|
|
114
|
+
status="unknown",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return sessions
|
|
119
|
+
|
|
120
|
+
def load_messages(self, session_id: str) -> list[Message]:
|
|
121
|
+
"""Load messages for a given Antigravity session ID.
|
|
122
|
+
|
|
123
|
+
Antigravity stores full conversations as encrypted protobuf (``.pb``)
|
|
124
|
+
files under ``conversations/``. ContextForge cannot currently parse
|
|
125
|
+
those encrypted files, so this method attempts the following, in order:
|
|
126
|
+
|
|
127
|
+
1. Look for a JSON sidecar ``<session_id>.json`` (e.g. from a test
|
|
128
|
+
fixture or future export).
|
|
129
|
+
2. Read the history JSONL entries with this ``conversationId`` and
|
|
130
|
+
synthesise minimal ``Message`` objects from the user prompts.
|
|
131
|
+
"""
|
|
132
|
+
conv_dir = self._conversations_dir()
|
|
133
|
+
if conv_dir is not None:
|
|
134
|
+
json_path = conv_dir / f"{session_id}.json"
|
|
135
|
+
if json_path.exists():
|
|
136
|
+
return self._parse_json_conversation(json_path)
|
|
137
|
+
|
|
138
|
+
# Fallback: reconstruct user turns from history.jsonl.
|
|
139
|
+
history_path = self._history_path()
|
|
140
|
+
if history_path is None:
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
messages: list[Message] = []
|
|
144
|
+
with history_path.open() as f:
|
|
145
|
+
for line in f:
|
|
146
|
+
line = line.strip()
|
|
147
|
+
if not line:
|
|
148
|
+
continue
|
|
149
|
+
try:
|
|
150
|
+
entry = json.loads(line)
|
|
151
|
+
except json.JSONDecodeError:
|
|
152
|
+
continue
|
|
153
|
+
if entry.get("conversationId") != session_id:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
text = entry.get("display", "")
|
|
157
|
+
if not text:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
ts = entry.get("timestamp")
|
|
161
|
+
timestamp = _ms_to_dt(ts) if ts else None
|
|
162
|
+
messages.append(
|
|
163
|
+
Message(
|
|
164
|
+
role="user",
|
|
165
|
+
content=text,
|
|
166
|
+
timestamp=timestamp,
|
|
167
|
+
token_count=_count_tokens(text),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return messages
|
|
172
|
+
|
|
173
|
+
def _parse_json_conversation(self, path: Path) -> list[Message]:
|
|
174
|
+
"""Parse a JSON-sidecar conversation file into Messages.
|
|
175
|
+
|
|
176
|
+
Expected shape::
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
"messages": [
|
|
180
|
+
{"role": "user", "content": "...", "timestamp": 1234567890000},
|
|
181
|
+
{"role": "assistant", "content": "...", "timestamp": 1234567891000}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
"""
|
|
185
|
+
messages: list[Message] = []
|
|
186
|
+
try:
|
|
187
|
+
with path.open() as f:
|
|
188
|
+
data = json.load(f)
|
|
189
|
+
except (json.JSONDecodeError, OSError):
|
|
190
|
+
return messages
|
|
191
|
+
|
|
192
|
+
for msg in data.get("messages", []):
|
|
193
|
+
role = msg.get("role", "")
|
|
194
|
+
if role not in ("user", "assistant"):
|
|
195
|
+
continue
|
|
196
|
+
content = str(msg.get("content", ""))
|
|
197
|
+
if not content:
|
|
198
|
+
continue
|
|
199
|
+
ts = msg.get("timestamp")
|
|
200
|
+
timestamp = _ms_to_dt(ts) if isinstance(ts, int) else None
|
|
201
|
+
messages.append(
|
|
202
|
+
Message(
|
|
203
|
+
role=role,
|
|
204
|
+
content=content,
|
|
205
|
+
timestamp=timestamp,
|
|
206
|
+
token_count=_count_tokens(content),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return messages
|
|
211
|
+
|
|
212
|
+
def build_inject_command(
|
|
213
|
+
self,
|
|
214
|
+
context: str,
|
|
215
|
+
target_session_id: str | None = None,
|
|
216
|
+
cwd: str | None = None,
|
|
217
|
+
method: str = "system_prompt",
|
|
218
|
+
) -> str:
|
|
219
|
+
"""Build a shell command to start/resume an Antigravity CLI session.
|
|
220
|
+
|
|
221
|
+
``agy -i <prompt>`` starts an interactive session seeded with the given
|
|
222
|
+
prompt. ``agy --conversation <id>`` resumes an existing conversation.
|
|
223
|
+
"""
|
|
224
|
+
safe_ctx = shlex.quote(context)
|
|
225
|
+
|
|
226
|
+
if method == "resume" and target_session_id:
|
|
227
|
+
return f"agy --conversation {target_session_id}"
|
|
228
|
+
|
|
229
|
+
# Default: new interactive session with the context as the opening prompt.
|
|
230
|
+
cmd = f"agy -i {safe_ctx}"
|
|
231
|
+
if cwd:
|
|
232
|
+
cmd = f"cd {shlex.quote(cwd)} && {cmd}"
|
|
233
|
+
return cmd
|
|
@@ -6,6 +6,7 @@ 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.antigravity import AntigravityAdapter
|
|
9
10
|
from contextforge.adapters.gemini import GeminiAdapter
|
|
10
11
|
|
|
11
12
|
ADAPTERS: dict[str, type[ToolAdapter]] = {
|
|
@@ -14,6 +15,7 @@ ADAPTERS: dict[str, type[ToolAdapter]] = {
|
|
|
14
15
|
"codex": CodexAdapter,
|
|
15
16
|
"altimate_code": AltimateCodeAdapter,
|
|
16
17
|
"gemini": GeminiAdapter,
|
|
18
|
+
"antigravity": AntigravityAdapter,
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
|
{context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/session_detail.py
RENAMED
|
@@ -16,6 +16,7 @@ TOOL_COLORS = {
|
|
|
16
16
|
"altimate_code": "magenta",
|
|
17
17
|
"claude_desktop": "yellow",
|
|
18
18
|
"gemini": "blue",
|
|
19
|
+
"antigravity": "white",
|
|
19
20
|
}
|
|
20
21
|
TOOL_LABELS = {
|
|
21
22
|
"claude_code": "◆ Claude Code",
|
|
@@ -23,6 +24,7 @@ TOOL_LABELS = {
|
|
|
23
24
|
"altimate_code": "⚡ Altimate",
|
|
24
25
|
"claude_desktop": "◇ Claude Desktop",
|
|
25
26
|
"gemini": "✦ Gemini",
|
|
27
|
+
"antigravity": "◉ Antigravity",
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
|
{context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/session_table.py
RENAMED
|
@@ -19,6 +19,7 @@ TOOL_ICON = {
|
|
|
19
19
|
"altimate_code": "⚡", # Altimate — lightning
|
|
20
20
|
"claude_desktop": "◇", # Claude Desktop — open diamond
|
|
21
21
|
"gemini": "✦", # Google Gemini — four-pointed star
|
|
22
|
+
"antigravity": "◉", # Antigravity — circle
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
TOOL_MARKUP = {
|
|
@@ -27,6 +28,7 @@ TOOL_MARKUP = {
|
|
|
27
28
|
"altimate_code": "[magenta]⚡ Alt[/magenta]",
|
|
28
29
|
"claude_desktop": "[yellow]◇ Desktop[/yellow]",
|
|
29
30
|
"gemini": "[blue]✦ Gemini[/blue]",
|
|
31
|
+
"antigravity": "[white]◉ Agy[/white]",
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
TOOL_DISPLAY = {
|
|
@@ -35,6 +37,7 @@ TOOL_DISPLAY = {
|
|
|
35
37
|
"altimate_code": "⚡ Alt",
|
|
36
38
|
"claude_desktop": "◇ Desktop",
|
|
37
39
|
"gemini": "✦ Gemini",
|
|
40
|
+
"antigravity": "◉ Agy",
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
_FTOOL_MAP = {
|
|
@@ -43,6 +46,7 @@ _FTOOL_MAP = {
|
|
|
43
46
|
"ftool-codex": "codex",
|
|
44
47
|
"ftool-gem": "gemini",
|
|
45
48
|
"ftool-alt": "altimate_code",
|
|
49
|
+
"ftool-agy": "antigravity",
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
|
|
@@ -159,6 +163,7 @@ class SessionTable(Widget):
|
|
|
159
163
|
yield Button("⬡ Codex", id="ftool-codex", classes="filter-tool-btn")
|
|
160
164
|
yield Button("✦ Gem", id="ftool-gem", classes="filter-tool-btn")
|
|
161
165
|
yield Button("⚡ Alt", id="ftool-alt", classes="filter-tool-btn")
|
|
166
|
+
yield Button("◉ Agy", id="ftool-agy", classes="filter-tool-btn")
|
|
162
167
|
yield DataTable(cursor_type="row", zebra_stripes=True, id="sessions-datatable")
|
|
163
168
|
|
|
164
169
|
def on_mount(self) -> None:
|
{context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/stats_panel.py
RENAMED
|
@@ -21,6 +21,7 @@ TOOL_COLORS = {
|
|
|
21
21
|
"altimate_code": "magenta",
|
|
22
22
|
"claude_desktop": "yellow",
|
|
23
23
|
"gemini": "blue",
|
|
24
|
+
"antigravity": "white",
|
|
24
25
|
}
|
|
25
26
|
TOOL_LABELS = {
|
|
26
27
|
"claude_code": "◆ Claude Code",
|
|
@@ -28,9 +29,10 @@ TOOL_LABELS = {
|
|
|
28
29
|
"altimate_code": "⚡ Altimate",
|
|
29
30
|
"claude_desktop": "◇ Desktop",
|
|
30
31
|
"gemini": "✦ Gemini",
|
|
32
|
+
"antigravity": "◉ Antigravity",
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
_TOOL_ORDER = ["claude_code", "codex", "gemini", "altimate_code", "claude_desktop"]
|
|
35
|
+
_TOOL_ORDER = ["claude_code", "codex", "gemini", "altimate_code", "claude_desktop", "antigravity"]
|
|
34
36
|
|
|
35
37
|
WINDOW_LABELS = {
|
|
36
38
|
"7d": "7 days",
|
{context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/tui/widgets/status_bar.py
RENAMED
|
@@ -69,6 +69,7 @@ class StatusBar(Widget):
|
|
|
69
69
|
"altimate_code": "⚡ Alt",
|
|
70
70
|
"claude_desktop": "◇ Desktop",
|
|
71
71
|
"gemini": "✦ Gemini",
|
|
72
|
+
"antigravity": "◉ Agy",
|
|
72
73
|
}
|
|
73
74
|
for tool, label in labels.items():
|
|
74
75
|
n = counts.get(tool, 0)
|
|
@@ -95,7 +96,7 @@ class StatusBar(Widget):
|
|
|
95
96
|
if text:
|
|
96
97
|
parts.append(f'"{text}"')
|
|
97
98
|
if tool:
|
|
98
|
-
short = {"claude_code": "◆ CC", "codex": "⬡ Codex", "altimate_code": "⚡ Alt", "claude_desktop": "◇ Desktop", "gemini": "✦ Gemini"}.get(tool, tool)
|
|
99
|
+
short = {"claude_code": "◆ CC", "codex": "⬡ Codex", "altimate_code": "⚡ Alt", "claude_desktop": "◇ Desktop", "gemini": "✦ Gemini", "antigravity": "◉ Agy"}.get(tool, tool)
|
|
99
100
|
parts.append(short)
|
|
100
101
|
|
|
101
102
|
self.query_one("#status-filter", Static).update(
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Tests for the Antigravity adapter using fixture data."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import tempfile
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from contextforge.adapters.antigravity import AntigravityAdapter, _count_tokens, _ms_to_dt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_ms_to_dt():
|
|
15
|
+
ts = 1779426988061 # milliseconds
|
|
16
|
+
dt = _ms_to_dt(ts)
|
|
17
|
+
assert dt.year == 2026
|
|
18
|
+
assert dt.tzinfo is not None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_count_tokens():
|
|
22
|
+
text = "hello world"
|
|
23
|
+
n = _count_tokens(text)
|
|
24
|
+
assert isinstance(n, int) and n > 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_discover_sessions_from_fixture():
|
|
28
|
+
adapter = AntigravityAdapter()
|
|
29
|
+
|
|
30
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
31
|
+
history = Path(tmpdir) / ".gemini" / "antigravity-cli" / "history.jsonl"
|
|
32
|
+
history.parent.mkdir(parents=True)
|
|
33
|
+
entries = [
|
|
34
|
+
{
|
|
35
|
+
"display": "First prompt",
|
|
36
|
+
"timestamp": 1700000000000,
|
|
37
|
+
"workspace": "/tmp/ws1",
|
|
38
|
+
"conversationId": "conv-a",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"display": "Second prompt",
|
|
42
|
+
"timestamp": 1700000100000,
|
|
43
|
+
"workspace": "/tmp/ws1",
|
|
44
|
+
"conversationId": "conv-a",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"display": "Other prompt",
|
|
48
|
+
"timestamp": 1700001000000,
|
|
49
|
+
"workspace": "/tmp/ws2",
|
|
50
|
+
"conversationId": "conv-b",
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
history.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
|
54
|
+
|
|
55
|
+
import contextforge.adapters.antigravity as mod
|
|
56
|
+
original_history = mod._AGY_HISTORY
|
|
57
|
+
mod._AGY_HISTORY = history
|
|
58
|
+
try:
|
|
59
|
+
sessions = adapter.discover_sessions()
|
|
60
|
+
finally:
|
|
61
|
+
mod._AGY_HISTORY = original_history
|
|
62
|
+
|
|
63
|
+
assert len(sessions) == 2
|
|
64
|
+
ids = {s.id for s in sessions}
|
|
65
|
+
assert ids == {"conv-a", "conv-b"}
|
|
66
|
+
|
|
67
|
+
conv_a = next(s for s in sessions if s.id == "conv-a")
|
|
68
|
+
assert conv_a.title == "First prompt"
|
|
69
|
+
assert conv_a.cwd == "/tmp/ws1"
|
|
70
|
+
assert conv_a.tool == "antigravity"
|
|
71
|
+
assert conv_a.created_at == _ms_to_dt(1700000000000)
|
|
72
|
+
assert conv_a.updated_at == _ms_to_dt(1700000100000)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_discover_skips_entries_without_conversation_id():
|
|
76
|
+
adapter = AntigravityAdapter()
|
|
77
|
+
|
|
78
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
79
|
+
history = Path(tmpdir) / ".gemini" / "antigravity-cli" / "history.jsonl"
|
|
80
|
+
history.parent.mkdir(parents=True)
|
|
81
|
+
entries = [
|
|
82
|
+
{"display": "orphan", "timestamp": 1700000000000, "workspace": "/tmp/ws"},
|
|
83
|
+
# No conversationId
|
|
84
|
+
]
|
|
85
|
+
history.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
|
86
|
+
|
|
87
|
+
import contextforge.adapters.antigravity as mod
|
|
88
|
+
original_history = mod._AGY_HISTORY
|
|
89
|
+
mod._AGY_HISTORY = history
|
|
90
|
+
try:
|
|
91
|
+
sessions = adapter.discover_sessions()
|
|
92
|
+
finally:
|
|
93
|
+
mod._AGY_HISTORY = original_history
|
|
94
|
+
|
|
95
|
+
assert sessions == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_load_messages_json_sidecar():
|
|
99
|
+
adapter = AntigravityAdapter()
|
|
100
|
+
|
|
101
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
102
|
+
history = Path(tmpdir) / ".gemini" / "antigravity-cli" / "history.jsonl"
|
|
103
|
+
history.parent.mkdir(parents=True)
|
|
104
|
+
history.write_text(json.dumps({"display": "hi", "timestamp": 1700000000000, "conversationId": "c1"}) + "\n")
|
|
105
|
+
|
|
106
|
+
conversations = history.parent / "conversations"
|
|
107
|
+
conversations.mkdir()
|
|
108
|
+
json_path = conversations / "c1.json"
|
|
109
|
+
json_path.write_text(
|
|
110
|
+
json.dumps(
|
|
111
|
+
{
|
|
112
|
+
"messages": [
|
|
113
|
+
{"role": "user", "content": "hello", "timestamp": 1700000000000},
|
|
114
|
+
{"role": "assistant", "content": "world", "timestamp": 1700000010000},
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
import contextforge.adapters.antigravity as mod
|
|
121
|
+
original_history = mod._AGY_HISTORY
|
|
122
|
+
original_conv = mod._AGY_CONVERSATIONS
|
|
123
|
+
mod._AGY_HISTORY = history
|
|
124
|
+
mod._AGY_CONVERSATIONS = conversations
|
|
125
|
+
try:
|
|
126
|
+
messages = adapter.load_messages("c1")
|
|
127
|
+
finally:
|
|
128
|
+
mod._AGY_HISTORY = original_history
|
|
129
|
+
mod._AGY_CONVERSATIONS = original_conv
|
|
130
|
+
|
|
131
|
+
assert len(messages) == 2
|
|
132
|
+
assert messages[0].role == "user"
|
|
133
|
+
assert messages[0].content == "hello"
|
|
134
|
+
assert messages[1].role == "assistant"
|
|
135
|
+
assert messages[1].content == "world"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_load_messages_fallback_to_history():
|
|
139
|
+
adapter = AntigravityAdapter()
|
|
140
|
+
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
142
|
+
history = Path(tmpdir) / ".gemini" / "antigravity-cli" / "history.jsonl"
|
|
143
|
+
history.parent.mkdir(parents=True)
|
|
144
|
+
entries = [
|
|
145
|
+
{
|
|
146
|
+
"display": "Prompt one",
|
|
147
|
+
"timestamp": 1700000000000,
|
|
148
|
+
"workspace": "/tmp/ws",
|
|
149
|
+
"conversationId": "c1",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"display": "Prompt two",
|
|
153
|
+
"timestamp": 1700000010000,
|
|
154
|
+
"workspace": "/tmp/ws",
|
|
155
|
+
"conversationId": "c1",
|
|
156
|
+
},
|
|
157
|
+
]
|
|
158
|
+
history.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
|
159
|
+
|
|
160
|
+
import contextforge.adapters.antigravity as mod
|
|
161
|
+
original_history = mod._AGY_HISTORY
|
|
162
|
+
mod._AGY_HISTORY = history
|
|
163
|
+
try:
|
|
164
|
+
messages = adapter.load_messages("c1")
|
|
165
|
+
finally:
|
|
166
|
+
mod._AGY_HISTORY = original_history
|
|
167
|
+
|
|
168
|
+
assert len(messages) == 2
|
|
169
|
+
assert messages[0].role == "user"
|
|
170
|
+
assert messages[0].content == "Prompt one"
|
|
171
|
+
assert messages[1].role == "user"
|
|
172
|
+
assert messages[1].content == "Prompt two"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_build_inject_command_new_session():
|
|
176
|
+
adapter = AntigravityAdapter()
|
|
177
|
+
cmd = adapter.build_inject_command("some context", method="system_prompt")
|
|
178
|
+
assert "agy" in cmd
|
|
179
|
+
assert "-i" in cmd
|
|
180
|
+
assert "some context" in cmd
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_build_inject_command_resume():
|
|
184
|
+
adapter = AntigravityAdapter()
|
|
185
|
+
cmd = adapter.build_inject_command("ctx", target_session_id="abc-123", method="resume")
|
|
186
|
+
assert "agy" in cmd
|
|
187
|
+
assert "--conversation" in cmd
|
|
188
|
+
assert "abc-123" in cmd
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_build_inject_command_with_cwd():
|
|
192
|
+
adapter = AntigravityAdapter()
|
|
193
|
+
cmd = adapter.build_inject_command("ctx", cwd="/tmp/proj")
|
|
194
|
+
assert cmd.startswith("cd /tmp/proj")
|
|
195
|
+
assert "agy" in cmd
|
|
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.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/altimate_code.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.4.10 → context_forge_cli-0.4.11}/contextforge/adapters/claude_desktop.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
|
|
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.4.10 → context_forge_cli-0.4.11}/tests/fixtures/claude_session_sample.jsonl
RENAMED
|
File without changes
|
|
File without changes
|