context-forge-cli 0.2.1__tar.gz → 0.2.2__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.2.1 → context_forge_cli-0.2.2}/PKG-INFO +1 -1
  2. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/altimate_code.py +2 -1
  3. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/claude_code.py +5 -28
  4. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/claude_desktop.py +13 -4
  5. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/codex.py +24 -2
  6. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/gemini.py +13 -0
  7. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/pyproject.toml +1 -1
  8. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/uv.lock +2 -2
  9. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/.contextforge.toml +0 -0
  10. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/.github/workflows/publish.yml +0 -0
  11. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/.gitignore +0 -0
  12. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/.python-version +0 -0
  13. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/AGENTS.md +0 -0
  14. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/PLAN.md +0 -0
  15. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/README.md +0 -0
  16. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/__init__.py +0 -0
  17. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/__init__.py +0 -0
  18. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/base.py +0 -0
  19. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/adapters/registry.py +0 -0
  20. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/cli.py +0 -0
  21. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/__init__.py +0 -0
  22. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/analytics.py +0 -0
  23. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/compactor.py +0 -0
  24. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/db.py +0 -0
  25. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/injector.py +0 -0
  26. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/scanner.py +0 -0
  27. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/summarizer.py +0 -0
  28. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/core/token_analyzer.py +0 -0
  29. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/models/__init__.py +0 -0
  30. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/models/config.py +0 -0
  31. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/models/session.py +0 -0
  32. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/__init__.py +0 -0
  33. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/app.py +0 -0
  34. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/styles.tcss +0 -0
  35. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/__init__.py +0 -0
  36. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/session_detail.py +0 -0
  37. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/session_table.py +0 -0
  38. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/stats_panel.py +0 -0
  39. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/status_bar.py +0 -0
  40. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/tokens_panel.py +0 -0
  41. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/tui/widgets/transfer_panel.py +0 -0
  42. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/utils/__init__.py +0 -0
  43. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/utils/display.py +0 -0
  44. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/contextforge/utils/tokens.py +0 -0
  45. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/__init__.py +0 -0
  46. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/adapters/__init__.py +0 -0
  47. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/adapters/test_claude_code.py +0 -0
  48. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/core/__init__.py +0 -0
  49. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/core/test_compactor.py +0 -0
  50. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/core/test_db.py +0 -0
  51. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/core/test_injector.py +0 -0
  52. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/core/test_summarizer.py +0 -0
  53. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/core/test_token_analyzer.py +0 -0
  54. {context_forge_cli-0.2.1 → context_forge_cli-0.2.2}/tests/fixtures/claude_session_sample.jsonl +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: context-forge-cli
3
- Version: 0.2.1
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
@@ -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
- try:
345
- with jsonl_path.open() as f:
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(role="user" if role == "user" else "assistant",
244
- content=content, timestamp=ts)
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(role=role, content=content, timestamp=ts))
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(
@@ -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
- messages.append(Message(role="user", content=str(text)))
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
- messages.append(Message(role="assistant", content=str(text)))
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
 
@@ -6,12 +6,24 @@ 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
14
  _GEMINI_TMP_BASE = Path.home() / ".gemini" / "tmp"
13
15
 
14
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
25
+
26
+
15
27
  class GeminiAdapter(ToolAdapter):
16
28
  tool_name = "gemini"
17
29
  default_paths = [_GEMINI_TMP_BASE]
@@ -163,6 +175,7 @@ class GeminiAdapter(ToolAdapter):
163
175
  role="user" if msg_type == "user" else "assistant",
164
176
  content=content,
165
177
  timestamp=ts,
178
+ token_count=_count_tokens(content),
166
179
  )
167
180
  )
168
181
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "context-forge-cli"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Session manager and context bridge for agentic CLI tools"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -151,8 +151,8 @@ wheels = [
151
151
  ]
152
152
 
153
153
  [[package]]
154
- name = "context-forge"
155
- version = "0.1.0"
154
+ name = "context-forge-cli"
155
+ version = "0.2.1"
156
156
  source = { editable = "." }
157
157
  dependencies = [
158
158
  { name = "anthropic" },