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.
Files changed (41) hide show
  1. cram_ai-0.1.0/PKG-INFO +228 -0
  2. cram_ai-0.1.0/README.md +190 -0
  3. cram_ai-0.1.0/cram/__init__.py +1 -0
  4. cram_ai-0.1.0/cram/__main__.py +2 -0
  5. cram_ai-0.1.0/cram/add_context.py +143 -0
  6. cram_ai-0.1.0/cram/autostart.py +115 -0
  7. cram_ai-0.1.0/cram/benchmark.py +196 -0
  8. cram_ai-0.1.0/cram/cli.py +77 -0
  9. cram_ai-0.1.0/cram/decide.py +73 -0
  10. cram_ai-0.1.0/cram/doctor.py +144 -0
  11. cram_ai-0.1.0/cram/find_context.py +374 -0
  12. cram_ai-0.1.0/cram/hooks.py +192 -0
  13. cram_ai-0.1.0/cram/init.py +217 -0
  14. cram_ai-0.1.0/cram/mcp_server.py +262 -0
  15. cram_ai-0.1.0/cram/menubar.py +227 -0
  16. cram_ai-0.1.0/cram/session.py +93 -0
  17. cram_ai-0.1.0/cram/status.py +135 -0
  18. cram_ai-0.1.0/cram/suggest.py +77 -0
  19. cram_ai-0.1.0/cram/symbols.py +105 -0
  20. cram_ai-0.1.0/cram/sync_context.py +159 -0
  21. cram_ai-0.1.0/cram/targets.py +179 -0
  22. cram_ai-0.1.0/cram/tray.py +353 -0
  23. cram_ai-0.1.0/cram/tray_server.py +538 -0
  24. cram_ai-0.1.0/cram/tray_ui/popup.css +800 -0
  25. cram_ai-0.1.0/cram/tray_ui/popup.html +160 -0
  26. cram_ai-0.1.0/cram/tray_ui/popup.js +644 -0
  27. cram_ai-0.1.0/cram/utils.py +363 -0
  28. cram_ai-0.1.0/cram/vscode.py +131 -0
  29. cram_ai-0.1.0/cram_ai.egg-info/PKG-INFO +228 -0
  30. cram_ai-0.1.0/cram_ai.egg-info/SOURCES.txt +39 -0
  31. cram_ai-0.1.0/cram_ai.egg-info/dependency_links.txt +1 -0
  32. cram_ai-0.1.0/cram_ai.egg-info/entry_points.txt +4 -0
  33. cram_ai-0.1.0/cram_ai.egg-info/requires.txt +22 -0
  34. cram_ai-0.1.0/cram_ai.egg-info/top_level.txt +1 -0
  35. cram_ai-0.1.0/pyproject.toml +52 -0
  36. cram_ai-0.1.0/setup.cfg +4 -0
  37. cram_ai-0.1.0/tests/test_find_context.py +205 -0
  38. cram_ai-0.1.0/tests/test_init.py +175 -0
  39. cram_ai-0.1.0/tests/test_sync.py +127 -0
  40. cram_ai-0.1.0/tests/test_targets.py +109 -0
  41. 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
@@ -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,2 @@
1
+ from cram.cli import main
2
+ main()
@@ -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()