context-forge-cli 0.1.0__py3-none-any.whl
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.1.0.dist-info/METADATA +340 -0
- context_forge_cli-0.1.0.dist-info/RECORD +37 -0
- context_forge_cli-0.1.0.dist-info/WHEEL +4 -0
- context_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
- contextforge/__init__.py +3 -0
- contextforge/adapters/__init__.py +3 -0
- contextforge/adapters/altimate_code.py +179 -0
- contextforge/adapters/base.py +44 -0
- contextforge/adapters/claude_code.py +393 -0
- contextforge/adapters/claude_desktop.py +306 -0
- contextforge/adapters/codex.py +220 -0
- contextforge/adapters/registry.py +31 -0
- contextforge/cli.py +523 -0
- contextforge/core/__init__.py +0 -0
- contextforge/core/analytics.py +226 -0
- contextforge/core/compactor.py +182 -0
- contextforge/core/db.py +181 -0
- contextforge/core/injector.py +94 -0
- contextforge/core/scanner.py +72 -0
- contextforge/core/summarizer.py +155 -0
- contextforge/core/token_analyzer.py +91 -0
- contextforge/models/__init__.py +4 -0
- contextforge/models/config.py +34 -0
- contextforge/models/session.py +37 -0
- contextforge/tui/__init__.py +0 -0
- contextforge/tui/app.py +204 -0
- contextforge/tui/styles.tcss +50 -0
- contextforge/tui/widgets/__init__.py +0 -0
- contextforge/tui/widgets/session_detail.py +191 -0
- contextforge/tui/widgets/session_table.py +277 -0
- contextforge/tui/widgets/stats_panel.py +340 -0
- contextforge/tui/widgets/status_bar.py +102 -0
- contextforge/tui/widgets/tokens_panel.py +134 -0
- contextforge/tui/widgets/transfer_panel.py +119 -0
- contextforge/utils/__init__.py +0 -0
- contextforge/utils/display.py +105 -0
- contextforge/utils/tokens.py +17 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: context-forge-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Session manager and context bridge for agentic CLI tools
|
|
5
|
+
Project-URL: Homepage, https://github.com/emmver/contextforge
|
|
6
|
+
Project-URL: Repository, https://github.com/emmver/contextforge
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/emmver/contextforge/issues
|
|
8
|
+
Author-email: emmver <emmvereroudakis@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai,claude,cli,codex,context,sessions,tui
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Requires-Dist: anthropic>=0.89.0
|
|
21
|
+
Requires-Dist: pydantic>=2.12.5
|
|
22
|
+
Requires-Dist: rich>=14.3.3
|
|
23
|
+
Requires-Dist: sqlite-utils>=3.39
|
|
24
|
+
Requires-Dist: textual>=8.2.2
|
|
25
|
+
Requires-Dist: tiktoken>=0.12.0
|
|
26
|
+
Requires-Dist: typer>=0.24.1
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ContextForge
|
|
30
|
+
|
|
31
|
+
**Session manager and context bridge for agentic CLI tools.**
|
|
32
|
+
|
|
33
|
+
ContextForge (`cf`) tracks sessions from Claude Code, Codex, altimate-code, and other
|
|
34
|
+
agentic CLI tools. It generates plain-English summaries of each session and can compact
|
|
35
|
+
and transfer context from one or more sessions into a new session — within the same tool
|
|
36
|
+
or across different tools.
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
- **Discovers sessions** from Claude Code (`~/.claude/projects/`), Codex (`~/.codex/state_5.sqlite`), and altimate-code (`~/.local/share/altimate-code/`)
|
|
41
|
+
- **Generates summaries** of what each session accomplished (with optional Claude API integration)
|
|
42
|
+
- **Compacts context** intelligently, reducing multi-turn conversations to a token-efficient ContextBundle
|
|
43
|
+
- **Transfers context** across tools and sessions (Claude Code → Codex, Codex → altimate-code, etc.)
|
|
44
|
+
- **Provides a TUI dashboard** to browse and manage sessions interactively
|
|
45
|
+
|
|
46
|
+
## Prerequisites
|
|
47
|
+
|
|
48
|
+
- **Python 3.13+** (ContextForge requires Python 3.13 or later)
|
|
49
|
+
- **uv** or **pipx** (for installation as a tool)
|
|
50
|
+
- At least one of: Claude Code, Codex, or altimate-code installed and with session history
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
### Using `uv` (recommended)
|
|
55
|
+
```bash
|
|
56
|
+
uv tool install context-forge-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Using `pipx`
|
|
60
|
+
```bash
|
|
61
|
+
pipx install context-forge-cli
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### From source (development)
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/emmver/contextforge.git
|
|
67
|
+
cd contextforge
|
|
68
|
+
uv sync
|
|
69
|
+
uv run cf --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Verify installation
|
|
73
|
+
```bash
|
|
74
|
+
cf --help # Should show the CLI with all commands
|
|
75
|
+
cf config # Show or edit configuration
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
### 1. Scan for sessions
|
|
81
|
+
```bash
|
|
82
|
+
cf scan # discover and index sessions from all installed tools
|
|
83
|
+
```
|
|
84
|
+
This reads tool-native storage and builds a local SQLite index at `~/.contextforge/contextforge.db`.
|
|
85
|
+
First run typically takes a few seconds depending on session count.
|
|
86
|
+
|
|
87
|
+
### 2. List and explore sessions
|
|
88
|
+
```bash
|
|
89
|
+
cf ls # list all recent sessions (with summaries if available)
|
|
90
|
+
cf ls --format json # machine-readable output
|
|
91
|
+
cf show <id> # show full detail + summary for a specific session
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. Generate summaries (optional)
|
|
95
|
+
```bash
|
|
96
|
+
cf summarize --all # generate summaries for all unsummarized sessions
|
|
97
|
+
cf summarize <id> # refresh summary for a single session
|
|
98
|
+
cf summarize --all --force # regenerate all summaries (e.g., after config change)
|
|
99
|
+
```
|
|
100
|
+
Summaries require an Anthropic API key (see [Configuration](#configuration) below).
|
|
101
|
+
|
|
102
|
+
### 4. Launch the dashboard
|
|
103
|
+
```bash
|
|
104
|
+
cf dashboard # interactive TUI with live session browser
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 5. Transfer context to a new session
|
|
108
|
+
```bash
|
|
109
|
+
# Preview the context bundle
|
|
110
|
+
cf compact <session-id>
|
|
111
|
+
|
|
112
|
+
# Inject context into a new Claude Code session
|
|
113
|
+
cf transfer <session-id> --to claude_code --execute
|
|
114
|
+
|
|
115
|
+
# Inject into Codex with richer context
|
|
116
|
+
cf transfer <session-id> --to codex --strategy key_messages --execute
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Context Transfer
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Preview: compact a session into a ContextBundle and display it
|
|
123
|
+
cf compact <session-id>
|
|
124
|
+
|
|
125
|
+
# Save a bundle to the DB for later reuse
|
|
126
|
+
cf compact <session-id> --save
|
|
127
|
+
|
|
128
|
+
# Preview: show the exact command that would inject context into a new Codex session
|
|
129
|
+
cf transfer <session-id> --to codex
|
|
130
|
+
|
|
131
|
+
# Execute: launch a new Codex session with context injected
|
|
132
|
+
cf transfer <session-id> --to codex --execute
|
|
133
|
+
|
|
134
|
+
# Multi-session bundle → altimate-code, richer key_messages strategy
|
|
135
|
+
cf transfer <id1> <id2> --to altimate-code --strategy key_messages --execute
|
|
136
|
+
|
|
137
|
+
# Resume an existing session with injected context
|
|
138
|
+
cf transfer <id> --to claude_code --session <existing-session-id> --execute
|
|
139
|
+
|
|
140
|
+
# Machine-readable output
|
|
141
|
+
cf transfer <id> --to codex --format json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Injection methods
|
|
145
|
+
|
|
146
|
+
| Method | When used | How |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `system_prompt` | New session, small context (≤4k tokens) | Passed via `--system-prompt` flag |
|
|
149
|
+
| `resume` | Continuing an existing session | `--resume` / `resume` / `run -s` |
|
|
150
|
+
| `fork` | Branch from existing session | `fork` / `--fork` |
|
|
151
|
+
| `file` | Any context >4k tokens | Writes `CONTEXT.md` to target dir; system prompt references it |
|
|
152
|
+
|
|
153
|
+
The method is chosen automatically based on token count and whether a target session is specified. Override with `--method`.
|
|
154
|
+
|
|
155
|
+
## Compaction Strategies
|
|
156
|
+
|
|
157
|
+
| Strategy | Tokens | Best for |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `summary_only` | ~100–300 per session | Default; maximum token efficiency |
|
|
160
|
+
| `key_messages` | 1k–8k | Richer context; scores messages by importance |
|
|
161
|
+
| `full_recent` | Up to budget | Same-tool transfers; preserves recent conversation |
|
|
162
|
+
|
|
163
|
+
## Supported Tools
|
|
164
|
+
|
|
165
|
+
| Tool | Discovery | Inject method |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| Claude Code (CLI) | `~/.claude/projects/` JSONL | `--system-prompt` / `--resume` |
|
|
168
|
+
| Claude Desktop | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | `--system-prompt` |
|
|
169
|
+
| Codex | `~/.codex/state_5.sqlite` | `resume` / `fork` |
|
|
170
|
+
| altimate-code | `~/.local/share/altimate-code/opencode.db` | `run -s` / `import` |
|
|
171
|
+
|
|
172
|
+
## Session Summaries
|
|
173
|
+
|
|
174
|
+
ContextForge generates plain-English summaries of what each session accomplished.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cf summarize <id> # generate or refresh summary for one session
|
|
178
|
+
cf summarize --all # bulk-generate summaries for all sessions
|
|
179
|
+
cf summarize --all --force # regenerate even existing ones
|
|
180
|
+
|
|
181
|
+
# Scan + summarize in one step
|
|
182
|
+
cf scan --summarize
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**How summaries work:**
|
|
186
|
+
1. **Codex** — reuses pre-computed `stage1_outputs.rollout_summary` when available (zero LLM cost)
|
|
187
|
+
2. **All tools** — calls the Claude API (Haiku model) to produce a 3–5 sentence summary
|
|
188
|
+
3. **No API key** — falls back to showing the first user message as a preview
|
|
189
|
+
|
|
190
|
+
Summaries are cached in the local SQLite index and shown in `cf ls` and `cf show`.
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
Config file: `~/.contextforge/config.toml` (created on first run)
|
|
195
|
+
|
|
196
|
+
### Manage config from CLI
|
|
197
|
+
```bash
|
|
198
|
+
cf config show # display current config
|
|
199
|
+
cf config set llm.api_key sk-ant-...
|
|
200
|
+
cf config set compactor.default_strategy key_messages
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Full reference
|
|
204
|
+
```toml
|
|
205
|
+
[llm]
|
|
206
|
+
api_key = "sk-ant-..." # optional; enables LLM summarization
|
|
207
|
+
model = "claude-haiku-4-5-20251001" # or any Claude model
|
|
208
|
+
|
|
209
|
+
[compactor]
|
|
210
|
+
default_strategy = "summary_only" # summary_only | key_messages | full_recent
|
|
211
|
+
default_token_budget = 4096 # max tokens per compacted session
|
|
212
|
+
|
|
213
|
+
[scanner]
|
|
214
|
+
max_sessions_per_tool = 200 # limit sessions indexed per tool
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Notes on configuration
|
|
218
|
+
|
|
219
|
+
- **Without API key**: Summaries fall back to showing the first user message (no LLM cost)
|
|
220
|
+
- **With API key**: Summaries use Claude Haiku via the Anthropic API (~0.30¢ per summary)
|
|
221
|
+
- **Token budget**: Controls how aggressively context is compacted; higher = richer context
|
|
222
|
+
- **Strategies**: See [Compaction Strategies](#compaction-strategies) for details on each
|
|
223
|
+
|
|
224
|
+
## Storage
|
|
225
|
+
|
|
226
|
+
ContextForge stores its index at `~/.contextforge/contextforge.db` (SQLite).
|
|
227
|
+
It **never modifies** tool-native storage. All source files are read-only.
|
|
228
|
+
|
|
229
|
+
## Agent / Machine Usage
|
|
230
|
+
|
|
231
|
+
All structured commands support `--format json`:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
cf ls --format json
|
|
235
|
+
cf show <id> --format json
|
|
236
|
+
cf compact <id> --format json
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
See [AGENTS.md](AGENTS.md) for contribution guidelines and [PLAN.md](PLAN.md) for development status.
|
|
240
|
+
|
|
241
|
+
## TUI Dashboard
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
cf dashboard
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A full-screen Textual dashboard with live session browsing, filtering, and analytics.
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
┌ Sessions ────────────────┬ Detail ─────────────────────────┐
|
|
251
|
+
│ 🔵 CC utastar 1.2M │ utastar_thesis │
|
|
252
|
+
│ 🟢 Codex dataset 45k │ │
|
|
253
|
+
│ 🟣 Alt BigQuery 12k │ Tool Claude Code │
|
|
254
|
+
│ 🔵 CC jira-mcp 890k │ CWD ~/Github/utastar_... │
|
|
255
|
+
│ ... │ Tokens 1.2M │
|
|
256
|
+
│ │ Created 2026-03-10 09:14 UTC │
|
|
257
|
+
│ │ Updated 2026-04-03 22:41 UTC │
|
|
258
|
+
│ │ │
|
|
259
|
+
│ │ ── Summary ── │
|
|
260
|
+
│ │ │
|
|
261
|
+
│ │ Worked on UTASTAR ensemble │
|
|
262
|
+
│ │ pruning framework... │
|
|
263
|
+
└──────────────────────────┴─────────────────────────────────┘
|
|
264
|
+
Sessions — CC:12 │ Codex:4 │ Alt:3 │ Total:19 │ 129M tok 14:32:01
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Key bindings
|
|
268
|
+
|
|
269
|
+
| Key | Action |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `a` | Analytics dashboard (tool charts, activity sparkline, top projects) |
|
|
272
|
+
| `x` | Per-turn token breakdown for selected session |
|
|
273
|
+
| `/` | Toggle live filter bar (text search + tool buttons) |
|
|
274
|
+
| `r` | Rescan all tools |
|
|
275
|
+
| `s` | Summarize selected session |
|
|
276
|
+
| `t` | Open transfer modal (choose tool + strategy) |
|
|
277
|
+
| `c` | Compact selected session and preview bundle |
|
|
278
|
+
| `q` | Quit |
|
|
279
|
+
| `↑↓` | Navigate sessions |
|
|
280
|
+
|
|
281
|
+
### Analytics modal (`a`)
|
|
282
|
+
|
|
283
|
+
Shows aggregate stats with configurable time windows:
|
|
284
|
+
|
|
285
|
+
- **W** = last 7 days, **M** = 30 days, **H** = 6 months, **Y** = 1 year
|
|
286
|
+
- Sessions and token usage per tool (unicode bar charts)
|
|
287
|
+
- Activity sparkline over time
|
|
288
|
+
- Top 5 projects by session count
|
|
289
|
+
|
|
290
|
+
### Live filter (`/`)
|
|
291
|
+
|
|
292
|
+
Press `/` to open the filter bar above the session list:
|
|
293
|
+
- Type to filter by title or project path (instant, no DB re-query)
|
|
294
|
+
- Click **All / CC / Codex / Alt** to filter by tool
|
|
295
|
+
- Combine text + tool filters; match count shown in the status bar
|
|
296
|
+
- Press `ESC` to clear and close
|
|
297
|
+
|
|
298
|
+
### Transfer modal (`t`)
|
|
299
|
+
|
|
300
|
+
Press `t` on any session to open the transfer panel. Choose:
|
|
301
|
+
- **Target tool** — Claude Code, Codex, or altimate-code
|
|
302
|
+
- **Strategy** — `summary_only`, `key_messages`, or `full_recent`
|
|
303
|
+
- **Preview** — shows the exact shell command (no side effects)
|
|
304
|
+
- **Execute** — builds the bundle and launches the target tool
|
|
305
|
+
|
|
306
|
+
## Troubleshooting
|
|
307
|
+
|
|
308
|
+
### `command not found: cf`
|
|
309
|
+
- **Cause**: Tool not in PATH after installation
|
|
310
|
+
- **Fix**: Restart your shell, or reinstall: `uv tool install contextforge --force`
|
|
311
|
+
|
|
312
|
+
### `RuntimeError: no sessions found` or empty `cf ls`
|
|
313
|
+
- **Cause**: No sessions discovered from installed tools
|
|
314
|
+
- **Fix**: Run `cf scan` first; check that you have Claude Code/Codex/altimate-code with session history
|
|
315
|
+
|
|
316
|
+
### `ANTHROPIC_API_KEY not set` warning
|
|
317
|
+
- **Cause**: No API key configured for summaries
|
|
318
|
+
- **Fix**: Set it via `cf config set llm.api_key sk-ant-...` or set `ANTHROPIC_API_KEY` env var
|
|
319
|
+
- **Note**: Summaries are optional; you can still use ContextForge without API keys
|
|
320
|
+
|
|
321
|
+
### Session not appearing after tool update
|
|
322
|
+
- **Cause**: Tool storage location changed or was not yet indexed
|
|
323
|
+
- **Fix**: Run `cf scan` again to reindex all tools
|
|
324
|
+
|
|
325
|
+
### `cf dashboard` crashes or displays incorrectly
|
|
326
|
+
- **Cause**: Terminal size too small or unsupported terminal type
|
|
327
|
+
- **Fix**: Resize terminal to at least 80x24; try a different terminal emulator
|
|
328
|
+
|
|
329
|
+
### Database locked / concurrent access error
|
|
330
|
+
- **Cause**: Two `cf` commands running at once
|
|
331
|
+
- **Fix**: Wait for the first command to finish, or delete `~/.contextforge/contextforge.db` and run `cf scan` again
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
uv sync # install all dependencies including dev
|
|
337
|
+
uv run pytest # run tests
|
|
338
|
+
uv run cf --help # run CLI locally
|
|
339
|
+
uv tool install . --force # test local installation
|
|
340
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
contextforge/__init__.py,sha256=L5NnW7jpnDQrAhnqTWSzW-bk2Bp8TXpFK1-SmQXI2QE,104
|
|
2
|
+
contextforge/cli.py,sha256=OIHjV_pL4fKCvnn-2bQwza8NOUplptSfASPg4yzQmjA,18685
|
|
3
|
+
contextforge/adapters/__init__.py,sha256=HGIFdC_tNpX_QOPrePwU5cEVBhp3Hsh6Sihwb23HdTk,171
|
|
4
|
+
contextforge/adapters/altimate_code.py,sha256=aFo3FvO6XdN8eORnU_F8PbEB7rTTB2quIyU381AHE1E,5967
|
|
5
|
+
contextforge/adapters/base.py,sha256=q3_pn0XoDV_r2GkuSzuRof0SSqynHqiaCAe4piq3tGs,1355
|
|
6
|
+
contextforge/adapters/claude_code.py,sha256=RcFucjQGuDztU_BFVdm-O7mQ12FHFSD6y9AUrYJg4X4,15315
|
|
7
|
+
contextforge/adapters/claude_desktop.py,sha256=uXJgMt2BdbhkwU9DMXhik--kb-RNKqxTdMc4Z9O-9lc,11957
|
|
8
|
+
contextforge/adapters/codex.py,sha256=WhLHOyZZM4KI-G2hEvsqq9PYOjuCyI2Zo8S9ircZtf8,8005
|
|
9
|
+
contextforge/adapters/registry.py,sha256=u0R7SwEHwCwEZBSSDv8zg-49OqRM2Gc7vRvL4kWd9iE,1093
|
|
10
|
+
contextforge/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
contextforge/core/analytics.py,sha256=qffm7oqfmosU6XPUftbf68AOvDrylrxBR4rJyLhvoXU,7056
|
|
12
|
+
contextforge/core/compactor.py,sha256=ub1eU31fNLXl9_-hLW_YeoHT9JTyIytB2Nh4HlmU7hA,5482
|
|
13
|
+
contextforge/core/db.py,sha256=1d8HQZEwiEvotByCWWgDyOPJT744lh6vb77XURjscco,5205
|
|
14
|
+
contextforge/core/injector.py,sha256=_4UeJn-hJfs2-cZcqScpJjEBZCOnKc7odcKDYZTj55I,2670
|
|
15
|
+
contextforge/core/scanner.py,sha256=Wbgp4TJ7SmFf-f6ZY6aKlmL_wUTUCFhuMTEjYYPPUhQ,2487
|
|
16
|
+
contextforge/core/summarizer.py,sha256=4qPzBHfLno1C_nyiTGM_It_0vm3alcsYCZKueMQITlA,4631
|
|
17
|
+
contextforge/core/token_analyzer.py,sha256=HKktdSye1Jy8XJ_LktLfVBg2ElYTsaEy9mZb_1vDnTw,2398
|
|
18
|
+
contextforge/models/__init__.py,sha256=abmJPhl_AVrhA526IYpF7NqWTy8MBH5jVSAxwOLzuL4,151
|
|
19
|
+
contextforge/models/config.py,sha256=TbqPaOV_oF3kvVFJDGIADRuJid-9UsGq2QQyb8BP_-g,953
|
|
20
|
+
contextforge/models/session.py,sha256=Q3bT3j6r4LdKgV0Yq7DH1RUJPIu1KF6cyJG3iR7qPss,878
|
|
21
|
+
contextforge/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
contextforge/tui/app.py,sha256=-3ETEJjXAsigVvvB65wpXMBt4L4NiqM4Nj8cS1dBFQ8,7774
|
|
23
|
+
contextforge/tui/styles.tcss,sha256=hiSe3g2OJFME8QAHVHbtvgaB8_BNYfwKvQUmZzbWHHc,945
|
|
24
|
+
contextforge/tui/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
contextforge/tui/widgets/session_detail.py,sha256=xjnbD67Es7du7S3q960rZR31erwkNjX-KfS0QTFNZLw,6567
|
|
26
|
+
contextforge/tui/widgets/session_table.py,sha256=s-E1j629eY5MgO3IaxOWs03YP6oToaLBXur27h4EV80,9782
|
|
27
|
+
contextforge/tui/widgets/stats_panel.py,sha256=N5rPdrI0_JNsJaggZ30y6eVNGX_eRbHVT40dOm61FiY,12277
|
|
28
|
+
contextforge/tui/widgets/status_bar.py,sha256=uAtgZWA8ah8CynoaEgKqhGYH7MHq6Ih-Nw3_go3-Dkg,3068
|
|
29
|
+
contextforge/tui/widgets/tokens_panel.py,sha256=xsLB-by3ta1zKts9lGSUConAKMHcpVDOyq6BD-utTQk,4669
|
|
30
|
+
contextforge/tui/widgets/transfer_panel.py,sha256=Qrgkt1-MpzRHWuwNyu2ErM26IRl0yaAMdbx3XMS7Lqk,4145
|
|
31
|
+
contextforge/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
contextforge/utils/display.py,sha256=bagZgpZ6WGki2THNHA8lfg9ITqM8W0q_M6Os-pgwFHk,3111
|
|
33
|
+
contextforge/utils/tokens.py,sha256=UVGz-vvmCIxMgnRPn1T53v2ZdKMmxKhoJcc1qbmjpDQ,414
|
|
34
|
+
context_forge_cli-0.1.0.dist-info/METADATA,sha256=_3IFKAuY9oSBaEqZXk7-tYOCBBOwWWNIkjOE_TxDCHQ,12600
|
|
35
|
+
context_forge_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
36
|
+
context_forge_cli-0.1.0.dist-info/entry_points.txt,sha256=nPN0PWb85KCPXqFp4opmBsNejdCHyl7rK_cREsu4r7A,44
|
|
37
|
+
context_forge_cli-0.1.0.dist-info/RECORD,,
|
contextforge/__init__.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Adapter for altimate-code (opencode) sessions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import shlex
|
|
6
|
+
import sqlite3
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import tiktoken
|
|
11
|
+
|
|
12
|
+
from contextforge.adapters.base import ToolAdapter
|
|
13
|
+
from contextforge.models.session import Message, Session
|
|
14
|
+
|
|
15
|
+
_OPENCODE_DB = Path.home() / ".local" / "share" / "altimate-code" / "opencode.db"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _count_tokens(text: str) -> int:
|
|
19
|
+
"""Count tokens in text using tiktoken for Claude models."""
|
|
20
|
+
try:
|
|
21
|
+
enc = tiktoken.encoding_for_model("claude-3-5-sonnet-20241022")
|
|
22
|
+
return len(enc.encode(text))
|
|
23
|
+
except Exception:
|
|
24
|
+
# Fallback: rough estimate (~4 chars per token)
|
|
25
|
+
return len(text) // 4
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AltimateCodeAdapter(ToolAdapter):
|
|
29
|
+
tool_name = "altimate_code"
|
|
30
|
+
default_paths = [_OPENCODE_DB]
|
|
31
|
+
|
|
32
|
+
def discover_sessions(self) -> list[Session]:
|
|
33
|
+
if not _OPENCODE_DB.exists():
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
sessions: list[Session] = []
|
|
37
|
+
try:
|
|
38
|
+
conn = sqlite3.connect(str(_OPENCODE_DB))
|
|
39
|
+
conn.row_factory = sqlite3.Row
|
|
40
|
+
cur = conn.cursor()
|
|
41
|
+
cur.execute(
|
|
42
|
+
"""
|
|
43
|
+
SELECT id, title, directory, time_created, time_updated
|
|
44
|
+
FROM session
|
|
45
|
+
ORDER BY time_updated DESC
|
|
46
|
+
"""
|
|
47
|
+
)
|
|
48
|
+
rows = cur.fetchall()
|
|
49
|
+
conn.close()
|
|
50
|
+
except sqlite3.Error:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
for row in rows:
|
|
54
|
+
try:
|
|
55
|
+
created = datetime.fromtimestamp(row["time_created"] / 1000, tz=timezone.utc)
|
|
56
|
+
updated = datetime.fromtimestamp(row["time_updated"] / 1000, tz=timezone.utc)
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
now = datetime.now(timezone.utc)
|
|
59
|
+
created = updated = now
|
|
60
|
+
|
|
61
|
+
# Calculate token count from session messages
|
|
62
|
+
session_id = str(row["id"])
|
|
63
|
+
token_count = self._count_session_tokens(session_id)
|
|
64
|
+
|
|
65
|
+
sessions.append(
|
|
66
|
+
Session(
|
|
67
|
+
id=session_id,
|
|
68
|
+
tool=self.tool_name,
|
|
69
|
+
title=row["title"] or None,
|
|
70
|
+
cwd=row["directory"] or None,
|
|
71
|
+
created_at=created,
|
|
72
|
+
updated_at=updated,
|
|
73
|
+
raw_path=str(_OPENCODE_DB),
|
|
74
|
+
status="unknown",
|
|
75
|
+
token_count=token_count,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return sessions
|
|
80
|
+
|
|
81
|
+
def load_messages(self, session_id: str) -> list[Message]:
|
|
82
|
+
if not _OPENCODE_DB.exists():
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
conn = sqlite3.connect(str(_OPENCODE_DB))
|
|
87
|
+
conn.row_factory = sqlite3.Row
|
|
88
|
+
cur = conn.cursor()
|
|
89
|
+
# Join message + part to get full content
|
|
90
|
+
cur.execute(
|
|
91
|
+
"""
|
|
92
|
+
SELECT m.role, p.type as part_type, p.content, m.time as ts
|
|
93
|
+
FROM message m
|
|
94
|
+
LEFT JOIN part p ON p.message_id = m.id
|
|
95
|
+
WHERE m.session_id = ?
|
|
96
|
+
ORDER BY m.time ASC, p.id ASC
|
|
97
|
+
""",
|
|
98
|
+
(session_id,),
|
|
99
|
+
)
|
|
100
|
+
rows = cur.fetchall()
|
|
101
|
+
conn.close()
|
|
102
|
+
except sqlite3.Error:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
messages: list[Message] = []
|
|
106
|
+
current_role: str | None = None
|
|
107
|
+
current_parts: list[str] = []
|
|
108
|
+
current_ts: datetime | None = None
|
|
109
|
+
|
|
110
|
+
def flush():
|
|
111
|
+
nonlocal current_role, current_parts, current_ts
|
|
112
|
+
if current_role and current_parts:
|
|
113
|
+
content = "\n".join(p for p in current_parts if p)
|
|
114
|
+
if content:
|
|
115
|
+
messages.append(
|
|
116
|
+
Message(
|
|
117
|
+
role=current_role,
|
|
118
|
+
content=content,
|
|
119
|
+
timestamp=current_ts,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
current_role = None
|
|
123
|
+
current_parts = []
|
|
124
|
+
current_ts = None
|
|
125
|
+
|
|
126
|
+
for row in rows:
|
|
127
|
+
role = row["role"]
|
|
128
|
+
if role not in ("user", "assistant"):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if role != current_role:
|
|
132
|
+
flush()
|
|
133
|
+
current_role = role
|
|
134
|
+
try:
|
|
135
|
+
current_ts = datetime.fromtimestamp(row["ts"] / 1000, tz=timezone.utc)
|
|
136
|
+
except (TypeError, ValueError):
|
|
137
|
+
current_ts = None
|
|
138
|
+
|
|
139
|
+
part_type = row["part_type"] or ""
|
|
140
|
+
content = row["content"] or ""
|
|
141
|
+
|
|
142
|
+
if part_type in ("text", "reasoning") and content:
|
|
143
|
+
if isinstance(content, str):
|
|
144
|
+
try:
|
|
145
|
+
parsed = json.loads(content)
|
|
146
|
+
if isinstance(parsed, dict):
|
|
147
|
+
content = parsed.get("text", str(parsed))
|
|
148
|
+
except (json.JSONDecodeError, TypeError):
|
|
149
|
+
pass
|
|
150
|
+
current_parts.append(str(content).strip())
|
|
151
|
+
|
|
152
|
+
flush()
|
|
153
|
+
return messages
|
|
154
|
+
|
|
155
|
+
def _count_session_tokens(self, session_id: str) -> int:
|
|
156
|
+
"""Count total tokens in a session."""
|
|
157
|
+
messages = self.load_messages(session_id)
|
|
158
|
+
total = 0
|
|
159
|
+
for msg in messages:
|
|
160
|
+
total += _count_tokens(msg.content)
|
|
161
|
+
return total
|
|
162
|
+
|
|
163
|
+
def build_inject_command(
|
|
164
|
+
self,
|
|
165
|
+
context: str,
|
|
166
|
+
target_session_id: str | None = None,
|
|
167
|
+
cwd: str | None = None,
|
|
168
|
+
method: str = "system_prompt",
|
|
169
|
+
) -> str:
|
|
170
|
+
safe_ctx = shlex.quote(context)
|
|
171
|
+
if method == "resume" and target_session_id:
|
|
172
|
+
return f"altimate-code run -s {target_session_id} {safe_ctx}"
|
|
173
|
+
if method == "fork" and target_session_id:
|
|
174
|
+
return f"altimate-code run --fork -s {target_session_id} {safe_ctx}"
|
|
175
|
+
# New session
|
|
176
|
+
cmd = f"altimate-code run {safe_ctx}"
|
|
177
|
+
if cwd:
|
|
178
|
+
cmd = f"cd {shlex.quote(cwd)} && {cmd}"
|
|
179
|
+
return cmd
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Abstract base class for all tool adapters."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from contextforge.models.session import Message, Session
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolAdapter(ABC):
|
|
11
|
+
tool_name: str
|
|
12
|
+
default_paths: list[Path]
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def discover_sessions(self) -> list[Session]:
|
|
16
|
+
"""Return a list of sessions discovered for this tool."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def load_messages(self, session_id: str) -> list[Message]:
|
|
21
|
+
"""Return the full message list for a given session ID."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def build_inject_command(
|
|
26
|
+
self,
|
|
27
|
+
context: str,
|
|
28
|
+
target_session_id: str | None = None,
|
|
29
|
+
cwd: str | None = None,
|
|
30
|
+
method: str = "system_prompt",
|
|
31
|
+
) -> str:
|
|
32
|
+
"""Return the shell command to start/continue a session with context injected.
|
|
33
|
+
|
|
34
|
+
This method MUST NOT execute anything — it only returns the command string.
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def get_rollup_summary(self, session_id: str) -> str | None:
|
|
39
|
+
"""Return a pre-computed summary if the tool stores one natively, else None."""
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def is_available(self) -> bool:
|
|
43
|
+
"""Return True if this tool is installed and its data paths exist."""
|
|
44
|
+
return any(p.exists() for p in self.default_paths)
|