cram-ai 0.1.0__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.
- cram_ai-0.1.0/PKG-INFO +228 -0
- cram_ai-0.1.0/README.md +190 -0
- cram_ai-0.1.0/cram/__init__.py +1 -0
- cram_ai-0.1.0/cram/__main__.py +2 -0
- cram_ai-0.1.0/cram/add_context.py +143 -0
- cram_ai-0.1.0/cram/autostart.py +115 -0
- cram_ai-0.1.0/cram/benchmark.py +196 -0
- cram_ai-0.1.0/cram/cli.py +77 -0
- cram_ai-0.1.0/cram/decide.py +73 -0
- cram_ai-0.1.0/cram/doctor.py +144 -0
- cram_ai-0.1.0/cram/find_context.py +374 -0
- cram_ai-0.1.0/cram/hooks.py +192 -0
- cram_ai-0.1.0/cram/init.py +217 -0
- cram_ai-0.1.0/cram/mcp_server.py +262 -0
- cram_ai-0.1.0/cram/menubar.py +227 -0
- cram_ai-0.1.0/cram/session.py +93 -0
- cram_ai-0.1.0/cram/status.py +135 -0
- cram_ai-0.1.0/cram/suggest.py +77 -0
- cram_ai-0.1.0/cram/symbols.py +105 -0
- cram_ai-0.1.0/cram/sync_context.py +159 -0
- cram_ai-0.1.0/cram/targets.py +179 -0
- cram_ai-0.1.0/cram/tray.py +353 -0
- cram_ai-0.1.0/cram/tray_server.py +538 -0
- cram_ai-0.1.0/cram/tray_ui/popup.css +800 -0
- cram_ai-0.1.0/cram/tray_ui/popup.html +160 -0
- cram_ai-0.1.0/cram/tray_ui/popup.js +644 -0
- cram_ai-0.1.0/cram/utils.py +363 -0
- cram_ai-0.1.0/cram/vscode.py +131 -0
- cram_ai-0.1.0/cram_ai.egg-info/PKG-INFO +228 -0
- cram_ai-0.1.0/cram_ai.egg-info/SOURCES.txt +39 -0
- cram_ai-0.1.0/cram_ai.egg-info/dependency_links.txt +1 -0
- cram_ai-0.1.0/cram_ai.egg-info/entry_points.txt +4 -0
- cram_ai-0.1.0/cram_ai.egg-info/requires.txt +22 -0
- cram_ai-0.1.0/cram_ai.egg-info/top_level.txt +1 -0
- cram_ai-0.1.0/pyproject.toml +52 -0
- cram_ai-0.1.0/setup.cfg +4 -0
- cram_ai-0.1.0/tests/test_find_context.py +205 -0
- cram_ai-0.1.0/tests/test_init.py +175 -0
- cram_ai-0.1.0/tests/test_sync.py +127 -0
- cram_ai-0.1.0/tests/test_targets.py +109 -0
- cram_ai-0.1.0/tests/test_utils.py +107 -0
cram_ai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cram-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cut AI coding token costs by 96-98% — curated context, MCP server, and tray app for AI coding tools
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/vishbay/cram-ai
|
|
7
|
+
Project-URL: Repository, https://github.com/vishbay/cram-ai
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/vishbay/cram-ai/issues
|
|
9
|
+
Keywords: ai,llm,context,token,coding,claude,cursor,copilot
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: anthropic>=0.40.0
|
|
24
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
25
|
+
Provides-Extra: multi-provider
|
|
26
|
+
Requires-Dist: litellm>=1.40.0; extra == "multi-provider"
|
|
27
|
+
Provides-Extra: mac
|
|
28
|
+
Requires-Dist: rumps>=0.4.0; extra == "mac"
|
|
29
|
+
Provides-Extra: tray
|
|
30
|
+
Requires-Dist: pystray>=0.19.0; extra == "tray"
|
|
31
|
+
Requires-Dist: pillow>=10.0; extra == "tray"
|
|
32
|
+
Requires-Dist: pywebview>=5.0; extra == "tray"
|
|
33
|
+
Requires-Dist: flask>=3.0; extra == "tray"
|
|
34
|
+
Provides-Extra: mcp
|
|
35
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
38
|
+
|
|
39
|
+
# cram-ai
|
|
40
|
+
|
|
41
|
+
> Slash AI coding token costs by injecting only what the model needs — nothing more.
|
|
42
|
+
|
|
43
|
+
AI coding tools auto-index your entire repo at session start. That indexing generates **cache writes** — the most expensive token type (3–4× the cost of reads). cram-ai replaces auto-indexing with a small set of curated files that give the model exactly what it needs: repo structure, key decisions, and focused excerpts of only the files relevant to your current task.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Benchmarks
|
|
48
|
+
|
|
49
|
+
### cram-ai itself (31 source files, Python CLI tool)
|
|
50
|
+
|
|
51
|
+
| | Tokens | Sonnet cost/session | Opus cost/session |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| **Without cram** — full repo auto-indexed | 49,683 | $0.186 | $0.932 |
|
|
54
|
+
| **Without cram** — orientation set only¹ | 19,687 | $0.074 | $0.369 |
|
|
55
|
+
| **With cram** — ARCHITECTURE + SYMBOLS + task context | 1,898 | $0.007 | $0.036 |
|
|
56
|
+
|
|
57
|
+
**96% token reduction. $0.18 saved per session. $18 over 100 sessions (Sonnet).**
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### pallets/flask (118 source files, Python web framework)
|
|
62
|
+
|
|
63
|
+
| | Tokens | Sonnet cost/session | Opus cost/session |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| **Without cram** — full repo auto-indexed | 171,641 | $0.644 | $3.22 |
|
|
66
|
+
| **Without cram** — orientation set only¹ | 60,863 | $0.228 | $1.14 |
|
|
67
|
+
| **With cram** — ARCHITECTURE + SYMBOLS + task context | 5,929 | $0.022 | $0.111 |
|
|
68
|
+
|
|
69
|
+
**96.5% token reduction. $0.62 saved per session. $62 over 100 sessions (Sonnet).**
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### hoppscotch/hoppscotch (2,151 source files, TypeScript monorepo)
|
|
74
|
+
|
|
75
|
+
| | Tokens | Sonnet cost/session |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| **Without cram** — full repo auto-indexed | 418,697 | $1.57 |
|
|
78
|
+
| **With cram** | 7,239 | $0.027 |
|
|
79
|
+
|
|
80
|
+
**98.3% reduction. $154 saved over 100 sessions (Sonnet).**
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
> ¹ *Orientation set = file tree + README + pyproject.toml/package.json + 5 largest source files. A realistic estimate for tools that don't index everything.*
|
|
85
|
+
> Pricing: Claude Sonnet 4.6 cache write $3.75/M, Opus 4.8 $18.75/M. Savings scale with team size and session frequency.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
AI agents spend most tokens on **orientation** — finding relevant files, understanding structure, reading configs. cram-ai replaces that with a curated map the model reads instead of building itself.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
your-repo/
|
|
95
|
+
└── .cram-ai-context/
|
|
96
|
+
├── ARCHITECTURE.md ← repo structure, tech stack, key files (auto-generated by Haiku)
|
|
97
|
+
├── DECISIONS.md ← architectural decisions you want the AI to respect
|
|
98
|
+
├── SYMBOLS.md ← public function/class index across all source files (auto-generated)
|
|
99
|
+
└── CURRENT_TASK.md ← per-session: task + focused excerpts of relevant files
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**`SYMBOLS.md`** is the key accuracy improvement. Rather than asking a model to guess which files matter based on filenames alone, cram maps every source file to its public identifiers (`api/routes.py: handle_rate_limit, check_throttle, apply_backoff`). The model uses that map to select files *and* identify the exact functions to excerpt — so "fix the rate limiter" finds `check_throttle` even if the words don't match.
|
|
103
|
+
|
|
104
|
+
`cram task "..."` runs before every session:
|
|
105
|
+
|
|
106
|
+
1. **`[1/4]`** Loads `SYMBOLS.md` — 455 identifiers across 65 files, zero LLM calls
|
|
107
|
+
2. **`[2/4]`** Sends architecture + symbol index to Haiku → model returns `path | RelevantFunc, OtherClass`
|
|
108
|
+
3. **`[3/4]`** Extracts identifier-focused excerpts — only the lines that contain those functions, plus context window
|
|
109
|
+
4. **`[4/4]`** Writes to your tool's instruction file, warns if below cache minimum for your model
|
|
110
|
+
|
|
111
|
+
All stages stream live to the popup so you see exactly what's happening.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Quick start
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install cram-ai
|
|
119
|
+
|
|
120
|
+
cd your-repo
|
|
121
|
+
cram init # one-time setup — scans repo, generates docs, indexes symbols
|
|
122
|
+
cram task "add login validation" # run before every session
|
|
123
|
+
# → context pre-loaded into your AI tool
|
|
124
|
+
cram sync # run after every commit (or fires automatically via git hook)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**First command to context ready: under 60 seconds.**
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## CLI commands
|
|
132
|
+
|
|
133
|
+
| Command | When to run | What it does |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `cram init` | Once per repo | Scans structure, generates `ARCHITECTURE.md` + `SYMBOLS.md` via Haiku |
|
|
136
|
+
| `cram task "..."` | Before every session | Identifies relevant files by symbol, inlines focused excerpts |
|
|
137
|
+
| `cram continue` | Mid-session before committing | Extends grace period — prevents context reset on mid-task commits |
|
|
138
|
+
| `cram sync` | After every commit | Updates `ARCHITECTURE.md` + `SYMBOLS.md` from git diff |
|
|
139
|
+
| `cram decide "..."` | When making arch choices | Appends a dated decision entry to `DECISIONS.md` |
|
|
140
|
+
| `cram status` | Anytime | Shows `.cram-ai-context/` files and freshness |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Provider support
|
|
145
|
+
|
|
146
|
+
The tool is model-agnostic. Set `AICONTEXT_MODEL` to any provider:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Claude CLI (default — works inside Claude Code with no API key)
|
|
150
|
+
cram init
|
|
151
|
+
|
|
152
|
+
# Anthropic SDK
|
|
153
|
+
export ANTHROPIC_API_KEY=sk-...
|
|
154
|
+
export AICONTEXT_MODEL=anthropic/claude-haiku-4-5-20251001
|
|
155
|
+
cram init
|
|
156
|
+
|
|
157
|
+
# OpenAI
|
|
158
|
+
export OPENAI_API_KEY=sk-...
|
|
159
|
+
export AICONTEXT_MODEL=openai/gpt-4o-mini
|
|
160
|
+
cram init
|
|
161
|
+
|
|
162
|
+
# Google Gemini
|
|
163
|
+
export GEMINI_API_KEY=...
|
|
164
|
+
export AICONTEXT_MODEL=gemini/gemini-2.0-flash
|
|
165
|
+
cram init
|
|
166
|
+
|
|
167
|
+
# Local (Ollama — free, no key needed)
|
|
168
|
+
export AICONTEXT_MODEL=ollama/mistral
|
|
169
|
+
cram init
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Also supports: AWS Bedrock, GCP Vertex AI, Azure OpenAI, custom LiteLLM proxies — auto-discovered from env/credentials.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Session discipline
|
|
177
|
+
|
|
178
|
+
The context files handle orientation. These rules handle the rest:
|
|
179
|
+
|
|
180
|
+
1. **Run `cram task "..."` before every session** — never let the model hunt for files itself.
|
|
181
|
+
2. **Hard session boundary** — end the session the moment a feature works. New code = growing context = rising cost.
|
|
182
|
+
3. **Mid-task commit?** Run `cram continue` first to extend the grace period.
|
|
183
|
+
4. **Run `cram sync` after every commit** — keeps `ARCHITECTURE.md` and `SYMBOLS.md` accurate.
|
|
184
|
+
5. **Architectural decision?** Run `cram decide "use Redis for sessions"` — keeps `DECISIONS.md` current without opening the file.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Environment variables
|
|
189
|
+
|
|
190
|
+
| Variable | Default | Description |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `AICONTEXT_MODEL` | auto-detected | Model for context tasks — bare alias or `provider/model` |
|
|
193
|
+
| `ANTHROPIC_API_KEY` | — | Anthropic API key (optional inside Claude Code) |
|
|
194
|
+
| `AICONTEXT_MAX_FILES` | `5` | Max files inlined per task |
|
|
195
|
+
| `AICONTEXT_MAX_LINES` | `300` | Max lines per ARCHITECTURE.md |
|
|
196
|
+
| `AICONTEXT_MAX_EXCERPT_LINES` | `80` | Max lines excerpted per file in `CURRENT_TASK.md` |
|
|
197
|
+
| `CRAM_TASK_GRACE_SECONDS` | `600` | Seconds after `cram task` before a commit resets context |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Works with any AI coding tool
|
|
202
|
+
|
|
203
|
+
| Tool | How context loads |
|
|
204
|
+
|---|---|
|
|
205
|
+
| **Claude Code** | Reads `.cram-ai-context/` recursively — all files auto-loaded |
|
|
206
|
+
| **Cursor** | Writes to `.cursor/rules/cram-task.md` — auto-loaded by Cursor |
|
|
207
|
+
| **Windsurf** | Writes to `.windsurf/rules/cram-task.md` — auto-loaded |
|
|
208
|
+
| **Codex** | Writes to `.cram-ai-context/AGENTS.md` — auto-loaded |
|
|
209
|
+
| **GitHub Copilot** | Writes to `.github/cram-task.md` — include once in `copilot-instructions.md` |
|
|
210
|
+
|
|
211
|
+
For non-Claude tools, cram automatically prepends a compact architecture summary so the model has repo orientation even without recursive file loading.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Running tests
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pip install pytest
|
|
219
|
+
pytest
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
57 passing tests, no API key required. All model calls are mocked.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
cram_ai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# cram-ai
|
|
2
|
+
|
|
3
|
+
> Slash AI coding token costs by injecting only what the model needs — nothing more.
|
|
4
|
+
|
|
5
|
+
AI coding tools auto-index your entire repo at session start. That indexing generates **cache writes** — the most expensive token type (3–4× the cost of reads). cram-ai replaces auto-indexing with a small set of curated files that give the model exactly what it needs: repo structure, key decisions, and focused excerpts of only the files relevant to your current task.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Benchmarks
|
|
10
|
+
|
|
11
|
+
### cram-ai itself (31 source files, Python CLI tool)
|
|
12
|
+
|
|
13
|
+
| | Tokens | Sonnet cost/session | Opus cost/session |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **Without cram** — full repo auto-indexed | 49,683 | $0.186 | $0.932 |
|
|
16
|
+
| **Without cram** — orientation set only¹ | 19,687 | $0.074 | $0.369 |
|
|
17
|
+
| **With cram** — ARCHITECTURE + SYMBOLS + task context | 1,898 | $0.007 | $0.036 |
|
|
18
|
+
|
|
19
|
+
**96% token reduction. $0.18 saved per session. $18 over 100 sessions (Sonnet).**
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### pallets/flask (118 source files, Python web framework)
|
|
24
|
+
|
|
25
|
+
| | Tokens | Sonnet cost/session | Opus cost/session |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| **Without cram** — full repo auto-indexed | 171,641 | $0.644 | $3.22 |
|
|
28
|
+
| **Without cram** — orientation set only¹ | 60,863 | $0.228 | $1.14 |
|
|
29
|
+
| **With cram** — ARCHITECTURE + SYMBOLS + task context | 5,929 | $0.022 | $0.111 |
|
|
30
|
+
|
|
31
|
+
**96.5% token reduction. $0.62 saved per session. $62 over 100 sessions (Sonnet).**
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### hoppscotch/hoppscotch (2,151 source files, TypeScript monorepo)
|
|
36
|
+
|
|
37
|
+
| | Tokens | Sonnet cost/session |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| **Without cram** — full repo auto-indexed | 418,697 | $1.57 |
|
|
40
|
+
| **With cram** | 7,239 | $0.027 |
|
|
41
|
+
|
|
42
|
+
**98.3% reduction. $154 saved over 100 sessions (Sonnet).**
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
> ¹ *Orientation set = file tree + README + pyproject.toml/package.json + 5 largest source files. A realistic estimate for tools that don't index everything.*
|
|
47
|
+
> Pricing: Claude Sonnet 4.6 cache write $3.75/M, Opus 4.8 $18.75/M. Savings scale with team size and session frequency.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
AI agents spend most tokens on **orientation** — finding relevant files, understanding structure, reading configs. cram-ai replaces that with a curated map the model reads instead of building itself.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
your-repo/
|
|
57
|
+
└── .cram-ai-context/
|
|
58
|
+
├── ARCHITECTURE.md ← repo structure, tech stack, key files (auto-generated by Haiku)
|
|
59
|
+
├── DECISIONS.md ← architectural decisions you want the AI to respect
|
|
60
|
+
├── SYMBOLS.md ← public function/class index across all source files (auto-generated)
|
|
61
|
+
└── CURRENT_TASK.md ← per-session: task + focused excerpts of relevant files
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**`SYMBOLS.md`** is the key accuracy improvement. Rather than asking a model to guess which files matter based on filenames alone, cram maps every source file to its public identifiers (`api/routes.py: handle_rate_limit, check_throttle, apply_backoff`). The model uses that map to select files *and* identify the exact functions to excerpt — so "fix the rate limiter" finds `check_throttle` even if the words don't match.
|
|
65
|
+
|
|
66
|
+
`cram task "..."` runs before every session:
|
|
67
|
+
|
|
68
|
+
1. **`[1/4]`** Loads `SYMBOLS.md` — 455 identifiers across 65 files, zero LLM calls
|
|
69
|
+
2. **`[2/4]`** Sends architecture + symbol index to Haiku → model returns `path | RelevantFunc, OtherClass`
|
|
70
|
+
3. **`[3/4]`** Extracts identifier-focused excerpts — only the lines that contain those functions, plus context window
|
|
71
|
+
4. **`[4/4]`** Writes to your tool's instruction file, warns if below cache minimum for your model
|
|
72
|
+
|
|
73
|
+
All stages stream live to the popup so you see exactly what's happening.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install cram-ai
|
|
81
|
+
|
|
82
|
+
cd your-repo
|
|
83
|
+
cram init # one-time setup — scans repo, generates docs, indexes symbols
|
|
84
|
+
cram task "add login validation" # run before every session
|
|
85
|
+
# → context pre-loaded into your AI tool
|
|
86
|
+
cram sync # run after every commit (or fires automatically via git hook)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**First command to context ready: under 60 seconds.**
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## CLI commands
|
|
94
|
+
|
|
95
|
+
| Command | When to run | What it does |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `cram init` | Once per repo | Scans structure, generates `ARCHITECTURE.md` + `SYMBOLS.md` via Haiku |
|
|
98
|
+
| `cram task "..."` | Before every session | Identifies relevant files by symbol, inlines focused excerpts |
|
|
99
|
+
| `cram continue` | Mid-session before committing | Extends grace period — prevents context reset on mid-task commits |
|
|
100
|
+
| `cram sync` | After every commit | Updates `ARCHITECTURE.md` + `SYMBOLS.md` from git diff |
|
|
101
|
+
| `cram decide "..."` | When making arch choices | Appends a dated decision entry to `DECISIONS.md` |
|
|
102
|
+
| `cram status` | Anytime | Shows `.cram-ai-context/` files and freshness |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Provider support
|
|
107
|
+
|
|
108
|
+
The tool is model-agnostic. Set `AICONTEXT_MODEL` to any provider:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Claude CLI (default — works inside Claude Code with no API key)
|
|
112
|
+
cram init
|
|
113
|
+
|
|
114
|
+
# Anthropic SDK
|
|
115
|
+
export ANTHROPIC_API_KEY=sk-...
|
|
116
|
+
export AICONTEXT_MODEL=anthropic/claude-haiku-4-5-20251001
|
|
117
|
+
cram init
|
|
118
|
+
|
|
119
|
+
# OpenAI
|
|
120
|
+
export OPENAI_API_KEY=sk-...
|
|
121
|
+
export AICONTEXT_MODEL=openai/gpt-4o-mini
|
|
122
|
+
cram init
|
|
123
|
+
|
|
124
|
+
# Google Gemini
|
|
125
|
+
export GEMINI_API_KEY=...
|
|
126
|
+
export AICONTEXT_MODEL=gemini/gemini-2.0-flash
|
|
127
|
+
cram init
|
|
128
|
+
|
|
129
|
+
# Local (Ollama — free, no key needed)
|
|
130
|
+
export AICONTEXT_MODEL=ollama/mistral
|
|
131
|
+
cram init
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Also supports: AWS Bedrock, GCP Vertex AI, Azure OpenAI, custom LiteLLM proxies — auto-discovered from env/credentials.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Session discipline
|
|
139
|
+
|
|
140
|
+
The context files handle orientation. These rules handle the rest:
|
|
141
|
+
|
|
142
|
+
1. **Run `cram task "..."` before every session** — never let the model hunt for files itself.
|
|
143
|
+
2. **Hard session boundary** — end the session the moment a feature works. New code = growing context = rising cost.
|
|
144
|
+
3. **Mid-task commit?** Run `cram continue` first to extend the grace period.
|
|
145
|
+
4. **Run `cram sync` after every commit** — keeps `ARCHITECTURE.md` and `SYMBOLS.md` accurate.
|
|
146
|
+
5. **Architectural decision?** Run `cram decide "use Redis for sessions"` — keeps `DECISIONS.md` current without opening the file.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Environment variables
|
|
151
|
+
|
|
152
|
+
| Variable | Default | Description |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `AICONTEXT_MODEL` | auto-detected | Model for context tasks — bare alias or `provider/model` |
|
|
155
|
+
| `ANTHROPIC_API_KEY` | — | Anthropic API key (optional inside Claude Code) |
|
|
156
|
+
| `AICONTEXT_MAX_FILES` | `5` | Max files inlined per task |
|
|
157
|
+
| `AICONTEXT_MAX_LINES` | `300` | Max lines per ARCHITECTURE.md |
|
|
158
|
+
| `AICONTEXT_MAX_EXCERPT_LINES` | `80` | Max lines excerpted per file in `CURRENT_TASK.md` |
|
|
159
|
+
| `CRAM_TASK_GRACE_SECONDS` | `600` | Seconds after `cram task` before a commit resets context |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Works with any AI coding tool
|
|
164
|
+
|
|
165
|
+
| Tool | How context loads |
|
|
166
|
+
|---|---|
|
|
167
|
+
| **Claude Code** | Reads `.cram-ai-context/` recursively — all files auto-loaded |
|
|
168
|
+
| **Cursor** | Writes to `.cursor/rules/cram-task.md` — auto-loaded by Cursor |
|
|
169
|
+
| **Windsurf** | Writes to `.windsurf/rules/cram-task.md` — auto-loaded |
|
|
170
|
+
| **Codex** | Writes to `.cram-ai-context/AGENTS.md` — auto-loaded |
|
|
171
|
+
| **GitHub Copilot** | Writes to `.github/cram-task.md` — include once in `copilot-instructions.md` |
|
|
172
|
+
|
|
173
|
+
For non-Claude tools, cram automatically prepends a compact architecture summary so the model has repo orientation even without recursive file loading.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Running tests
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install pytest
|
|
181
|
+
pytest
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
57 passing tests, no API key required. All model calls are mocked.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""cram add — append extra files to the current session context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
CONTEXT_DIR = '.cram-ai-context'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_task_text(content: str) -> str:
|
|
12
|
+
m = re.search(r'## Task\n(.+?)(?=\n##|\Z)', content, re.DOTALL)
|
|
13
|
+
return m.group(1).strip() if m else ''
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _task_keywords(task_text: str) -> list[str]:
|
|
17
|
+
stop = {
|
|
18
|
+
'add', 'fix', 'the', 'a', 'an', 'to', 'in', 'for', 'of', 'and', 'or',
|
|
19
|
+
'with', 'from', 'that', 'this', 'it', 'is', 'on', 'at', 'by', 'be',
|
|
20
|
+
'update', 'make', 'create', 'change', 'use', 'get', 'set', 'run', 'into',
|
|
21
|
+
'new', 'old', 'all', 'not', 'its', 'has',
|
|
22
|
+
}
|
|
23
|
+
words = re.findall(r'[A-Za-z][A-Za-z0-9_]*', task_text)
|
|
24
|
+
return [w for w in words if len(w) > 2 and w.lower() not in stop]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _replace_section(content: str, resolved: str, ext: str, excerpt: str) -> str:
|
|
28
|
+
pattern = r'\n### ' + re.escape(resolved) + r'\n```[^\n]*\n.*?\n```'
|
|
29
|
+
replacement = f'\n### {resolved}\n```{ext}\n{excerpt}\n```'
|
|
30
|
+
result, n = re.subn(pattern, replacement, content, flags=re.DOTALL)
|
|
31
|
+
return result if n else content
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def add_files(
|
|
35
|
+
file_specs: list[str],
|
|
36
|
+
replace: bool = False,
|
|
37
|
+
target: str | None = None,
|
|
38
|
+
) -> bool:
|
|
39
|
+
from cram.find_context import _extract_excerpt, _resolve_path
|
|
40
|
+
from cram.utils import find_git_root as _find_git_root
|
|
41
|
+
from cram import targets as _targets
|
|
42
|
+
|
|
43
|
+
task_path = os.path.join(CONTEXT_DIR, 'CURRENT_TASK.md')
|
|
44
|
+
if not os.path.exists(task_path):
|
|
45
|
+
print('Error: no active session. Run `cram task "..."` first.', file=sys.stderr)
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
with open(task_path) as f:
|
|
49
|
+
current = f.read()
|
|
50
|
+
|
|
51
|
+
keywords = _task_keywords(_read_task_text(current))
|
|
52
|
+
existing = set(re.findall(r'^### (.+)$', current, re.MULTILINE))
|
|
53
|
+
|
|
54
|
+
changed = False
|
|
55
|
+
|
|
56
|
+
for spec in file_specs:
|
|
57
|
+
# Support "path/file.py | Func1, Func2" for explicit identifier targeting
|
|
58
|
+
if '|' in spec:
|
|
59
|
+
path_part, id_part = spec.split('|', 1)
|
|
60
|
+
fpath = path_part.strip()
|
|
61
|
+
ids = [i.strip() for i in id_part.split(',') if i.strip()]
|
|
62
|
+
else:
|
|
63
|
+
fpath = spec.strip()
|
|
64
|
+
ids = keywords # use current task's keywords as focus hints
|
|
65
|
+
|
|
66
|
+
resolved = _resolve_path(fpath)
|
|
67
|
+
|
|
68
|
+
if not os.path.exists(resolved):
|
|
69
|
+
print(f' ✗ {fpath} — not found')
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if resolved in existing and not replace:
|
|
73
|
+
print(f' ! {resolved} — already in context (use --replace to update)')
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
ext = os.path.splitext(resolved)[1].lstrip('.')
|
|
77
|
+
excerpt = _extract_excerpt(resolved, ids)
|
|
78
|
+
tok = len(excerpt) // 4
|
|
79
|
+
|
|
80
|
+
if resolved in existing:
|
|
81
|
+
current = _replace_section(current, resolved, ext, excerpt)
|
|
82
|
+
print(f' ↺ {resolved} ~{tok:,} tokens (updated)')
|
|
83
|
+
else:
|
|
84
|
+
current += f'\n### {resolved}\n```{ext}\n{excerpt}\n```\n'
|
|
85
|
+
print(f' + {resolved} ~{tok:,} tokens')
|
|
86
|
+
|
|
87
|
+
changed = True
|
|
88
|
+
|
|
89
|
+
if not changed:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
with open(task_path, 'w') as f:
|
|
93
|
+
f.write(current)
|
|
94
|
+
|
|
95
|
+
total = len(current) // 4
|
|
96
|
+
print(f'\n Total context: ~{total:,} tokens')
|
|
97
|
+
|
|
98
|
+
root = _find_git_root(os.getcwd())
|
|
99
|
+
eff_target = target if target is not None else _targets.load_default_target(root)
|
|
100
|
+
if eff_target:
|
|
101
|
+
arch_path = os.path.join(CONTEXT_DIR, 'ARCHITECTURE.md')
|
|
102
|
+
arch = open(arch_path).read() if os.path.exists(arch_path) else ''
|
|
103
|
+
if eff_target == 'all':
|
|
104
|
+
for p in _targets.write_to_all_detected(root, current, arch):
|
|
105
|
+
print(f' → {os.path.relpath(p)}')
|
|
106
|
+
else:
|
|
107
|
+
path = _targets.write_to_target(root, eff_target, current, arch)
|
|
108
|
+
print(f' → {os.path.relpath(path)}')
|
|
109
|
+
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main() -> None:
|
|
114
|
+
import argparse
|
|
115
|
+
from cram.utils import find_git_root
|
|
116
|
+
from cram import targets as _targets
|
|
117
|
+
|
|
118
|
+
parser = argparse.ArgumentParser(
|
|
119
|
+
prog='cram add',
|
|
120
|
+
description='Append files to the current session context',
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
'files', nargs='+',
|
|
124
|
+
help='Files to add. Quote "path | Func1, Func2" to focus on specific identifiers.',
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument('--replace', action='store_true',
|
|
127
|
+
help='Replace the excerpt if the file is already in context')
|
|
128
|
+
parser.add_argument('--target',
|
|
129
|
+
choices=[*_targets.TARGET_FILES, 'all'],
|
|
130
|
+
default=None, metavar='TARGET')
|
|
131
|
+
parser.add_argument('--path', default=None, metavar='REPO_PATH')
|
|
132
|
+
args = parser.parse_args()
|
|
133
|
+
|
|
134
|
+
start = os.path.abspath(args.path) if args.path else os.getcwd()
|
|
135
|
+
root = find_git_root(start)
|
|
136
|
+
|
|
137
|
+
if not os.path.isdir(os.path.join(root, CONTEXT_DIR)):
|
|
138
|
+
print(f'Error: {CONTEXT_DIR}/ not found in {root}.', file=sys.stderr)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
os.chdir(root)
|
|
142
|
+
ok = add_files(args.files, replace=args.replace, target=args.target)
|
|
143
|
+
sys.exit(0 if ok else 1)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Install or remove a macOS LaunchAgent that starts cram-menu at login."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_LABEL = 'ai.cram.menu'
|
|
11
|
+
_PLIST = Path.home() / 'Library' / 'LaunchAgents' / f'{_LABEL}.plist'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cram_menu_bin() -> str:
|
|
15
|
+
"""Return the absolute path to the cram-menu binary."""
|
|
16
|
+
# Prefer the binary in the same venv as this interpreter
|
|
17
|
+
candidate = Path(sys.executable).parent / 'cram-menu'
|
|
18
|
+
if candidate.exists():
|
|
19
|
+
return str(candidate)
|
|
20
|
+
found = shutil.which('cram-menu')
|
|
21
|
+
if found:
|
|
22
|
+
return found
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
"cram-menu binary not found. "
|
|
25
|
+
"Install with: pip install 'cram-ai[tray]'"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install(repo_path: str | None = None) -> None:
|
|
30
|
+
from cram.utils import find_git_root
|
|
31
|
+
repo = find_git_root(repo_path or os.getcwd())
|
|
32
|
+
|
|
33
|
+
binary = _cram_menu_bin()
|
|
34
|
+
|
|
35
|
+
plist = f"""\
|
|
36
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
37
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
38
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
39
|
+
<plist version="1.0">
|
|
40
|
+
<dict>
|
|
41
|
+
<key>Label</key> <string>{_LABEL}</string>
|
|
42
|
+
<key>ProgramArguments</key>
|
|
43
|
+
<array>
|
|
44
|
+
<string>{binary}</string>
|
|
45
|
+
<string>{repo}</string>
|
|
46
|
+
</array>
|
|
47
|
+
<key>RunAtLoad</key> <true/>
|
|
48
|
+
<key>KeepAlive</key> <false/>
|
|
49
|
+
<key>StandardOutPath</key> <string>{Path.home()}/.config/cram-ai/cram-menu.log</string>
|
|
50
|
+
<key>StandardErrorPath</key> <string>{Path.home()}/.config/cram-ai/cram-menu.log</string>
|
|
51
|
+
</dict>
|
|
52
|
+
</plist>
|
|
53
|
+
"""
|
|
54
|
+
Path.home().joinpath('.config', 'cram-ai').mkdir(parents=True, exist_ok=True)
|
|
55
|
+
_PLIST.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
_PLIST.write_text(plist)
|
|
57
|
+
|
|
58
|
+
# Load immediately (also activates on next login)
|
|
59
|
+
subprocess.run(['launchctl', 'load', str(_PLIST)], check=False)
|
|
60
|
+
|
|
61
|
+
print(f"cram-menu will now start automatically at login.")
|
|
62
|
+
print(f"Repo: {repo}")
|
|
63
|
+
print(f"Plist: {_PLIST}")
|
|
64
|
+
print(f"\nTo stop: cram autostart off")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def uninstall() -> None:
|
|
68
|
+
if _PLIST.exists():
|
|
69
|
+
subprocess.run(['launchctl', 'unload', str(_PLIST)], check=False)
|
|
70
|
+
_PLIST.unlink()
|
|
71
|
+
print(f"Removed {_PLIST}")
|
|
72
|
+
print("cram-menu will no longer start at login.")
|
|
73
|
+
else:
|
|
74
|
+
print("No autostart entry found.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def status() -> None:
|
|
78
|
+
if _PLIST.exists():
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
['launchctl', 'list', _LABEL],
|
|
81
|
+
capture_output=True, text=True,
|
|
82
|
+
)
|
|
83
|
+
running = result.returncode == 0
|
|
84
|
+
print(f"Autostart: installed ({'running' if running else 'not currently running'})")
|
|
85
|
+
print(f"Plist: {_PLIST}")
|
|
86
|
+
else:
|
|
87
|
+
print("Autostart: not installed")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
if sys.platform != 'darwin':
|
|
92
|
+
print(
|
|
93
|
+
"cram autostart is currently macOS-only.\n"
|
|
94
|
+
"Windows: add cram-menu to the Startup folder.\n"
|
|
95
|
+
"Linux: add to ~/.config/autostart/ or your WM's autostart.",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
action = sys.argv[1] if len(sys.argv) > 1 else 'on'
|
|
101
|
+
path = sys.argv[2] if len(sys.argv) > 2 else None
|
|
102
|
+
|
|
103
|
+
if action in ('on', 'install', 'enable'):
|
|
104
|
+
install(path)
|
|
105
|
+
elif action in ('off', 'uninstall', 'disable'):
|
|
106
|
+
uninstall()
|
|
107
|
+
elif action in ('status',):
|
|
108
|
+
status()
|
|
109
|
+
else:
|
|
110
|
+
print(f"Usage: cram autostart [on|off|status] [repo_path]")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == '__main__':
|
|
115
|
+
main()
|