nabla-cli 0.1.0__tar.gz → 0.2.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 (36) hide show
  1. nabla_cli-0.2.0/PKG-INFO +236 -0
  2. nabla_cli-0.2.0/README.md +223 -0
  3. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/__init__.py +1 -1
  4. nabla_cli-0.2.0/nabla/__main__.py +8 -0
  5. nabla_cli-0.2.0/nabla/cli.py +439 -0
  6. nabla_cli-0.2.0/nabla/config.py +113 -0
  7. nabla_cli-0.2.0/nabla/samplegen.py +118 -0
  8. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/schemas/site_profile.schema.json +1 -1
  9. nabla_cli-0.2.0/nabla_cli.egg-info/PKG-INFO +236 -0
  10. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla_cli.egg-info/SOURCES.txt +6 -0
  11. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/pyproject.toml +1 -1
  12. nabla_cli-0.2.0/tests/test_cli_ux.py +332 -0
  13. nabla_cli-0.2.0/tests/test_config.py +179 -0
  14. nabla_cli-0.2.0/tests/test_samplegen.py +81 -0
  15. nabla_cli-0.1.0/PKG-INFO +0 -82
  16. nabla_cli-0.1.0/README.md +0 -69
  17. nabla_cli-0.1.0/nabla/cli.py +0 -121
  18. nabla_cli-0.1.0/nabla_cli.egg-info/PKG-INFO +0 -82
  19. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/contract.py +0 -0
  20. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/induce.py +0 -0
  21. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/profile.py +0 -0
  22. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/prompt_recon.py +0 -0
  23. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/recorder.py +0 -0
  24. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/scanner.py +0 -0
  25. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla/schema_resolver.py +0 -0
  26. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla_cli.egg-info/dependency_links.txt +0 -0
  27. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla_cli.egg-info/entry_points.txt +0 -0
  28. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla_cli.egg-info/requires.txt +0 -0
  29. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/nabla_cli.egg-info/top_level.txt +0 -0
  30. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/setup.cfg +0 -0
  31. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/tests/test_e2e_profile.py +0 -0
  32. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/tests/test_induce.py +0 -0
  33. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/tests/test_prompt_recon.py +0 -0
  34. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/tests/test_recorder.py +0 -0
  35. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/tests/test_scanner.py +0 -0
  36. {nabla_cli-0.1.0 → nabla_cli-0.2.0}/tests/test_schema_resolver.py +0 -0
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: nabla-cli
3
+ Version: 0.2.0
4
+ Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: jsonschema
9
+ Requires-Dist: pydantic>=2
10
+ Requires-Dist: openai>=1.40
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == "dev"
13
+
14
+ # nabla — OpenAI call-site harvester
15
+
16
+ `nabla` finds the OpenAI call sites in a Python repo, records real
17
+ (prompt, completion) traffic for one of them, and emits a single
18
+ self-contained **site profile** (`site_profile.json`) describing that
19
+ call site: prompt template + slots, resolved output JSON Schema,
20
+ sampling parameters, recorded examples, an inferred goal with difficulty
21
+ dimensions, and a cost/latency baseline.
22
+
23
+ The profile is the handoff artifact for training a small, cheap
24
+ replacement model for that one call site — everything downstream codes
25
+ against the profile, never against your repo.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install nabla-cli
31
+ # or, isolated:
32
+ pipx install nabla-cli
33
+ ```
34
+
35
+ Requires Python ≥ 3.11.
36
+
37
+ ### `nabla` not found after `pip install`?
38
+
39
+ That means pip's script directory isn't on your PATH (pip prints a
40
+ yellow warning about this during install). Two fixes, either works:
41
+
42
+ - Run it as a module instead — always works, no PATH needed:
43
+
44
+ ```bash
45
+ python -m nabla scan path/to/repo
46
+ ```
47
+
48
+ - Or add pip's script directory to PATH. Find it with
49
+ `python -c "import sysconfig; print(sysconfig.get_path('scripts'))"`
50
+ and add that folder to your PATH (on Windows: Settings → search
51
+ "environment variables" → edit `Path` → add the folder, then open a
52
+ new terminal).
53
+
54
+ `pipx install nabla-cli` avoids the problem entirely if you have pipx.
55
+
56
+ ## Quickstart
57
+
58
+ Run from inside the target repo:
59
+
60
+ ```bash
61
+ pip install nabla-cli
62
+
63
+ nabla init # one-time setup, once per machine
64
+ nabla scan
65
+ nabla record
66
+ nabla profile
67
+ ```
68
+
69
+ No flags needed on the happy path: the repo defaults to the current
70
+ directory, `--site` is auto-picked when the repo has exactly one
71
+ supported call site (with more than one, pass `--site <symbol>` — the
72
+ names come from `scan`'s output), `record` writes
73
+ `<site>.recorded.jsonl`, and `profile` finds that file automatically by
74
+ the same convention.
75
+
76
+ If you don't pass `--samples`, `record` looks for
77
+ `<site>.samples.jsonl` and otherwise generates a handful of synthetic
78
+ inputs from the site's prompt template (saving them to that file so you
79
+ can edit them and rerun with realistic data). Profiles built on
80
+ generated inputs are marked `generated_samples` in their provenance —
81
+ fine for a first look, but prefer real sample data for a profile you
82
+ intend to hand off.
83
+
84
+ ### `nabla init`
85
+
86
+ Interactive, run once per machine:
87
+
88
+ ```
89
+ $ nabla init
90
+ Pick a provider:
91
+ 1) openai
92
+ 2) groq
93
+ 3) gemini
94
+ > 2
95
+ Paste your GROQ_API_KEY: ****************
96
+ Saved to ~/.nabla/config.json
97
+ ```
98
+
99
+ That's the only credential nabla needs. Base URLs, models, and request
100
+ pacing all come from a built-in preset for whichever provider you picked
101
+ — see [Providers](#providers).
102
+
103
+ ### `nabla scan`
104
+
105
+ ```
106
+ SYMBOL FILE KIND VERIFIABILITY FLAGS
107
+ extract_invoice app/invoice_extract.py create json_schema -
108
+
109
+ 1 call site found.
110
+ ```
111
+
112
+ ### `nabla record`
113
+
114
+ One progress line per sample, then a summary:
115
+
116
+ ```
117
+ nabla record: sample 1/6 -> 1 capture(s)
118
+ nabla record: sample 2/6 -> 1 capture(s)
119
+ ...
120
+ nabla record: 6 examples -> extract_invoice.recorded.jsonl
121
+ ```
122
+
123
+ `--out` defaults to `<site>.recorded.jsonl` in the current directory (so
124
+ `profile` can find it by the same convention afterward) — pass `--out`
125
+ yourself only to pick a different name or location.
126
+
127
+ To record your own inputs instead of generated ones, pass `--samples`
128
+ (or create `<site>.samples.jsonl`, which is picked up automatically): a
129
+ JSONL file, one line per call to record. Each line is
130
+ `{"input_slots": {...}}`, where the keys match the site function's
131
+ keyword arguments (as reported by `scan`):
132
+
133
+ ```json
134
+ {"input_slots": {"document_text": "Invoice INV-1001 total $250.00 due 2026-08-01", "locale_hint": "en-US"}}
135
+ {"input_slots": {"document_text": "Facture FR-552 montant 99,50 EUR", "locale_hint": "fr-FR"}}
136
+ ```
137
+
138
+ nabla runs the site's function once per line, in a subprocess, with its
139
+ OpenAI client rerouted through a local recording proxy.
140
+
141
+ ### `nabla profile`
142
+
143
+ ```
144
+ nabla profile: goal — "Extract structured invoice fields (total, currency,
145
+ due date) from free-form invoice or receipt text, normalizing
146
+ locale-specific number and date formats."
147
+ nabla profile: wrote extract_invoice.profile.json (site 4f2a9b1c3d8e…, supported=True, 6 examples)
148
+ ```
149
+
150
+ `--out` defaults to `<site>.profile.json` in the current directory.
151
+ `--examples` is optional: `profile` looks for `<site>.recorded.jsonl` in
152
+ the current directory — the file `record` just wrote — and you only need
153
+ `--examples <path>` to point it at a different recording. The one LLM
154
+ call in this step is goal inference: it reads the reconstructed prompt
155
+ template, the resolved output schema, and the recorded examples, and
156
+ returns a goal description, implicit constraints, and difficulty
157
+ dimensions with cited evidence. That's the goal line printed above, and
158
+ it's what lands in the profile's `goal` block.
159
+
160
+ ## Providers
161
+
162
+ `nabla init` supports three providers. Pick whichever you have a key
163
+ for — it doesn't need to be the provider your target repo already calls.
164
+
165
+ | Provider | Default model | Role |
166
+ |---|---|---|
167
+ | `openai` | your repo's own model (goal inference uses `gpt-4o-mini`) | Real upstream — `record` calls OpenAI directly with your key, auth passthrough. |
168
+ | `groq` | `openai/gpt-oss-120b` | Stand-in oracle — `record` calls Groq instead of OpenAI. |
169
+ | `gemini` | `gemini-3.5-flash` | Stand-in oracle — `record` calls Gemini instead of OpenAI. |
170
+
171
+ When you pick `groq` or `gemini`, they only stand in for *generating* the
172
+ completions during `record`. The profile's cost/latency baseline always
173
+ reports numbers for the target repo's own model — the one your code
174
+ actually calls in production — never the stand-in's.
175
+
176
+ Gemini's free tier has a very low daily quota: expect `record` to pace
177
+ requests with multi-second delays and to still hit rate limits past a
178
+ handful of samples. `groq` or `openai` handle larger sample sets more
179
+ comfortably.
180
+
181
+ ## What gets detected
182
+
183
+ `scan` classifies every `chat.completions.create` / `beta...parse` call
184
+ by verifiability: `json_schema` and `pydantic_parse` sites are supported
185
+ harvest targets; `tool_call`, `json_object_no_schema`, and `free_text`
186
+ sites are detected and reported but not harvested. Streaming,
187
+ multi-turn, vision, and `n>1` sites are flagged unsupported. Generic
188
+ wrapper functions are expanded to their callers; functions that own
189
+ their prompt template are kept as the site.
190
+
191
+ ## Advanced: overrides
192
+
193
+ ### Flags
194
+
195
+ Auto-detection is a default, not a requirement — the explicit flags still
196
+ work and take priority over it:
197
+
198
+ - `--site <symbol>` — skip site auto-pick; required once a repo has more
199
+ than one supported call site.
200
+ - `--out <path>` (`record`) — write recorded examples somewhere other than
201
+ `<site>.recorded.jsonl`.
202
+ - `--examples <path>` (`profile`) — read recorded examples from somewhere
203
+ other than `<site>.recorded.jsonl`.
204
+ - `--out <path>` (`profile`) — write the profile somewhere other than
205
+ `<site>.profile.json`.
206
+
207
+ ### Environment variables
208
+
209
+ Everything `nabla init` sets up can also be overridden by hand. Precedence,
210
+ highest first:
211
+
212
+ 1. An env var you set yourself
213
+ 2. `~/.nabla/config.json` (written by `nabla init`)
214
+ 3. The provider preset's built-in defaults
215
+
216
+ | Variable | Purpose |
217
+ |---|---|
218
+ | `OPENAI_API_KEY` | Default upstream for `record` (auth passthrough). |
219
+ | `GROQ_API_KEY` / `GEMINI_API_KEY` | That provider's conventional key env, read if `~/.nabla/config.json` has no stored key. |
220
+ | `NABLA_PROVIDER` | Overrides the provider `nabla init` selected. |
221
+ | `NABLA_UPSTREAM_BASE_URL` | Record through any OpenAI-compatible stand-in oracle instead. |
222
+ | `NABLA_UPSTREAM_API_KEY` | Key for the stand-in oracle. |
223
+ | `NABLA_UPSTREAM_MODEL` | Rewrite the model for the stand-in only (the target repo's own model still lands in the profile). |
224
+ | `NABLA_RECORD_DELAY_MS` | Pacing between samples for rate-limited upstreams (default 0). |
225
+ | `NABLA_SAMPLE_TIMEOUT` | Per-sample subprocess timeout in seconds (default 300). |
226
+ | `NABLA_INDUCE_BASE_URL` / `NABLA_INDUCE_API_KEY_ENV` / `NABLA_INDUCE_MODEL` | Swap the goal-inference provider/model. |
227
+
228
+ ## Notes
229
+
230
+ - `record` runs the site's enclosing function in a subprocess per sample,
231
+ with `OPENAI_BASE_URL` pointed at a local recording proxy. If the
232
+ target's tests mock the SDK, the proxy path reports zero traffic
233
+ explicitly instead of writing an empty profile.
234
+ - Recorded prompts/completions are real data from your repo and leave
235
+ the machine only via the goal-inference call. Review before sharing
236
+ profiles.
@@ -0,0 +1,223 @@
1
+ # nabla — OpenAI call-site harvester
2
+
3
+ `nabla` finds the OpenAI call sites in a Python repo, records real
4
+ (prompt, completion) traffic for one of them, and emits a single
5
+ self-contained **site profile** (`site_profile.json`) describing that
6
+ call site: prompt template + slots, resolved output JSON Schema,
7
+ sampling parameters, recorded examples, an inferred goal with difficulty
8
+ dimensions, and a cost/latency baseline.
9
+
10
+ The profile is the handoff artifact for training a small, cheap
11
+ replacement model for that one call site — everything downstream codes
12
+ against the profile, never against your repo.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install nabla-cli
18
+ # or, isolated:
19
+ pipx install nabla-cli
20
+ ```
21
+
22
+ Requires Python ≥ 3.11.
23
+
24
+ ### `nabla` not found after `pip install`?
25
+
26
+ That means pip's script directory isn't on your PATH (pip prints a
27
+ yellow warning about this during install). Two fixes, either works:
28
+
29
+ - Run it as a module instead — always works, no PATH needed:
30
+
31
+ ```bash
32
+ python -m nabla scan path/to/repo
33
+ ```
34
+
35
+ - Or add pip's script directory to PATH. Find it with
36
+ `python -c "import sysconfig; print(sysconfig.get_path('scripts'))"`
37
+ and add that folder to your PATH (on Windows: Settings → search
38
+ "environment variables" → edit `Path` → add the folder, then open a
39
+ new terminal).
40
+
41
+ `pipx install nabla-cli` avoids the problem entirely if you have pipx.
42
+
43
+ ## Quickstart
44
+
45
+ Run from inside the target repo:
46
+
47
+ ```bash
48
+ pip install nabla-cli
49
+
50
+ nabla init # one-time setup, once per machine
51
+ nabla scan
52
+ nabla record
53
+ nabla profile
54
+ ```
55
+
56
+ No flags needed on the happy path: the repo defaults to the current
57
+ directory, `--site` is auto-picked when the repo has exactly one
58
+ supported call site (with more than one, pass `--site <symbol>` — the
59
+ names come from `scan`'s output), `record` writes
60
+ `<site>.recorded.jsonl`, and `profile` finds that file automatically by
61
+ the same convention.
62
+
63
+ If you don't pass `--samples`, `record` looks for
64
+ `<site>.samples.jsonl` and otherwise generates a handful of synthetic
65
+ inputs from the site's prompt template (saving them to that file so you
66
+ can edit them and rerun with realistic data). Profiles built on
67
+ generated inputs are marked `generated_samples` in their provenance —
68
+ fine for a first look, but prefer real sample data for a profile you
69
+ intend to hand off.
70
+
71
+ ### `nabla init`
72
+
73
+ Interactive, run once per machine:
74
+
75
+ ```
76
+ $ nabla init
77
+ Pick a provider:
78
+ 1) openai
79
+ 2) groq
80
+ 3) gemini
81
+ > 2
82
+ Paste your GROQ_API_KEY: ****************
83
+ Saved to ~/.nabla/config.json
84
+ ```
85
+
86
+ That's the only credential nabla needs. Base URLs, models, and request
87
+ pacing all come from a built-in preset for whichever provider you picked
88
+ — see [Providers](#providers).
89
+
90
+ ### `nabla scan`
91
+
92
+ ```
93
+ SYMBOL FILE KIND VERIFIABILITY FLAGS
94
+ extract_invoice app/invoice_extract.py create json_schema -
95
+
96
+ 1 call site found.
97
+ ```
98
+
99
+ ### `nabla record`
100
+
101
+ One progress line per sample, then a summary:
102
+
103
+ ```
104
+ nabla record: sample 1/6 -> 1 capture(s)
105
+ nabla record: sample 2/6 -> 1 capture(s)
106
+ ...
107
+ nabla record: 6 examples -> extract_invoice.recorded.jsonl
108
+ ```
109
+
110
+ `--out` defaults to `<site>.recorded.jsonl` in the current directory (so
111
+ `profile` can find it by the same convention afterward) — pass `--out`
112
+ yourself only to pick a different name or location.
113
+
114
+ To record your own inputs instead of generated ones, pass `--samples`
115
+ (or create `<site>.samples.jsonl`, which is picked up automatically): a
116
+ JSONL file, one line per call to record. Each line is
117
+ `{"input_slots": {...}}`, where the keys match the site function's
118
+ keyword arguments (as reported by `scan`):
119
+
120
+ ```json
121
+ {"input_slots": {"document_text": "Invoice INV-1001 total $250.00 due 2026-08-01", "locale_hint": "en-US"}}
122
+ {"input_slots": {"document_text": "Facture FR-552 montant 99,50 EUR", "locale_hint": "fr-FR"}}
123
+ ```
124
+
125
+ nabla runs the site's function once per line, in a subprocess, with its
126
+ OpenAI client rerouted through a local recording proxy.
127
+
128
+ ### `nabla profile`
129
+
130
+ ```
131
+ nabla profile: goal — "Extract structured invoice fields (total, currency,
132
+ due date) from free-form invoice or receipt text, normalizing
133
+ locale-specific number and date formats."
134
+ nabla profile: wrote extract_invoice.profile.json (site 4f2a9b1c3d8e…, supported=True, 6 examples)
135
+ ```
136
+
137
+ `--out` defaults to `<site>.profile.json` in the current directory.
138
+ `--examples` is optional: `profile` looks for `<site>.recorded.jsonl` in
139
+ the current directory — the file `record` just wrote — and you only need
140
+ `--examples <path>` to point it at a different recording. The one LLM
141
+ call in this step is goal inference: it reads the reconstructed prompt
142
+ template, the resolved output schema, and the recorded examples, and
143
+ returns a goal description, implicit constraints, and difficulty
144
+ dimensions with cited evidence. That's the goal line printed above, and
145
+ it's what lands in the profile's `goal` block.
146
+
147
+ ## Providers
148
+
149
+ `nabla init` supports three providers. Pick whichever you have a key
150
+ for — it doesn't need to be the provider your target repo already calls.
151
+
152
+ | Provider | Default model | Role |
153
+ |---|---|---|
154
+ | `openai` | your repo's own model (goal inference uses `gpt-4o-mini`) | Real upstream — `record` calls OpenAI directly with your key, auth passthrough. |
155
+ | `groq` | `openai/gpt-oss-120b` | Stand-in oracle — `record` calls Groq instead of OpenAI. |
156
+ | `gemini` | `gemini-3.5-flash` | Stand-in oracle — `record` calls Gemini instead of OpenAI. |
157
+
158
+ When you pick `groq` or `gemini`, they only stand in for *generating* the
159
+ completions during `record`. The profile's cost/latency baseline always
160
+ reports numbers for the target repo's own model — the one your code
161
+ actually calls in production — never the stand-in's.
162
+
163
+ Gemini's free tier has a very low daily quota: expect `record` to pace
164
+ requests with multi-second delays and to still hit rate limits past a
165
+ handful of samples. `groq` or `openai` handle larger sample sets more
166
+ comfortably.
167
+
168
+ ## What gets detected
169
+
170
+ `scan` classifies every `chat.completions.create` / `beta...parse` call
171
+ by verifiability: `json_schema` and `pydantic_parse` sites are supported
172
+ harvest targets; `tool_call`, `json_object_no_schema`, and `free_text`
173
+ sites are detected and reported but not harvested. Streaming,
174
+ multi-turn, vision, and `n>1` sites are flagged unsupported. Generic
175
+ wrapper functions are expanded to their callers; functions that own
176
+ their prompt template are kept as the site.
177
+
178
+ ## Advanced: overrides
179
+
180
+ ### Flags
181
+
182
+ Auto-detection is a default, not a requirement — the explicit flags still
183
+ work and take priority over it:
184
+
185
+ - `--site <symbol>` — skip site auto-pick; required once a repo has more
186
+ than one supported call site.
187
+ - `--out <path>` (`record`) — write recorded examples somewhere other than
188
+ `<site>.recorded.jsonl`.
189
+ - `--examples <path>` (`profile`) — read recorded examples from somewhere
190
+ other than `<site>.recorded.jsonl`.
191
+ - `--out <path>` (`profile`) — write the profile somewhere other than
192
+ `<site>.profile.json`.
193
+
194
+ ### Environment variables
195
+
196
+ Everything `nabla init` sets up can also be overridden by hand. Precedence,
197
+ highest first:
198
+
199
+ 1. An env var you set yourself
200
+ 2. `~/.nabla/config.json` (written by `nabla init`)
201
+ 3. The provider preset's built-in defaults
202
+
203
+ | Variable | Purpose |
204
+ |---|---|
205
+ | `OPENAI_API_KEY` | Default upstream for `record` (auth passthrough). |
206
+ | `GROQ_API_KEY` / `GEMINI_API_KEY` | That provider's conventional key env, read if `~/.nabla/config.json` has no stored key. |
207
+ | `NABLA_PROVIDER` | Overrides the provider `nabla init` selected. |
208
+ | `NABLA_UPSTREAM_BASE_URL` | Record through any OpenAI-compatible stand-in oracle instead. |
209
+ | `NABLA_UPSTREAM_API_KEY` | Key for the stand-in oracle. |
210
+ | `NABLA_UPSTREAM_MODEL` | Rewrite the model for the stand-in only (the target repo's own model still lands in the profile). |
211
+ | `NABLA_RECORD_DELAY_MS` | Pacing between samples for rate-limited upstreams (default 0). |
212
+ | `NABLA_SAMPLE_TIMEOUT` | Per-sample subprocess timeout in seconds (default 300). |
213
+ | `NABLA_INDUCE_BASE_URL` / `NABLA_INDUCE_API_KEY_ENV` / `NABLA_INDUCE_MODEL` | Swap the goal-inference provider/model. |
214
+
215
+ ## Notes
216
+
217
+ - `record` runs the site's enclosing function in a subprocess per sample,
218
+ with `OPENAI_BASE_URL` pointed at a local recording proxy. If the
219
+ target's tests mock the SDK, the proxy path reports zero traffic
220
+ explicitly instead of writing an empty profile.
221
+ - Recorded prompts/completions are real data from your repo and leave
222
+ the machine only via the goal-inference call. Review before sharing
223
+ profiles.
@@ -1,3 +1,3 @@
1
1
  """nabla — call-site harvester CLI (pre-Phase-0)."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -0,0 +1,8 @@
1
+ """Allow `python -m nabla` — works even when pip's Scripts dir isn't on PATH."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())