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.
Files changed (58) hide show
  1. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/PKG-INFO +1 -1
  2. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/app.py +13 -0
  3. context_forge_cli-0.4.9/contextforge/tui/clipboard.py +28 -0
  4. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/session_detail.py +16 -5
  5. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/tokens_panel.py +32 -17
  6. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/pyproject.toml +1 -1
  7. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.contextforge.toml +0 -0
  8. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.github/workflows/publish.yml +0 -0
  9. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.gitignore +0 -0
  10. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/.python-version +0 -0
  11. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/AGENTS.md +0 -0
  12. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/PLAN.md +0 -0
  13. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/README.md +0 -0
  14. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/__init__.py +0 -0
  15. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/__init__.py +0 -0
  16. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/altimate_code.py +0 -0
  17. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/base.py +0 -0
  18. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/claude_code.py +0 -0
  19. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/claude_desktop.py +0 -0
  20. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/codex.py +0 -0
  21. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/gemini.py +0 -0
  22. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/adapters/registry.py +0 -0
  23. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/cli.py +0 -0
  24. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/__init__.py +0 -0
  25. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/analytics.py +0 -0
  26. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/compactor.py +0 -0
  27. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/db.py +0 -0
  28. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/injector.py +0 -0
  29. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/scanner.py +0 -0
  30. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/summarizer.py +0 -0
  31. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/core/token_analyzer.py +0 -0
  32. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/mcp_server.py +0 -0
  33. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/models/__init__.py +0 -0
  34. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/models/config.py +0 -0
  35. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/models/session.py +0 -0
  36. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/__init__.py +0 -0
  37. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/styles.tcss +0 -0
  38. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/__init__.py +0 -0
  39. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/session_table.py +0 -0
  40. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/stats_panel.py +0 -0
  41. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/status_bar.py +0 -0
  42. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/tui/widgets/transfer_panel.py +0 -0
  43. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/utils/__init__.py +0 -0
  44. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/utils/display.py +0 -0
  45. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/contextforge/utils/tokens.py +0 -0
  46. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/__init__.py +0 -0
  47. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/adapters/__init__.py +0 -0
  48. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/adapters/test_claude_code.py +0 -0
  49. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/conftest.py +0 -0
  50. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/__init__.py +0 -0
  51. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_compactor.py +0 -0
  52. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_db.py +0 -0
  53. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_injector.py +0 -0
  54. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_summarizer.py +0 -0
  55. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/core/test_token_analyzer.py +0 -0
  56. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/fixtures/claude_session_sample.jsonl +0 -0
  57. {context_forge_cli-0.4.7 → context_forge_cli-0.4.9}/tests/test_mcp_server.py +0 -0
  58. {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.7
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
@@ -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 Static("", id="detail-cwd")
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 Static("", id="detail-id")
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", Static).update(_meta_line("Project", f"[dim]{cwd}[/dim]"))
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", Static).update(_meta_line("ID", f"[dim]{session_id}[/dim]"))
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)
@@ -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 ScrollableContainer, Vertical
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 ScrollableContainer {
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
- with ScrollableContainer():
97
- yield Static("", id="turn-body")
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
- body = Text()
127
+ plain_parts = []
121
128
 
122
129
  if msg.content:
123
- body.append("── Content ──\n", style="bold dim")
124
- body.append(msg.content + "\n")
130
+ plain_parts.append("── Content ──\n")
131
+ plain_parts.append(msg.content + "\n")
125
132
 
126
133
  if msg.tool_calls:
127
- body.append("\n── Tool Calls ──\n", style="bold dim")
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
- body.append(f"⚙ {i}. {name}\n", style="bold yellow")
136
- body.append(formatted + "\n", style="dim")
142
+ plain_parts.append(f"⚙ {i}. {name}\n")
143
+ plain_parts.append(formatted + "\n")
137
144
 
138
145
  if msg.tool_results:
139
- body.append("\n── Tool Results ──\n", style="bold dim")
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
- body.append(f"⇥ Result {i}\n", style="bold green")
145
- body.append(output + "\n", style="dim")
151
+ plain_parts.append(f"⇥ Result {i}\n")
152
+ plain_parts.append(output + "\n")
146
153
 
147
- self.query_one("#turn-body", Static).update(body)
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 ───────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "context-forge-cli"
3
- version = "0.4.7"
3
+ version = "0.4.9"
4
4
  description = "Session manager and context bridge for agentic CLI tools"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }