@drbaher/draft-cli 0.7.0 → 0.8.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,71 @@ All notable changes to this project will be documented in this file. The
4
4
  format is loosely based on [Keep a Changelog](https://keepachangelog.com/),
5
5
  and the project adheres to semantic versioning once it leaves 0.x.
6
6
 
7
+ ## 0.8.1 — 2026-05-17
8
+
9
+ ### Docs
10
+
11
+ - **README refreshed for the v2 surface.** New "v2 features" section
12
+ with one-paragraph showcases for each shipped v2 capability (.docx
13
+ round-trip, typed parameters, computed placeholders, positional
14
+ addressing, `parties.json` registry, multi-document bundles,
15
+ `--from-deal` LLM inference). Command reference updated with new
16
+ flags + version annotations. Exit-code table updated with code 4
17
+ (validation / typed / computed / ref / positional) and code 5
18
+ (LLM). Intro paragraph rewritten to reflect the v2 capability
19
+ scope instead of just v1.
20
+ - No code or behavior change. v0.8.0's API surface, schema contract,
21
+ and CLI flags are unchanged. Strictly a docs publish.
22
+
23
+ ## 0.8.0 — 2026-05-17
24
+
25
+ ### Added
26
+
27
+ - **LLM inference from a deal description** (last v2 item). New
28
+ `--from-deal PATH` flag reads a free-form deal description and
29
+ asks the configured T5 LLM provider to extract values for the
30
+ schema's declared placeholders:
31
+ ```sh
32
+ draft nda.md --from-deal deal-notes.txt --output draft.md
33
+ ```
34
+ Where `deal-notes.txt` is unstructured prose:
35
+ ```
36
+ Mutual NDA between Acme Corporation (DE) and Globex (UK), effective
37
+ June 1, 2026, for a 2-year term.
38
+ ```
39
+ The LLM is asked to fill `party_a`, `party_a_state`, `party_b`,
40
+ `effective_date`, etc. — only the keys already detected as
41
+ placeholders are extracted.
42
+ - **Value-resolution precedence updated:**
43
+ `CLI flag > --params JSON > --from-deal (LLM) > --interactive > schema default > error`.
44
+ CLI / --params always win, so users can fix or override anything
45
+ the LLM got wrong.
46
+ - **New public API:** `inferFromDeal(dealText, placeholders, providerCfg, { fetcher })`.
47
+
48
+ ### Decisions locked (V2_BRIEFS_REMAINING Q4.1–Q4.3)
49
+
50
+ - **Q4.1 Provider:** same T5 provider config — `ANTHROPIC_API_KEY`,
51
+ `OPENAI_API_KEY`, or explicit `DRAFT_LLM_*`. No separate inference
52
+ provider; one network surface, one set of env vars.
53
+ - **Q4.2 Extra keys:** keys the LLM emits that aren't in the
54
+ detected placeholders are **warned** to stderr (not dropped
55
+ silently). The LLM gets a fresh list of allowed keys in the
56
+ prompt so this is rare in practice.
57
+ - **Q4.3 Auto-LLM:** `--from-deal` does **not** require an
58
+ explicit `--llm` flag — the inference is implicit. `--no-llm`
59
+ still disables it (the user can opt out of the network call).
60
+
61
+ ### Notes
62
+
63
+ - `--from-deal` errors are fatal (`EXIT.LLM` for provider /
64
+ network / parse failures). Users with bad provider configs see
65
+ the issue immediately rather than silently running with no
66
+ inferred values.
67
+ - Bundle mode (v0.7.0) does not yet thread `--from-deal` through
68
+ per-template inference. Deferred to a follow-up; the shared
69
+ parameter resolution makes the single-doc API already useful
70
+ for bundles via `--params`.
71
+
7
72
  ## 0.7.0 — 2026-05-17
8
73
 
9
74
  ### Added
package/PARAM_SCHEMA.md CHANGED
@@ -515,6 +515,71 @@ keys and their sources.
515
515
  Programmatic API: `loadBundle(path)` parses + validates; `cmdBundle`
516
516
  runs the orchestration with the same IO contract as `cmdDraft`.
517
517
 
518
+ ### LLM inference from a deal description (v0.8.0, opt-in)
519
+
520
+ `--from-deal PATH` reads a free-form deal description and asks the
521
+ configured T5 LLM provider to extract values for the schema's
522
+ declared placeholders. The inverse of T5 detection — instead of
523
+ inferring *where* placeholders are in a template, infer *what
524
+ values* they should take from the deal prose:
525
+
526
+ ```sh
527
+ draft nda.md --from-deal deal-notes.txt --output draft.md
528
+ ```
529
+
530
+ ```
531
+ # deal-notes.txt
532
+ Mutual NDA between Acme Corporation (DE) and Globex (UK),
533
+ effective June 1, 2026, for a 2-year term.
534
+ ```
535
+
536
+ Then `[Party A]` → `Acme Corporation`, `[Effective Date]` →
537
+ `June 1, 2026`, etc., without any `--party-a` / `--effective-date`
538
+ flags.
539
+
540
+ **Value-resolution precedence** with `--from-deal`:
541
+
542
+ ```
543
+ CLI flag > --params JSON > --from-deal (LLM) > --interactive > schema default > error
544
+ ```
545
+
546
+ CLI / --params always win, so users can fix or override anything the
547
+ LLM got wrong without re-running inference.
548
+
549
+ **Q4.1 locked:** same T5 provider config (`ANTHROPIC_API_KEY`,
550
+ `OPENAI_API_KEY`, or explicit `DRAFT_LLM_*`). One network surface,
551
+ one set of env vars.
552
+
553
+ **Q4.2 locked:** extra keys (LLM emits keys not in the detected
554
+ placeholder list) are **warned** to stderr, not silently dropped.
555
+ The LLM gets the allowed-key list in the prompt so this is rare in
556
+ practice.
557
+
558
+ **Q4.3 locked:** `--from-deal` does **not** require explicit
559
+ `--llm` — the inference is implicit when the flag is present.
560
+ `--no-llm` still disables the inference call (the user can opt
561
+ out of the network).
562
+
563
+ **Provider missing:** if no LLM provider is configured (no
564
+ `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `DRAFT_LLM_*` in env),
565
+ `--from-deal` errors immediately with `EXIT.LLM` (exit 5) and a
566
+ clear message. Same for network / HTTP errors / non-JSON LLM
567
+ responses.
568
+
569
+ **Resolution interaction with typed parameters:** inferred values
570
+ go through the same typed-normalization step as user-supplied
571
+ values. So an LLM that returns `"June 1, 2026"` for a `type: date`
572
+ parameter with `format: yyyy-MM-d` gets normalized to `2026-06-1`
573
+ before substitution.
574
+
575
+ **Bundle mode (v0.7.0) interaction:** bundles do not currently
576
+ thread `--from-deal` through per-template inference. The shared
577
+ parameter resolution already accepts `--params` JSON, which is the
578
+ simpler structured-data path for bundle workflows. Deferred to a
579
+ future release.
580
+
581
+ Programmatic API: `inferFromDeal(dealText, placeholders, providerCfg, { fetcher })`.
582
+
518
583
  ### Orphan handling (Q4 locked)
519
584
 
520
585
  Schema declares a key whose alias list matches no detected phrase →
package/README.md CHANGED
@@ -1,62 +1,64 @@
1
+ <p align="center">
2
+ <img src="assets/icon.svg" width="120" alt="draft-cli">
3
+ </p>
4
+
1
5
  # draft-cli
2
6
 
3
- A **deterministic placeholder-filler** for legal-document templates. Reads
4
- bracketed (`[Party A]`) or mustache (`{{Party A}}`) markup, `.docx` yellow
5
- highlights, or generic-name heuristics; substitutes from CLI flags, a JSON
6
- params file, or an interactive prompt; writes a ready-to-review draft.
7
+ > Part of the contract-operations CLI suite. **draft-cli** (fill placeholders) → [**nda-review-cli**](https://github.com/DrBaher/nda-review-cli) (review, redline, negotiate) → [**docx2pdf-cli**](https://github.com/DrBaher/docx2pdf-cli) (DOCX → PDF) → [**sign-cli**](https://github.com/DrBaher/sign-cli) (signing + audit). [Showcase site](https://cli.drbaher.com/).
7
8
 
8
- Single-file Node.js. One runtime dependency (`jszip`, for `.docx`). Local-first,
9
- no telemetry, MIT-licensed. Part of the contract-operations suite (see
10
- [cli.drbaher.com](https://cli.drbaher.com)).
9
+ [![npm version](https://img.shields.io/npm/v/@drbaher/draft-cli.svg)](https://www.npmjs.com/package/@drbaher/draft-cli)
10
+ [![npm downloads](https://img.shields.io/npm/dw/@drbaher/draft-cli.svg)](https://www.npmjs.com/package/@drbaher/draft-cli)
11
+ [![CI](https://github.com/DrBaher/Draft-CLI/actions/workflows/ci.yml/badge.svg)](https://github.com/DrBaher/Draft-CLI/actions/workflows/ci.yml)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
13
 
12
- ---
14
+ Agent-first placeholder-filler for legal-document templates. Reads bracketed (`[Party A]`) or mustache markup, `.docx` yellow highlights, or generic-name heuristics; substitutes from CLI flags, a JSON params file, a shared `parties.json` registry, or — optionally — values extracted by an LLM from a free-form deal description; writes a ready-to-review draft as text or `.docx` (round-trip, runs and styles preserved). Schema-declared placeholders can be **typed** (`date` / `money` / `party`), **computed** (date arithmetic from another placeholder), **positional** (same text disambiguated by role), and **bundled** (fill multiple templates with one set of values).
13
15
 
14
- ## What it does
16
+ **The asymmetry is the architecture**: every step is deterministic and machine-driven except the values themselves — which can come from a flag, a params file, a parties registry, or an LLM extracting them from a prose deal description that a human wrote.
15
17
 
16
- You have a template. You have a deal. You want a draft you can review and
17
- send. The middle step — finding every `[Party A]`, `[Effective Date]`, and
18
- `[State of California]` and replacing them with real values — is mechanical,
19
- deterministic work that doesn't need an LLM and shouldn't need to leave your
20
- machine.
18
+ ## Run this
21
19
 
22
- ```
23
- template-vault get nda/house-mutual | draft - \
24
- --party-a "Acme Corporation" \
25
- --party-b "Vendor Inc." \
26
- --effective-date 2026-06-01 \
27
- --output draft.md
20
+ ```bash
21
+ npx @drbaher/draft-cli@latest --demo
28
22
  ```
29
23
 
30
- Or via a params file:
24
+ 30 seconds, no file authoring required. Walks through a bracketed-template NDA with three placeholders, demoing the full cascade end-to-end. Or if you want to dive into a real template:
31
25
 
32
- ```
33
- draft nda/house-mutual --params deal-acme.json --output draft.md
26
+ ```bash
27
+ npm i -g @drbaher/draft-cli
28
+ draft --list-placeholders examples/cp-mutual-nda-coverpage.md
34
29
  ```
35
30
 
36
- `draft` does **only** that step. Templates come from `template-vault-cli` or
37
- any markdown / .docx / stdin source. Review and red-line happens in
38
- `nda-review-cli`. Conversion to PDF goes through `docx2pdf-cli`. Signing
39
- goes through `sign-cli`. Each tool stays small and composable.
31
+ ## Where to go next
40
32
 
41
- ---
33
+ | If you are… | Start here |
34
+ |---|---|
35
+ | **A new user** evaluating the tool | This README's [Quick start](#quick-start) and [What this gives you](#what-this-gives-you) |
36
+ | **A drafter** filling your first template | [GETTING_STARTED.md](GETTING_STARTED.md) — 10-minute walkthrough of the main flows |
37
+ | **An LLM agent** driving the CLI | [AGENTS.md](AGENTS.md) → `draft --list-placeholders --json` → [PARAM_SCHEMA.md](PARAM_SCHEMA.md) for the locked contract |
38
+ | **A schema author** declaring typed / computed / positional placeholders | [PARAM_SCHEMA.md](PARAM_SCHEMA.md) §5 |
39
+ | **A contributor** | [ARCHITECTURE.md](ARCHITECTURE.md), [CONTRIBUTING.md](CONTRIBUTING.md) |
42
40
 
43
- ## Install
41
+ Concept deep-dives live in [PARAM_SCHEMA.md](PARAM_SCHEMA.md) (the v1 + v2 contract); architecture in [ARCHITECTURE.md](ARCHITECTURE.md); FAQ in [FAQ.md](FAQ.md).
44
42
 
45
- ```sh
46
- npm install -g @drbaher/draft-cli
47
- ```
43
+ ## Quick start
48
44
 
49
- Or run without installing:
45
+ ```bash
46
+ # Install
47
+ npm i -g @drbaher/draft-cli
50
48
 
51
- ```sh
49
+ # Or run without installing
52
50
  npx @drbaher/draft-cli@latest --demo
51
+
52
+ # After install, the binary is named `draft`
53
+ draft --version
54
+ draft --demo
53
55
  ```
54
56
 
55
57
  Requires Node.js ≥ 18. Tested on Ubuntu and macOS, Node 18 / 20 / 22.
56
58
 
57
59
  ### Shell completion
58
60
 
59
- ```sh
61
+ ```bash
60
62
  # bash
61
63
  draft --completion bash >> ~/.bashrc
62
64
 
@@ -65,40 +67,22 @@ draft --completion zsh > ~/.zsh/completions/_draft
65
67
  # ensure ~/.zsh/completions is in fpath, then: autoload -U compinit && compinit
66
68
  ```
67
69
 
68
- Completes flags, the `--syntax bracket|mustache` value, `--completion bash|zsh`,
69
- and file paths for `--params`, `--output`, and `--dictionary`.
70
-
71
- ---
72
-
73
- ## 30-second first run
74
-
75
- No file authoring required. The bundled demo runs end-to-end:
76
-
77
- ```sh
78
- npx @drbaher/draft-cli@latest --demo
79
- ```
80
-
81
- ```
82
- demo: substituting [Party A], [Party B], [Effective Date]
83
- # Mutual Non-Disclosure Agreement (demo)
84
-
85
- This Agreement is entered into on 2026-06-01 between Acme Corporation
86
- and Vendor Inc. (collectively, the "Parties").
87
-
88
- 1. Confidentiality. Acme Corporation and Vendor Inc. agree to keep confidential
89
- any information disclosed under this Agreement.
70
+ Completes flags, the `--syntax bracket|mustache` value, `--completion bash|zsh`, and file paths for `--params`, `--output`, `--dictionary`, `--parties`, `--bundle`, `--from-deal`.
90
71
 
91
- 2. Term. This Agreement remains in effect for two years from the
92
- 2026-06-01.
93
- ```
94
-
95
- What just happened: `draft-cli` detected three bracketed placeholders
96
- (`[Party A]`, `[Party B]`, `[Effective Date]`), mapped them to the
97
- canonical keys `party_a`, `party_b`, `effective_date`, and substituted
98
- in pre-canned demo values. Real runs use your own template and your own
99
- values.
72
+ ## What this gives you
100
73
 
101
- ---
74
+ - **Five-tier detection cascade** — `[Title Case]` brackets / `{{mustache}}` / `.docx` highlights (yellow/green/cyan/magenta) / heuristic dictionary / optional LLM. First tier with hits wins; the rest are skipped. Deterministic through tier 4.
75
+ - **`.docx` round-trip** — read a `.docx` template, fill placeholders, write `<basename>-filled.docx` with runs/styles/paragraph-breaks preserved. T3 highlight detection works against real templates (Common Paper, YC SAFE).
76
+ - **Schema file** for canonical keys, alias phrases, defaults, required-ness, and the v2 fields below (`type`, `format`, `currency`, `computed`, `positions`). Without a schema, every detected bracketed phrase is treated as a required parameter.
77
+ - **Typed parameters** — `type: date | money | party` validates and normalizes inputs before substitution. `"01/15/2027"` → `"January 15, 2027"`; `"$5M"` → `"$5,000,000.00"`. Bad inputs exit 4 with per-key error.
78
+ - **Computed placeholders** — derive one placeholder's value from another via date arithmetic: `{ "from": "effective_date", "op": "+", "value": "2 years" }`. Cycle detection at schema parse time.
79
+ - **Positional addressing** — same placeholder text with different semantic roles, addressed by position. Validated against the YC SAFE `$[_____________] × 2` case.
80
+ - **`parties.json` registry** — declare known parties once; schemas reference `ref:parties.<key>.<field>`. Eliminates duplicating party metadata across templates.
81
+ - **Multi-document bundles** — fill multiple templates with one shared set of values: `draft --bundle deal.bundle.json --params deal.json`. Abort-all on any pre-write error.
82
+ - **LLM-from-deal inference** — `--from-deal PATH` reads a free-form deal description and asks the configured T5 provider (Anthropic / OpenAI / `DRAFT_LLM_*`) to fill the schema's parameters. CLI / `--params` still win over inferred values.
83
+ - **Composable I/O** — stdin (`-`), stdout default, `--output PATH`, `template-vault get` integration for `<category>/<name>[@version]` template refs.
84
+ - **Three modes** — `draft` (substitute and emit), `--list-placeholders` (enumerate), `--validate` (completeness check). All support `--json` and `--why`.
85
+ - **Single file, stdlib + `jszip`**, no telemetry, local-first. Network only when the LLM tier is explicitly configured.
102
86
 
103
87
  ## End-to-end transcript
104
88
 
@@ -139,12 +123,9 @@ ok: 3 parameter(s) resolved
139
123
  ok
140
124
  ```
141
125
 
142
- ---
143
-
144
126
  ## Detection cascade
145
127
 
146
- `draft-cli` finds placeholders by trying five strategies in order. The
147
- **first non-empty tier wins** and the others are skipped.
128
+ `draft-cli` finds placeholders by trying five strategies in order. The **first non-empty tier wins** and the others are skipped.
148
129
 
149
130
  | Tier | Strategy | When |
150
131
  | ---- | -------------------- | ----------------------------------------- |
@@ -154,45 +135,32 @@ ok
154
135
  | 4 | Heuristic dictionary | Bundled list of generic names (`Acme Corporation`, `John Doe`, `example@example.com`, etc.). Warn-only by default. |
155
136
  | 5 | LLM | Last resort. Runs only when `.env` or process env configures `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `DRAFT_LLM_*`. |
156
137
 
157
- The cascade is **deterministic through tier 4**. Tier 5 is the only
158
- non-deterministic step and runs only when you've explicitly configured a
159
- provider key. Pass `--no-llm` to disable it even when configured. Pass
160
- `--no-heuristic` to skip tier 4.
138
+ The cascade is **deterministic through tier 4**. Tier 5 is the only non-deterministic step and runs only when you've explicitly configured a provider key. Pass `--no-llm` to disable it even when configured. Pass `--no-heuristic` to skip tier 4.
161
139
 
162
140
  See [PARAM_SCHEMA.md](PARAM_SCHEMA.md) for the full contract.
163
141
 
164
- ---
165
-
166
142
  ## Schema file (optional)
167
143
 
168
- A sibling `<template>.params.json` lets you declare canonical keys, alias
169
- phrases, defaults, and whether each parameter is required.
170
-
171
- **Short form:**
172
-
173
- ```json
174
- {
175
- "party_a": ["Party A", "Disclosing Party"],
176
- "party_b": ["Party B", "Receiving Party"],
177
- "effective_date": ["Effective Date"]
178
- }
179
- ```
180
-
181
- **Long form** (gates on `_meta`):
144
+ A sibling `<template>.params.json` lets you declare canonical keys, alias phrases, defaults, and required-ness. The **long form** (gated on `_meta`) unlocks the v2 fields:
182
145
 
183
146
  ```json
184
147
  {
185
148
  "_meta": { "schema_version": 1 },
186
- "party_a": { "aliases": ["Party A"], "required": true },
187
- "effective_date": { "aliases": ["Effective Date"], "required": false, "default": "the date first written above" }
149
+ "party_a": { "aliases": ["Party A"], "required": true, "type": "party" },
150
+ "effective_date": { "aliases": ["Effective Date"], "type": "date", "format": "MMMM d, yyyy" },
151
+ "term_end": { "aliases": ["Term End"], "type": "date",
152
+ "computed": { "from": "effective_date", "op": "+", "value": "2 years" } },
153
+ "purchase_amount": { "aliases": ["Purchase Amount"], "type": "money", "currency": "USD" },
154
+ "blank": { "aliases": ["_____________"], "type": "money", "currency": "USD",
155
+ "positions": [{ "role": "valuation_cap" }, { "role": "purchase_amount" }] }
188
156
  }
189
157
  ```
190
158
 
191
- With a schema, `draft-cli` substitutes **only** declared parameters and
192
- leaves other bracketed text untouched. Without one, every detected
193
- bracketed phrase is treated as a required parameter.
159
+ With a schema, `draft-cli` substitutes **only** declared parameters and leaves other bracketed text untouched. Without one, every detected bracketed phrase is treated as a required parameter.
160
+
161
+ The **short form** is just an aliases map: `{ "party_a": ["Party A"] }`.
194
162
 
195
- ---
163
+ See [PARAM_SCHEMA.md](PARAM_SCHEMA.md) §5 for the full schema contract and the locked design decisions per v2 feature.
196
164
 
197
165
  ## Command reference
198
166
 
@@ -203,10 +171,15 @@ draft - template body on stdin
203
171
  draft --demo bundled demo, no file needed
204
172
  draft --list-placeholders <t> enumerate placeholders and exit
205
173
  draft --validate <t> --params completeness check, no output
174
+ draft --bundle <bundle.json> multi-doc bundle mode (v0.7.0)
206
175
 
207
176
  OPTIONS
208
177
  --params FILE JSON file of param values (snake_case keys)
209
- -o, --output PATH write to PATH (default: stdout)
178
+ --parties PATH parties.json registry for ref:parties.<key>.<field> (v0.6.0)
179
+ --bundle PATH fill multiple templates in one invocation (v0.7.0)
180
+ --from-deal PATH LLM-extract values from a free-form deal description (v0.8.0)
181
+ -o, --output PATH write to PATH (default: stdout). `-` forces stdout.
182
+ For .docx input, default is <basename>-filled.docx (v0.2.0)
210
183
  --syntax bracket|mustache
211
184
  -i, --interactive prompt for missing required parameters
212
185
  --why structured explanation to stderr
@@ -214,7 +187,7 @@ OPTIONS
214
187
  -q, --silent suppress all stderr (warnings, --why, notes)
215
188
  --no-heuristic disable tier 4
216
189
  --yes-heuristic substitute tier-4 matches without confirmation
217
- --no-llm disable tier 5 even when env is configured
190
+ --no-llm disable tier 5 + --from-deal even when env is configured
218
191
  --llm assert that env is configured (fail-fast if not)
219
192
  --check-llm one-token roundtrip to verify provider config
220
193
  --diff show substitution table without writing output
@@ -224,81 +197,46 @@ OPTIONS
224
197
  -V, --version show version
225
198
  ```
226
199
 
227
- Exit codes: `0` ok · `1` i/o · `2` validation · `3` template-vault failure
228
- · `4` llm failure.
229
-
230
- ---
200
+ Exit codes: `0` ok · `1` i/o · `2` validation · `3` template-vault failure · `4` schema validation / typed parameter / computed / ref / positional · `5` llm failure.
231
201
 
232
202
  ## LLM tier (env-gated, opt-in)
233
203
 
234
- When tiers 1–4 all find nothing, `draft-cli` falls back to a language model
235
- **only if** a provider key is in the environment. Read order: `.env` in
236
- the working directory, then `process.env` (process wins).
204
+ When tiers 1–4 all find nothing, `draft-cli` falls back to a language model **only if** a provider key is in the environment. Read order: `.env` in the working directory, then `process.env` (process wins).
237
205
 
238
206
  ```sh
239
207
  echo 'ANTHROPIC_API_KEY=sk-ant-…' >> .env
240
208
  draft some-freeform-draft.md # tier 5 auto-runs when 1-4 empty
241
209
  ```
242
210
 
243
- Supported providers: Anthropic (`ANTHROPIC_API_KEY`), OpenAI
244
- (`OPENAI_API_KEY`), or explicit (`DRAFT_LLM_PROVIDER` + `DRAFT_LLM_API_KEY`
245
- + optional `DRAFT_LLM_MODEL`). The LLM receives template text only — no
246
- params file, no `.env` contents, no other data. Pass `--no-llm` to disable
247
- even when configured.
211
+ Supported providers: Anthropic (`ANTHROPIC_API_KEY`), OpenAI (`OPENAI_API_KEY`), or explicit (`DRAFT_LLM_PROVIDER` + `DRAFT_LLM_API_KEY` + optional `DRAFT_LLM_MODEL`). The LLM receives template text only — no params file, no `.env` contents, no other data. Pass `--no-llm` to disable even when configured.
248
212
 
249
- ---
213
+ **v0.8.0 inverse direction — `--from-deal PATH`:** feed prose deal notes and the LLM extracts values for the schema's placeholders (instead of inferring where placeholders are). Uses the same provider config. Errors on provider missing, network failure, or non-JSON response. CLI / `--params` values always win over LLM-inferred ones. See [PARAM_SCHEMA.md](PARAM_SCHEMA.md) §5.
250
214
 
251
215
  ## Composability
252
216
 
253
- `draft-cli` reads from stdin, writes to stdout by default, and exits with
254
- distinct codes for each failure class. It composes with `template-vault-cli`
255
- on the read side and `nda-review-cli` / `docx2pdf-cli` / `sign-cli` on
256
- the write side:
217
+ `draft-cli` reads from stdin, writes to stdout by default, and exits with distinct codes for each failure class. It composes with `template-vault-cli` on the read side and `nda-review-cli` / `docx2pdf-cli` / `sign-cli` on the write side:
257
218
 
258
- ```sh
259
- template-vault get nda/house-mutual \
260
- | draft - --params deal-acme.json \
261
- | nda-review review - --playbook house \
219
+ ```bash
220
+ # Pull a versioned template, fill it, hand it to review, convert to PDF, sign.
221
+ template-vault get nda/house-mutual@v3 \
222
+ | draft - --from-deal deal-notes.txt --parties parties.json \
223
+ | nda-review --redline \
262
224
  | docx2pdf - draft.pdf
225
+ sign-cli send draft.pdf --signer counsel@counterparty.com
263
226
  ```
264
227
 
265
- The `--why` and `--json` flags make every step inspectable by agents and
266
- shell pipelines.
267
-
268
- ---
269
-
270
- ## Part of the contract-operations suite
271
-
272
- `draft-cli` is one of a small set of single-purpose CLIs for contract
273
- operations. See [cli.drbaher.com](https://cli.drbaher.com) for the suite
274
- landing page.
275
-
276
- - **[nda-review-cli](https://github.com/DrBaher/nda-review-cli)** —
277
- draft, review, and negotiate NDAs against your own house playbook.
278
- Deterministic by default; opt-in LLM augmentation.
279
- - **[docx2pdf-cli](https://github.com/DrBaher/docx2pdf-cli)** —
280
- honest DOCX → PDF conversion with batch processing, parallel runs,
281
- font validation.
282
- - **[sign-cli](https://github.com/DrBaher/sign-cli)** —
283
- fully-offline PAdES e-signature with hash-chained audit events,
284
- RFC 3161 timestamps.
285
-
286
- `template-vault-cli` (a Git-backed, clause-aware package manager for
287
- legal-document templates) is the natural upstream of `draft-cli` and
288
- will join the suite when it ships.
289
-
290
- ---
228
+ Each tool stays small and replaceable. None of them know about each other beyond the standard `stdin / stdout / argv / exit codes` contract.
291
229
 
292
230
  ## Documentation
293
231
 
294
- - [GETTING_STARTED.md](GETTING_STARTED.md) — 10-minute walk-through of every flow.
232
+ - [GETTING_STARTED.md](GETTING_STARTED.md) — 10-minute walkthrough of the main flows.
295
233
  - [AGENTS.md](AGENTS.md) — JSON shapes, exit codes, library use; everything an LLM agent driving `draft-cli` needs.
296
- - [PARAM_SCHEMA.md](PARAM_SCHEMA.md) — locked v1 contract: cascade, schema file, precedence.
297
- - [ARCHITECTURE.md](ARCHITECTURE.md) — single-file rationale, substitution model, .docx parsing.
298
- - [FAQ.md](FAQ.md) — design questions and trade-offs.
299
- - [SECURITY.md](SECURITY.md) — threat model and how to report a vulnerability.
300
- - [CHANGELOG.md](CHANGELOG.md) — release notes and the v2 "Deferred" list.
301
- - [CONTRIBUTING.md](CONTRIBUTING.md) — scope rules and release flow.
234
+ - [ARCHITECTURE.md](ARCHITECTURE.md) — how the cascade, schema, and substitution pipeline fit together.
235
+ - [PARAM_SCHEMA.md](PARAM_SCHEMA.md) — v1 + v2 schema contract; locked decisions per feature.
236
+ - [FAQ.md](FAQ.md) — common questions about detection rules, T4 heuristics, and the LLM tier.
237
+ - [V2_BRIEFS_REMAINING.md](V2_BRIEFS_REMAINING.md) — design briefs for the four v2 items shipped after v0.1.x; historical now that all four have landed.
238
+ - [SECURITY.md](SECURITY.md) — reporting vulnerabilities.
239
+ - [CHANGELOG.md](CHANGELOG.md) — every shipped version with locked design decisions.
302
240
 
303
241
  ## License
304
242
 
package/draft-cli.mjs CHANGED
@@ -70,7 +70,7 @@ import { fileURLToPath } from "node:url";
70
70
  */
71
71
 
72
72
  /** @type {string} */
73
- export const VERSION = "0.7.0";
73
+ export const VERSION = "0.8.1";
74
74
 
75
75
  // ─── EXIT CODES ─────────────────────────────────────────────────────────────
76
76
  /**
@@ -279,6 +279,7 @@ export function parseArgs(argv) {
279
279
  if (a === "--params") { opts.params = argv[++i]; continue; }
280
280
  if (a === "--parties") { opts.parties = argv[++i]; continue; }
281
281
  if (a === "--bundle") { opts.bundle = argv[++i]; continue; }
282
+ if (a === "--from-deal") { opts.fromDeal = argv[++i]; continue; }
282
283
  if (a === "--output" || a === "-o") { opts.output = argv[++i]; continue; }
283
284
  if (a === "--syntax") {
284
285
  const v = argv[++i];
@@ -808,6 +809,95 @@ ${body.slice(0, 12000)}`;
808
809
  return out;
809
810
  }
810
811
 
812
+ /**
813
+ * v2 #4: LLM inference from a free-form deal description.
814
+ *
815
+ * Takes the prose deal description (the user's notes about parties, dates,
816
+ * amounts, etc.) and asks the configured T5 LLM provider to extract values
817
+ * for the placeholders the cascade has already detected. Returns
818
+ * `{values, extraKeys, warnings}`:
819
+ *
820
+ * - values: `{key: string}` for every placeholder key the LLM filled
821
+ * - extraKeys: any keys the LLM emitted that aren't in the placeholders list (Q4.2 → warn)
822
+ * - warnings: human-readable messages for malformed entries
823
+ *
824
+ * Throws on missing provider config, missing `fetch`, network/HTTP error, or
825
+ * non-JSON LLM response — same failure boundaries as `detectLlm`.
826
+ *
827
+ * @param {string} dealText — free-form deal description
828
+ * @param {Placeholder[]} placeholders — the post-detection placeholder list
829
+ * @param {ReturnType<llmProviderFromEnv>} providerCfg
830
+ * @param {{ fetcher?: typeof fetch | null }} [opts]
831
+ * @returns {Promise<{ values: Object<string,string>, extraKeys: string[], warnings: string[] }>}
832
+ */
833
+ export async function inferFromDeal(dealText, placeholders, providerCfg, { fetcher = (typeof fetch !== "undefined" ? fetch : null) } = {}) {
834
+ if (!fetcher) {
835
+ const e = new Error("fetch is not available; Node 18+ is required for --from-deal");
836
+ e.exitCode = EXIT.LLM;
837
+ throw e;
838
+ }
839
+ if (!providerCfg) {
840
+ const e = new Error("--from-deal requires an LLM provider; set ANTHROPIC_API_KEY / OPENAI_API_KEY / DRAFT_LLM_* in .env");
841
+ e.exitCode = EXIT.LLM;
842
+ throw e;
843
+ }
844
+ const wantedKeys = placeholders.map((p) => ({
845
+ key: p.key,
846
+ aliases: (p.aliases || []).slice(0, 4),
847
+ first_seen_as: p.first_seen_as,
848
+ }));
849
+ if (wantedKeys.length === 0) {
850
+ return { values: {}, extraKeys: [], warnings: [] };
851
+ }
852
+ const fieldList = wantedKeys.map((w) =>
853
+ ` - ${w.key} (template placeholder: "${w.first_seen_as}"${w.aliases.length > 1 ? `; aliases: ${w.aliases.join(", ")}` : ""})`
854
+ ).join("\n");
855
+ const prompt = `You are filling parameters for a legal-document drafting tool.
856
+ A user has written prose describing a deal. Extract values for the following
857
+ fields from the deal description. Output JSON ONLY in this exact shape, with
858
+ no commentary:
859
+
860
+ {"values":{"<key>":"<extracted_value>",...}}
861
+
862
+ If a field can't be confidently extracted from the description, omit it (do
863
+ NOT guess). Do not invent additional fields not in the list. Match the deal's
864
+ language verbatim — don't reformat dates, currencies, or names.
865
+
866
+ FIELDS:
867
+ ${fieldList}
868
+
869
+ DEAL DESCRIPTION:
870
+ ${dealText.slice(0, 12000)}`;
871
+ const raw = await callLlm(providerCfg, prompt, fetcher);
872
+ let parsed;
873
+ try {
874
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
875
+ parsed = JSON.parse(jsonMatch ? jsonMatch[0] : raw);
876
+ } catch {
877
+ const e = new Error(`LLM returned non-JSON response for --from-deal`);
878
+ e.exitCode = EXIT.LLM;
879
+ throw e;
880
+ }
881
+ const rawValues = (parsed && typeof parsed.values === "object" && parsed.values) ? parsed.values : {};
882
+ const knownKeys = new Set(placeholders.map((p) => p.key));
883
+ const values = {};
884
+ const extraKeys = [];
885
+ const warnings = [];
886
+ for (const [k, v] of Object.entries(rawValues)) {
887
+ if (!knownKeys.has(k)) {
888
+ extraKeys.push(k);
889
+ continue;
890
+ }
891
+ if (v === null || v === undefined) continue;
892
+ if (typeof v !== "string" && typeof v !== "number") {
893
+ warnings.push(`--from-deal: value for "${k}" was ${typeof v}, expected string; skipped`);
894
+ continue;
895
+ }
896
+ values[k] = String(v);
897
+ }
898
+ return { values, extraKeys, warnings };
899
+ }
900
+
811
901
  async function callLlm(cfg, prompt, fetcher) {
812
902
  if (cfg.provider === "anthropic") {
813
903
  const r = await fetcher("https://api.anthropic.com/v1/messages", {
@@ -1460,7 +1550,7 @@ export function loadBundle(path) {
1460
1550
  * @param {{ prompter?: (p: Placeholder) => Promise<string|null> }} [io]
1461
1551
  * @returns {Promise<ResolvedValues>}
1462
1552
  */
1463
- export async function resolveValues(placeholders, opts, paramsObj, { prompter = nodePrompter } = {}) {
1553
+ export async function resolveValues(placeholders, opts, paramsObj, { prompter = nodePrompter, inferred = null } = {}) {
1464
1554
  const resolved = {};
1465
1555
  const missing = [];
1466
1556
  const sources = {};
@@ -1475,6 +1565,12 @@ export async function resolveValues(placeholders, opts, paramsObj, { prompter =
1475
1565
  sources[p.key] = "params";
1476
1566
  continue;
1477
1567
  }
1568
+ // v2 #4: --from-deal LLM-inferred values, between --params and --interactive.
1569
+ if (inferred && Object.prototype.hasOwnProperty.call(inferred, p.key)) {
1570
+ resolved[p.key] = String(inferred[p.key]);
1571
+ sources[p.key] = "deal-llm";
1572
+ continue;
1573
+ }
1478
1574
  if (opts.interactive) {
1479
1575
  const v = await prompter(p);
1480
1576
  if (v !== null && v !== undefined && v !== "") {
@@ -2089,7 +2185,7 @@ export async function cmdListPlaceholders(opts, input, schema, envObj, { fetcher
2089
2185
  return EXIT.OK;
2090
2186
  }
2091
2187
 
2092
- export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null } = {}) {
2188
+ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null, dealText = null } = {}) {
2093
2189
  const result = await runCascade(input, opts, schema, envObj, { fetcher });
2094
2190
  if (result.tier === "none") {
2095
2191
  err.write(paint("error: no placeholders detected by any tier\n", "red", err));
@@ -2109,7 +2205,24 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
2109
2205
  }
2110
2206
  return EXIT.VALIDATION;
2111
2207
  }
2112
- const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj);
2208
+ // v2 #4: --from-deal LLM inference (when dealText is present and
2209
+ // --no-llm not set). Provider config comes from env. Errors are fatal
2210
+ // to keep the user from running with partial inferred values.
2211
+ let inferred = null;
2212
+ if (dealText && !opts.noLlm) {
2213
+ try {
2214
+ const r = await inferFromDeal(dealText, result.placeholders, llmProviderFromEnv(envObj), { fetcher });
2215
+ inferred = r.values;
2216
+ for (const k of r.extraKeys) {
2217
+ err.write(paint(`warning: --from-deal LLM emitted unknown key "${k}" (not in template/schema)\n`, "yellow", err));
2218
+ }
2219
+ for (const w of r.warnings) err.write(paint(`warning: ${w}\n`, "yellow", err));
2220
+ } catch (e) {
2221
+ err.write(paint(`error: ${e.message}\n`, "red", err));
2222
+ return e.exitCode || EXIT.LLM;
2223
+ }
2224
+ }
2225
+ const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj, { inferred });
2113
2226
  if (missing.length > 0) {
2114
2227
  printMissing(missing, err);
2115
2228
  if (opts.json) {
@@ -2170,7 +2283,7 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
2170
2283
  return EXIT.OK;
2171
2284
  }
2172
2285
 
2173
- export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null } = {}) {
2286
+ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null, dealText = null } = {}) {
2174
2287
  const result = await runCascade(input, opts, schema, envObj, { fetcher });
2175
2288
  if (result.tier === "none") {
2176
2289
  const hasProvider = Boolean(llmProviderFromEnv(envObj));
@@ -2221,7 +2334,25 @@ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher
2221
2334
  return EXIT.VALIDATION;
2222
2335
  }
2223
2336
 
2224
- const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj);
2337
+ // v2 #4: --from-deal LLM inference (when dealText is present and
2338
+ // --no-llm not set). Provider config comes from env. Errors are fatal
2339
+ // to keep the user from running with partial inferred values.
2340
+ let inferred = null;
2341
+ if (dealText && !opts.noLlm) {
2342
+ try {
2343
+ const r = await inferFromDeal(dealText, result.placeholders, llmProviderFromEnv(envObj), { fetcher });
2344
+ inferred = r.values;
2345
+ for (const k of r.extraKeys) {
2346
+ err.write(paint(`warning: --from-deal LLM emitted unknown key "${k}" (not in template/schema)\n`, "yellow", err));
2347
+ }
2348
+ for (const w of r.warnings) err.write(paint(`warning: ${w}\n`, "yellow", err));
2349
+ } catch (e) {
2350
+ err.write(paint(`error: ${e.message}\n`, "red", err));
2351
+ return e.exitCode || EXIT.LLM;
2352
+ }
2353
+ }
2354
+
2355
+ const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj, { inferred });
2225
2356
  // Footgun guard: flag --typo'd-key VALUE that didn't match any detected
2226
2357
  // placeholder. Without this warning, a typo'd flag is silently dropped and
2227
2358
  // the user sees only a "missing required" error without the connection.
@@ -2839,13 +2970,22 @@ export async function main(argv, io = {}) {
2839
2970
  return EXIT.IO;
2840
2971
  }
2841
2972
 
2842
- let input, schema, paramsObj, envObj, parties;
2973
+ let input, schema, paramsObj, envObj, parties, dealText;
2843
2974
  try {
2844
2975
  input = await resolveInput(opts.positional[0], { spawner, stdinReader });
2845
2976
  schema = loadSchema(input.path);
2846
2977
  paramsObj = loadParamsFile(opts.params);
2847
2978
  envObj = effectiveEnv(cwd, processEnv);
2848
2979
  parties = loadParties(opts.parties || null);
2980
+ // v2 #4: --from-deal PATH reads a free-form deal description.
2981
+ if (opts.fromDeal) {
2982
+ if (!existsSync(opts.fromDeal)) {
2983
+ const e = new Error(`deal description file not found: ${opts.fromDeal}`);
2984
+ e.exitCode = EXIT.IO;
2985
+ throw e;
2986
+ }
2987
+ dealText = readFileSync(opts.fromDeal, "utf8");
2988
+ }
2849
2989
  } catch (e) {
2850
2990
  err.write(paint(`error: ${e.message}\n`, "red", err));
2851
2991
  return e.exitCode || EXIT.IO;
@@ -2856,9 +2996,9 @@ export async function main(argv, io = {}) {
2856
2996
  return await cmdListPlaceholders(opts, input, schema, envObj, { fetcher, out, err });
2857
2997
  }
2858
2998
  if (opts.validate) {
2859
- return await cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties });
2999
+ return await cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties, dealText });
2860
3000
  }
2861
- return await cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties });
3001
+ return await cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties, dealText });
2862
3002
  } catch (e) {
2863
3003
  err.write(paint(`error: ${e.message}\n`, "red", err));
2864
3004
  return e.exitCode || EXIT.IO;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drbaher/draft-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Fill placeholders in a legal-document template with parameter values. Part of the contract-operations suite.",
5
5
  "type": "module",
6
6
  "bin": {