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.
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/PKG-INFO +1 -1
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/altimate_code.py +15 -4
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/base.py +15 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/claude_code.py +10 -3
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/cli.py +73 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/pyproject.toml +1 -1
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.contextforge.toml +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.github/workflows/publish.yml +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.gitignore +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/.python-version +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/AGENTS.md +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/PLAN.md +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/README.md +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/claude_desktop.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/codex.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/gemini.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/adapters/registry.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/analytics.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/compactor.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/db.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/injector.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/scanner.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/summarizer.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/core/token_analyzer.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/models/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/models/config.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/models/session.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/app.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/styles.tcss +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/session_detail.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/session_table.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/stats_panel.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/status_bar.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/tokens_panel.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/transfer_panel.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/utils/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/utils/display.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/utils/tokens.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/adapters/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/adapters/test_claude_code.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/__init__.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_compactor.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_db.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_injector.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_summarizer.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/core/test_token_analyzer.py +0 -0
- {context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/tests/fixtures/claude_session_sample.jsonl +0 -0
- {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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
# ---------------------------------------------------------------------------
|
|
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.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/session_detail.py
RENAMED
|
File without changes
|
{context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/session_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/contextforge/tui/widgets/tokens_panel.py
RENAMED
|
File without changes
|
{context_forge_cli-0.2.2 → context_forge_cli-0.2.3}/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.2 → context_forge_cli-0.2.3}/tests/fixtures/claude_session_sample.jsonl
RENAMED
|
File without changes
|
|
File without changes
|