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.
Files changed (37) hide show
  1. context_forge_cli-0.1.0.dist-info/METADATA +340 -0
  2. context_forge_cli-0.1.0.dist-info/RECORD +37 -0
  3. context_forge_cli-0.1.0.dist-info/WHEEL +4 -0
  4. context_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. contextforge/__init__.py +3 -0
  6. contextforge/adapters/__init__.py +3 -0
  7. contextforge/adapters/altimate_code.py +179 -0
  8. contextforge/adapters/base.py +44 -0
  9. contextforge/adapters/claude_code.py +393 -0
  10. contextforge/adapters/claude_desktop.py +306 -0
  11. contextforge/adapters/codex.py +220 -0
  12. contextforge/adapters/registry.py +31 -0
  13. contextforge/cli.py +523 -0
  14. contextforge/core/__init__.py +0 -0
  15. contextforge/core/analytics.py +226 -0
  16. contextforge/core/compactor.py +182 -0
  17. contextforge/core/db.py +181 -0
  18. contextforge/core/injector.py +94 -0
  19. contextforge/core/scanner.py +72 -0
  20. contextforge/core/summarizer.py +155 -0
  21. contextforge/core/token_analyzer.py +91 -0
  22. contextforge/models/__init__.py +4 -0
  23. contextforge/models/config.py +34 -0
  24. contextforge/models/session.py +37 -0
  25. contextforge/tui/__init__.py +0 -0
  26. contextforge/tui/app.py +204 -0
  27. contextforge/tui/styles.tcss +50 -0
  28. contextforge/tui/widgets/__init__.py +0 -0
  29. contextforge/tui/widgets/session_detail.py +191 -0
  30. contextforge/tui/widgets/session_table.py +277 -0
  31. contextforge/tui/widgets/stats_panel.py +340 -0
  32. contextforge/tui/widgets/status_bar.py +102 -0
  33. contextforge/tui/widgets/tokens_panel.py +134 -0
  34. contextforge/tui/widgets/transfer_panel.py +119 -0
  35. contextforge/utils/__init__.py +0 -0
  36. contextforge/utils/display.py +105 -0
  37. 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cf = contextforge.cli:app
@@ -0,0 +1,3 @@
1
+ """ContextForge — session manager and context bridge for agentic CLI tools."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .registry import get_adapter, get_all_adapters, get_available_adapters, ADAPTERS
2
+
3
+ __all__ = ["get_adapter", "get_all_adapters", "get_available_adapters", "ADAPTERS"]
@@ -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)