context-forge-cli 0.4.7__tar.gz → 0.4.9__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.7 → context_forge_cli-0.4.9}/PKG-INFO +1 -1
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/app.py +13 -0
- context_forge_cli-0.4.9/contextforge/tui/clipboard.py +28 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/session_detail.py +16 -5
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/tokens_panel.py +32 -17
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/pyproject.toml +1 -1
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.contextforge.toml +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.github/workflows/publish.yml +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.gitignore +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.python-version +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/AGENTS.md +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/PLAN.md +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/README.md +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/altimate_code.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/base.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/claude_code.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/claude_desktop.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/codex.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/gemini.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/registry.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/cli.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/analytics.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/compactor.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/db.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/injector.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/scanner.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/summarizer.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/token_analyzer.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/mcp_server.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/models/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/models/config.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/models/session.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/styles.tcss +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/session_table.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/stats_panel.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/status_bar.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/transfer_panel.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/utils/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/utils/display.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/utils/tokens.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/adapters/test_claude_code.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/conftest.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/__init__.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_compactor.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_db.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_injector.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_summarizer.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_token_analyzer.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/fixtures/claude_session_sample.jsonl +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/test_mcp_server.py +0 -0
- {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/uv.lock +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.9
|
|
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
|
|
@@ -8,6 +8,7 @@ from textual.binding import Binding
|
|
|
8
8
|
from textual.containers import Horizontal, Vertical
|
|
9
9
|
from textual.widgets import Footer, Header
|
|
10
10
|
|
|
11
|
+
from contextforge.tui.clipboard import copy as clipboard_copy
|
|
11
12
|
from contextforge.tui.widgets.session_detail import SessionDetail
|
|
12
13
|
from contextforge.tui.widgets.session_table import SessionTable
|
|
13
14
|
from contextforge.tui.widgets.status_bar import StatusBar
|
|
@@ -31,6 +32,7 @@ class ContextForgeApp(App):
|
|
|
31
32
|
Binding("x", "tokens", "# Tokens"),
|
|
32
33
|
Binding("a", "analytics", "▤ Analytics"),
|
|
33
34
|
Binding("/", "filter", "⌕ Filter"),
|
|
35
|
+
Binding("y", "yank_id", "📋 Copy ID"),
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
def __init__(self, db_path: Path | None = None) -> None:
|
|
@@ -203,3 +205,14 @@ class ContextForgeApp(App):
|
|
|
203
205
|
def action_filter(self) -> None:
|
|
204
206
|
"""Toggle the filter bar in the session table."""
|
|
205
207
|
self.query_one(SessionTable).toggle_filter()
|
|
208
|
+
|
|
209
|
+
def action_yank_id(self) -> None:
|
|
210
|
+
"""Copy the current session ID to the clipboard."""
|
|
211
|
+
sid = getattr(self, "_current_session_id", None)
|
|
212
|
+
if sid is None:
|
|
213
|
+
self.notify("Select a session first.", severity="warning")
|
|
214
|
+
return
|
|
215
|
+
if clipboard_copy(sid):
|
|
216
|
+
self.notify(f"Copied [cyan]{sid}[/cyan]", title="Yank")
|
|
217
|
+
else:
|
|
218
|
+
self.notify("Failed to copy to clipboard.", severity="error")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Cross-platform clipboard utilities for the TUI."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def copy(text: str) -> bool:
|
|
8
|
+
"""Copy *text* to the system clipboard. Returns True on success."""
|
|
9
|
+
if not text:
|
|
10
|
+
return False
|
|
11
|
+
system = platform.system()
|
|
12
|
+
try:
|
|
13
|
+
if system == "Darwin":
|
|
14
|
+
subprocess.run(["pbcopy"], input=text, text=True, check=True)
|
|
15
|
+
elif system == "Linux":
|
|
16
|
+
# try Wayland first, then X11
|
|
17
|
+
try:
|
|
18
|
+
subprocess.run(["wl-copy"], input=text, text=True, check=True)
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
subprocess.run(
|
|
21
|
+
["xclip", "-selection", "clipboard"],
|
|
22
|
+
input=text, text=True, check=True,
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
return False
|
|
26
|
+
return True
|
|
27
|
+
except Exception:
|
|
28
|
+
return False
|
{context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/session_detail.py
RENAMED
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from textual.app import ComposeResult
|
|
9
9
|
from textual.containers import Vertical
|
|
10
10
|
from textual.widget import Widget
|
|
11
|
-
from textual.widgets import Markdown, Static
|
|
11
|
+
from textual.widgets import Markdown, Static, TextArea
|
|
12
12
|
|
|
13
13
|
TOOL_COLORS = {
|
|
14
14
|
"claude_code": "cyan",
|
|
@@ -101,6 +101,17 @@ class SessionDetail(Widget):
|
|
|
101
101
|
background: transparent;
|
|
102
102
|
color: $text;
|
|
103
103
|
}
|
|
104
|
+
SessionDetail TextArea {
|
|
105
|
+
height: 1;
|
|
106
|
+
border: none;
|
|
107
|
+
background: transparent;
|
|
108
|
+
color: $text;
|
|
109
|
+
padding: 0;
|
|
110
|
+
margin: 0;
|
|
111
|
+
}
|
|
112
|
+
SessionDetail TextArea:focus {
|
|
113
|
+
border: none;
|
|
114
|
+
}
|
|
104
115
|
"""
|
|
105
116
|
|
|
106
117
|
_current_id: str | None = None
|
|
@@ -110,13 +121,13 @@ class SessionDetail(Widget):
|
|
|
110
121
|
yield Static("", id="detail-title")
|
|
111
122
|
with Vertical(id="detail-meta-block"):
|
|
112
123
|
yield Static("", id="detail-tool")
|
|
113
|
-
yield
|
|
124
|
+
yield TextArea("", id="detail-cwd", read_only=True)
|
|
114
125
|
yield Static("", id="detail-tokens")
|
|
115
126
|
yield Static("", id="detail-created")
|
|
116
127
|
yield Static("", id="detail-updated")
|
|
117
128
|
yield Static("", id="detail-status")
|
|
118
129
|
yield Static("", id="detail-tags")
|
|
119
|
-
yield
|
|
130
|
+
yield TextArea("", id="detail-id", read_only=True)
|
|
120
131
|
yield Static("✎ Summary", id="detail-summary-label")
|
|
121
132
|
yield Markdown("", id="detail-summary-md")
|
|
122
133
|
|
|
@@ -176,13 +187,13 @@ class SessionDetail(Widget):
|
|
|
176
187
|
|
|
177
188
|
self.query_one("#detail-title", Static).update(f"[bold]{title}[/bold]")
|
|
178
189
|
self.query_one("#detail-tool", Static).update(_meta_line("Tool", tool_markup))
|
|
179
|
-
self.query_one("#detail-cwd",
|
|
190
|
+
self.query_one("#detail-cwd", TextArea).load_text("Project " + cwd)
|
|
180
191
|
self.query_one("#detail-tokens", Static).update(_meta_line("Tokens", tok_markup))
|
|
181
192
|
self.query_one("#detail-created", Static).update(_meta_line("Created", f"[dim]{created_str}[/dim]"))
|
|
182
193
|
self.query_one("#detail-updated", Static).update(_meta_line("Updated", f"[dim]{updated_str}[/dim]"))
|
|
183
194
|
self.query_one("#detail-status", Static).update(_meta_line("Status", f"[dim]{status}[/dim]"))
|
|
184
195
|
self.query_one("#detail-tags", Static).update(_meta_line("Tags", f"[dim]{tags}[/dim]"))
|
|
185
|
-
self.query_one("#detail-id",
|
|
196
|
+
self.query_one("#detail-id", TextArea).load_text("ID " + session_id)
|
|
186
197
|
|
|
187
198
|
md = self.query_one("#detail-summary-md", Markdown)
|
|
188
199
|
self.app.call_later(md.update, summary)
|
{context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/tokens_panel.py
RENAMED
|
@@ -4,14 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from rich.text import Text
|
|
8
7
|
from textual.app import ComposeResult
|
|
9
8
|
from textual.binding import Binding
|
|
10
|
-
from textual.containers import
|
|
9
|
+
from textual.containers import Vertical
|
|
11
10
|
from textual.screen import ModalScreen
|
|
12
|
-
from textual.widgets import DataTable, Static
|
|
11
|
+
from textual.widgets import DataTable, Static, TextArea
|
|
13
12
|
|
|
14
13
|
from contextforge.models.session import Message
|
|
14
|
+
from contextforge.tui.clipboard import copy as clipboard_copy
|
|
15
15
|
|
|
16
16
|
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
@@ -55,6 +55,7 @@ class TurnDetailPanel(ModalScreen):
|
|
|
55
55
|
|
|
56
56
|
BINDINGS = [
|
|
57
57
|
Binding("escape,q", "dismiss(None)", "Close"),
|
|
58
|
+
Binding("y", "copy_content", "📋 Copy"),
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
DEFAULT_CSS = """
|
|
@@ -74,10 +75,16 @@ class TurnDetailPanel(ModalScreen):
|
|
|
74
75
|
border-bottom: solid $primary-background-lighten-1;
|
|
75
76
|
margin-bottom: 1;
|
|
76
77
|
}
|
|
77
|
-
TurnDetailPanel
|
|
78
|
+
TurnDetailPanel TextArea {
|
|
78
79
|
height: 1fr;
|
|
80
|
+
border: none;
|
|
81
|
+
background: transparent;
|
|
82
|
+
color: $text;
|
|
79
83
|
padding: 0 1;
|
|
80
84
|
}
|
|
85
|
+
TurnDetailPanel TextArea:focus {
|
|
86
|
+
border: none;
|
|
87
|
+
}
|
|
81
88
|
TurnDetailPanel #turn-footer {
|
|
82
89
|
height: 1;
|
|
83
90
|
color: $text-disabled;
|
|
@@ -89,13 +96,13 @@ class TurnDetailPanel(ModalScreen):
|
|
|
89
96
|
super().__init__()
|
|
90
97
|
self._turn_num = turn_num
|
|
91
98
|
self._msg = msg
|
|
99
|
+
self._plain_text = ""
|
|
92
100
|
|
|
93
101
|
def compose(self) -> ComposeResult:
|
|
94
102
|
with Vertical():
|
|
95
103
|
yield Static("", id="turn-header")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
yield Static("[dim]ESC / q — close[/dim]", id="turn-footer")
|
|
104
|
+
yield TextArea("", id="turn-body", read_only=True)
|
|
105
|
+
yield Static("[dim]ESC / q — close select & Ctrl+C[/dim]", id="turn-footer")
|
|
99
106
|
|
|
100
107
|
def on_mount(self) -> None:
|
|
101
108
|
msg = self._msg
|
|
@@ -117,14 +124,14 @@ class TurnDetailPanel(ModalScreen):
|
|
|
117
124
|
if msg.tool_results else "")
|
|
118
125
|
)
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
plain_parts = []
|
|
121
128
|
|
|
122
129
|
if msg.content:
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
plain_parts.append("── Content ──\n")
|
|
131
|
+
plain_parts.append(msg.content + "\n")
|
|
125
132
|
|
|
126
133
|
if msg.tool_calls:
|
|
127
|
-
|
|
134
|
+
plain_parts.append("\n── Tool Calls ──\n")
|
|
128
135
|
for i, tc in enumerate(msg.tool_calls, 1):
|
|
129
136
|
name = tc.get("name", "?")
|
|
130
137
|
raw_input = tc.get("input", "")
|
|
@@ -132,19 +139,27 @@ class TurnDetailPanel(ModalScreen):
|
|
|
132
139
|
formatted = json.dumps(json.loads(raw_input), indent=2)
|
|
133
140
|
except (json.JSONDecodeError, TypeError):
|
|
134
141
|
formatted = raw_input if isinstance(raw_input, str) else str(raw_input)
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
plain_parts.append(f"⚙ {i}. {name}\n")
|
|
143
|
+
plain_parts.append(formatted + "\n")
|
|
137
144
|
|
|
138
145
|
if msg.tool_results:
|
|
139
|
-
|
|
146
|
+
plain_parts.append("\n── Tool Results ──\n")
|
|
140
147
|
for i, tr in enumerate(msg.tool_results, 1):
|
|
141
148
|
output = tr.get("output", "")
|
|
142
149
|
if not isinstance(output, str):
|
|
143
150
|
output = str(output)
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
plain_parts.append(f"⇥ Result {i}\n")
|
|
152
|
+
plain_parts.append(output + "\n")
|
|
146
153
|
|
|
147
|
-
self.
|
|
154
|
+
self._plain_text = "".join(plain_parts)
|
|
155
|
+
self.query_one("#turn-body", TextArea).load_text(self._plain_text)
|
|
156
|
+
|
|
157
|
+
def action_copy_content(self) -> None:
|
|
158
|
+
"""Copy the full turn content to the clipboard."""
|
|
159
|
+
if clipboard_copy(self._plain_text):
|
|
160
|
+
self.notify(f"Copied turn #{self._turn_num} content", title="Copy")
|
|
161
|
+
else:
|
|
162
|
+
self.notify("Failed to copy to clipboard.", severity="error")
|
|
148
163
|
|
|
149
164
|
|
|
150
165
|
# ── TokensPanel ───────────────────────────────────────────────────────────────
|
|
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.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/session_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/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
|
|
File without changes
|
{context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/fixtures/claude_session_sample.jsonl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|