context-forge-cli 0.2.2__tar.gz → 0.2.3__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.2 → context_forge_cli-0.2.3}/PKG-INFO +1 -1
  2. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/altimate_code.py +15 -4
  3. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/base.py +15 -0
  4. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/claude_code.py +10 -3
  5. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/cli.py +73 -0
  6. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/pyproject.toml +1 -1
  7. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.contextforge.toml +0 -0
  8. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.github/workflows/publish.yml +0 -0
  9. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.gitignore +0 -0
  10. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.python-version +0 -0
  11. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/AGENTS.md +0 -0
  12. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/PLAN.md +0 -0
  13. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/README.md +0 -0
  14. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/__init__.py +0 -0
  15. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/__init__.py +0 -0
  16. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/claude_desktop.py +0 -0
  17. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/codex.py +0 -0
  18. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/gemini.py +0 -0
  19. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/registry.py +0 -0
  20. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/__init__.py +0 -0
  21. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/analytics.py +0 -0
  22. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/compactor.py +0 -0
  23. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/db.py +0 -0
  24. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/injector.py +0 -0
  25. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/scanner.py +0 -0
  26. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/summarizer.py +0 -0
  27. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/token_analyzer.py +0 -0
  28. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/models/__init__.py +0 -0
  29. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/models/config.py +0 -0
  30. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/models/session.py +0 -0
  31. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/__init__.py +0 -0
  32. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/app.py +0 -0
  33. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/styles.tcss +0 -0
  34. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/__init__.py +0 -0
  35. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/session_detail.py +0 -0
  36. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/session_table.py +0 -0
  37. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/stats_panel.py +0 -0
  38. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/status_bar.py +0 -0
  39. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/tokens_panel.py +0 -0
  40. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/transfer_panel.py +0 -0
  41. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/utils/__init__.py +0 -0
  42. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/utils/display.py +0 -0
  43. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/utils/tokens.py +0 -0
  44. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/__init__.py +0 -0
  45. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/adapters/__init__.py +0 -0
  46. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/adapters/test_claude_code.py +0 -0
  47. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/__init__.py +0 -0
  48. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_compactor.py +0 -0
  49. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_db.py +0 -0
  50. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_injector.py +0 -0
  51. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_summarizer.py +0 -0
  52. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_token_analyzer.py +0 -0
  53. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/fixtures/claude_session_sample.jsonl +0 -0
  54. {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: context-forge-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -87,13 +87,19 @@ class AltimateCodeAdapter(ToolAdapter):
87
87
  conn.row_factory = sqlite3.Row
88
88
  cur = conn.cursor()
89
89
  # Join message + part to get full content
90
+ # Note: altimate-code stores data as JSON in 'data' column
90
91
  cur.execute(
91
92
  """
92
- SELECT m.role, p.type as part_type, p.content, m.time as ts
93
+ SELECT
94
+ m.id,
95
+ json_extract(m.data, '$.role') as role,
96
+ m.time_created as ts,
97
+ json_extract(p.data, '$.type') as part_type,
98
+ json_extract(p.data, '$.text') as content
93
99
  FROM message m
94
100
  LEFT JOIN part p ON p.message_id = m.id
95
101
  WHERE m.session_id = ?
96
- ORDER BY m.time ASC, p.id ASC
102
+ ORDER BY m.time_created ASC, p.id ASC
97
103
  """,
98
104
  (session_id,),
99
105
  )
@@ -103,12 +109,13 @@ class AltimateCodeAdapter(ToolAdapter):
103
109
  return []
104
110
 
105
111
  messages: list[Message] = []
112
+ current_msg_id: str | None = None
106
113
  current_role: str | None = None
107
114
  current_parts: list[str] = []
108
115
  current_ts: datetime | None = None
109
116
 
110
117
  def flush():
111
- nonlocal current_role, current_parts, current_ts
118
+ nonlocal current_msg_id, current_role, current_parts, current_ts
112
119
  if current_role and current_parts:
113
120
  content = "\n".join(p for p in current_parts if p)
114
121
  if content:
@@ -120,17 +127,21 @@ class AltimateCodeAdapter(ToolAdapter):
120
127
  token_count=_count_tokens(content),
121
128
  )
122
129
  )
130
+ current_msg_id = None
123
131
  current_role = None
124
132
  current_parts = []
125
133
  current_ts = None
126
134
 
127
135
  for row in rows:
136
+ msg_id = row["id"]
128
137
  role = row["role"]
129
138
  if role not in ("user", "assistant"):
130
139
  continue
131
140
 
132
- if role != current_role:
141
+ # Flush when message ID changes (new message in the message table)
142
+ if msg_id != current_msg_id:
133
143
  flush()
144
+ current_msg_id = msg_id
134
145
  current_role = role
135
146
  try:
136
147
  current_ts = datetime.fromtimestamp(row["ts"] / 1000, tz=timezone.utc)
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
5
5
  from pathlib import Path
6
6
 
7
7
  from contextforge.models.session import Message, Session
8
+ from contextforge.utils.tokens import count_tokens
8
9
 
9
10
 
10
11
  class ToolAdapter(ABC):
@@ -42,3 +43,17 @@ class ToolAdapter(ABC):
42
43
  def is_available(self) -> bool:
43
44
  """Return True if this tool is installed and its data paths exist."""
44
45
  return any(p.exists() for p in self.default_paths)
46
+
47
+ def _count_session_tokens(self, session_id: str | Path) -> int:
48
+ """Count total tokens in a session by loading and summing message tokens.
49
+
50
+ This is the default implementation that all adapters can use.
51
+ Adapters may override this with more efficient implementations.
52
+ """
53
+ if isinstance(session_id, Path):
54
+ session_id = session_id.stem
55
+ messages = self.load_messages(str(session_id))
56
+ total = 0
57
+ for msg in messages:
58
+ total += msg.token_count or count_tokens(msg.content)
59
+ return total
@@ -339,9 +339,16 @@ class ClaudeCodeAdapter(ToolAdapter):
339
339
 
340
340
  return messages
341
341
 
342
- def _count_session_tokens(self, jsonl_path: Path) -> int:
343
- """Count total tokens in a session JSONL file."""
344
- session_id = jsonl_path.stem
342
+ def _count_session_tokens(self, jsonl_path: Path | str) -> int:
343
+ """Count total tokens in a session JSONL file or session ID.
344
+
345
+ Args:
346
+ jsonl_path: Either a Path to the JSONL file or a session ID string.
347
+ """
348
+ if isinstance(jsonl_path, str):
349
+ session_id = jsonl_path
350
+ else:
351
+ session_id = jsonl_path.stem
345
352
  messages = self.load_messages(session_id)
346
353
  total = 0
347
354
  for msg in messages:
@@ -505,6 +505,79 @@ def config_set(key: str, value: str):
505
505
  console.print(f"Would set [bold]{key}[/bold] = [bold]{value}[/bold]")
506
506
 
507
507
 
508
+ # ---------------------------------------------------------------------------
509
+ # cf refresh
510
+ # ---------------------------------------------------------------------------
511
+
512
+ @app.command()
513
+ def refresh(
514
+ quiet: Annotated[bool, typer.Option("--quiet", "-q")] = False,
515
+ ):
516
+ """Refresh token counts and metadata for all sessions.
517
+
518
+ Recalculates token_count for every session from source data.
519
+ Useful after adapter updates or schema changes.
520
+ """
521
+ cfg = _get_config()
522
+ database = _get_db(cfg)
523
+
524
+ from contextforge.adapters.registry import get_adapter
525
+
526
+ # Get all sessions grouped by tool
527
+ sessions_by_tool = {}
528
+ for row in database.execute("SELECT DISTINCT tool FROM sessions ORDER BY tool").fetchall():
529
+ tool = row[0]
530
+ sessions_by_tool[tool] = []
531
+ for s in database.execute("SELECT id, title FROM sessions WHERE tool = ? ORDER BY id", [tool]).fetchall():
532
+ sessions_by_tool[tool].append((s[0], s[1]))
533
+
534
+ from rich.progress import Progress, SpinnerColumn, TextColumn
535
+
536
+ total_sessions = sum(len(sessions) for sessions in sessions_by_tool.values())
537
+ updated_count = 0
538
+ error_count = 0
539
+
540
+ with Progress(
541
+ SpinnerColumn(),
542
+ TextColumn("[progress.description]{task.description}"),
543
+ disable=quiet,
544
+ ) as progress:
545
+ task = progress.add_task("Refreshing...", total=total_sessions)
546
+
547
+ for tool, sessions in sessions_by_tool.items():
548
+ if not sessions:
549
+ continue
550
+
551
+ try:
552
+ adapter = get_adapter(tool)
553
+ except Exception as e:
554
+ if not quiet:
555
+ err_console.print(f"[yellow]Warning:[/yellow] Skipping {tool}: {e}")
556
+ error_count += len(sessions)
557
+ progress.advance(task, len(sessions))
558
+ continue
559
+
560
+ for session_id, title in sessions:
561
+ try:
562
+ token_count = adapter._count_session_tokens(session_id)
563
+ database["sessions"].update(session_id, {"token_count": token_count})
564
+ updated_count += 1
565
+ if not quiet:
566
+ progress.update(task, description=f"Refreshed {session_id[:12]} ({token_count:,} tokens)")
567
+ except Exception as e:
568
+ error_count += 1
569
+ if not quiet:
570
+ err_console.print(f"[yellow]Warning:[/yellow] {session_id}: {e}")
571
+ finally:
572
+ progress.advance(task)
573
+
574
+ if not quiet:
575
+ console.print(
576
+ f"[green]Refresh complete:[/green] "
577
+ f"{updated_count} updated" + (f", {error_count} errors" if error_count else "")
578
+ )
579
+
580
+
508
581
  # ---------------------------------------------------------------------------
509
582
  # cf dashboard
510
583
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "context-forge-cli"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Session manager and context bridge for agentic CLI tools"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }