@drbaher/draft-cli 0.1.0
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/AGENTS.md +224 -0
- package/ARCHITECTURE.md +206 -0
- package/CHANGELOG.md +108 -0
- package/FAQ.md +190 -0
- package/GETTING_STARTED.md +263 -0
- package/LICENSE +21 -0
- package/PARAM_SCHEMA.md +341 -0
- package/README.md +305 -0
- package/SECURITY.md +76 -0
- package/draft-cli.mjs +1757 -0
- package/package.json +58 -0
package/PARAM_SCHEMA.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# PARAM_SCHEMA — v1 contract (locked)
|
|
2
|
+
|
|
3
|
+
This doc is the source of truth for how `draft-cli` discovers placeholders,
|
|
4
|
+
maps them to parameters, validates inputs, and reports results. Locked
|
|
5
|
+
after Q1–Q4 and D1–D4 review. Reviewer: DrBaher.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Stack & posture
|
|
10
|
+
|
|
11
|
+
- **Runtime:** Node.js ≥ 18 (global `fetch`, `node:test`, `--env-file`-style behavior re-implemented inline so we don't require ≥ 20.6).
|
|
12
|
+
- **Distribution:** `npm install -g @drbaher/draft-cli`, single-file `draft-cli.mjs` shebang executable.
|
|
13
|
+
- **Runtime dependencies (v1):** exactly one — `jszip` (MIT, zero transitive) for `.docx` unzip. Everything else uses Node's stdlib. LLM tier uses global `fetch` directly; no SDK dep.
|
|
14
|
+
- **Local-first.** No telemetry. The only network call is the optional LLM tier and only when explicitly configured (see §3 T5).
|
|
15
|
+
|
|
16
|
+
## 2. Inputs and outputs
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
draft <template> [flags]
|
|
20
|
+
draft - [flags] # template body from stdin
|
|
21
|
+
draft <cat>/<name>[@ver] ... # pulls via `template-vault get`
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Input forms accepted:** `path/to/file.md`, `path/to/file.txt`, `path/to/file.docx`, stdin (`-`), or a `template-vault` ref shaped `<category>/<name>[@version]`. Vault refs shell out to `template-vault get` — no library import.
|
|
25
|
+
- **Output:** stdout by default, `--output PATH` for files. Output is always plain text/markdown in v1 — `.docx` is **input-only** for v1. Writing `.docx` back is deferred to v2.
|
|
26
|
+
- **Encoding:** UTF-8 in, UTF-8 out. No BOM written; BOM tolerated on read.
|
|
27
|
+
|
|
28
|
+
## 3. Detection cascade (sequential-with-stop)
|
|
29
|
+
|
|
30
|
+
The cascade runs each tier in order. The first tier that returns **≥ 1 placeholder** wins and the others are skipped. The active tier is reported in `--why` and in `--json` output.
|
|
31
|
+
|
|
32
|
+
| Tier | Name | Deterministic | Default | Trigger to skip |
|
|
33
|
+
| ---- | -------------- | ------------- | ------- | -------------------------------- |
|
|
34
|
+
| T1 | Bracket | ✅ | on | `--syntax mustache` selected |
|
|
35
|
+
| T2 | Mustache | ✅ | opt-in | not selected via `--syntax` |
|
|
36
|
+
| T3 | DOCX highlight | ✅ | auto | input not `.docx` |
|
|
37
|
+
| T4 | Heuristic | ✅ | on | `--no-heuristic` |
|
|
38
|
+
| T5 | LLM | ❌ | env-gated | no LLM provider configured |
|
|
39
|
+
|
|
40
|
+
### T1 — Bracket `[...]`
|
|
41
|
+
|
|
42
|
+
A bracketed run is treated as a placeholder when **all** of:
|
|
43
|
+
|
|
44
|
+
1. `[...]`, no nested brackets, length 1–200.
|
|
45
|
+
2. **Not** immediately followed by `(` — i.e. not a markdown link
|
|
46
|
+
(`[label](url)` is skipped).
|
|
47
|
+
3. **Not** a checkbox marker — inner matches `[ xX]{1,3}` is skipped
|
|
48
|
+
(`[x]`, `[ ]`, `[X]`, etc.).
|
|
49
|
+
4. **Not** a pure section reference — inner matches `\d+(\.\d+)*$` is
|
|
50
|
+
skipped (`[3.1]`, `[4.2.1]`).
|
|
51
|
+
5. Inner contains **at least one letter** (excludes `[___]`, `[---]`).
|
|
52
|
+
6. Inner is **not entirely uppercase letters** (excludes
|
|
53
|
+
`[CONFIDENTIALITY]`, `[ARTICLE I]`).
|
|
54
|
+
|
|
55
|
+
Examples that match: `[Party A]`, `[Effective Date]`,
|
|
56
|
+
`[State of California]`, `[Today’s date]`, `[1 year(s)]`,
|
|
57
|
+
`[Fill in state]`,
|
|
58
|
+
`[Evaluating whether to enter into a business relationship with the other party.]`.
|
|
59
|
+
|
|
60
|
+
Examples that don't: `[3.1]`, `[ARTICLE I]`, `[CONFIDENTIALITY]`,
|
|
61
|
+
`[x]`, `[ ]`, `[the docs](https://example.com)`.
|
|
62
|
+
|
|
63
|
+
The rule is intentionally permissive because real legal templates
|
|
64
|
+
(Common Paper, YC SAFE, Bonterms) use sentence-shaped placeholders
|
|
65
|
+
with full punctuation. False positives are filtered via the
|
|
66
|
+
`<template>.params.json` schema (§5); false negatives in this domain
|
|
67
|
+
are higher-cost than false positives.
|
|
68
|
+
|
|
69
|
+
**Canonical key derivation** (when no schema is present): inner →
|
|
70
|
+
lowercase → non-alphanumeric runs collapsed to `_` → leading/trailing
|
|
71
|
+
`_` stripped → prefix `_` if leading char is a digit → truncated at 60
|
|
72
|
+
chars. So `[Party A]` → `party_a`, `[1 year(s)]` → `_1_year_s`,
|
|
73
|
+
`[Today’s date]` → `today_s_date`. Templates with long sentence-shaped
|
|
74
|
+
placeholders should ship a schema file to give them clean keys.
|
|
75
|
+
|
|
76
|
+
Cross-references like `[See Section 4]` *do* match T1 by design. The
|
|
77
|
+
**schema file** (§5) is the disambiguation tool: when present, only declared
|
|
78
|
+
keys substitute and other bracketed runs are left untouched.
|
|
79
|
+
|
|
80
|
+
### T2 — Mustache `{{...}}`
|
|
81
|
+
|
|
82
|
+
Opt-in via `--syntax mustache`. Matches `{{<inner>}}` where `<inner>` is
|
|
83
|
+
either Title Case (same rule as T1 inner) or snake_case `[a-z][a-z0-9_]{0,78}`.
|
|
84
|
+
|
|
85
|
+
Mixed-convention templates (both `[X]` and `{{X}}` present) emit a
|
|
86
|
+
`doctor`-style stderr warning. The selected `--syntax` family is the only
|
|
87
|
+
one substituted; the other is left untouched in output.
|
|
88
|
+
|
|
89
|
+
### T3 — DOCX highlight
|
|
90
|
+
|
|
91
|
+
Triggered only when input is `.docx`. Unzip with `jszip`, read
|
|
92
|
+
`word/document.xml`, regex-scan for highlight runs:
|
|
93
|
+
|
|
94
|
+
```xml
|
|
95
|
+
<w:r>
|
|
96
|
+
<w:rPr><w:highlight w:val="yellow"/></w:rPr>
|
|
97
|
+
<w:t>Acme Corporation</w:t>
|
|
98
|
+
</w:r>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Highlight colors recognized as placeholders: `yellow`, `green`, `cyan`,
|
|
102
|
+
`magenta` (Word's "highlight as TODO" colors). Black/white/auto highlights
|
|
103
|
+
are ignored.
|
|
104
|
+
|
|
105
|
+
The captured text becomes the bracket-equivalent. So `Acme Corporation`
|
|
106
|
+
in a yellow highlight is treated identically to `[Acme Corporation]` from
|
|
107
|
+
T1 — same canonical-key derivation, same alias machinery.
|
|
108
|
+
|
|
109
|
+
Output for .docx input is plain markdown: the text is extracted in
|
|
110
|
+
document order (one paragraph per `<w:p>`), highlights are replaced, and
|
|
111
|
+
the result is written to stdout or `--output`. Round-trip to `.docx`
|
|
112
|
+
is **v2**.
|
|
113
|
+
|
|
114
|
+
### T4 — Generic-name heuristic
|
|
115
|
+
|
|
116
|
+
A bundled dictionary (`config/heuristic.json` shipped in the wheel) lists
|
|
117
|
+
known generic placeholder values: `Acme Corporation`, `Acme Inc`,
|
|
118
|
+
`Foo Corp`, `John Doe`, `Jane Roe`, `123 Main Street`, `example@example.com`,
|
|
119
|
+
`555-555-1234`, `MM/DD/YYYY`, `TBD`, `[INSERT ___]`, etc. Curated, not
|
|
120
|
+
inferred.
|
|
121
|
+
|
|
122
|
+
Matches against the **untemplated body** (after T1–T3 ran and found
|
|
123
|
+
nothing). Case-sensitive whole-word matching.
|
|
124
|
+
|
|
125
|
+
**Safety gate (D3 locked):** T4 matches **never substitute silently**.
|
|
126
|
+
Behavior:
|
|
127
|
+
|
|
128
|
+
- In a TTY without `--yes-heuristic`: print each match, prompt `y/N`.
|
|
129
|
+
- In a non-TTY without `--yes-heuristic`: print a warning block and
|
|
130
|
+
**leave matches untouched**. Substitution requires explicit opt-in.
|
|
131
|
+
- With `--yes-heuristic`: substitute non-interactively (the user
|
|
132
|
+
has taken ownership).
|
|
133
|
+
- `--no-heuristic` disables T4 entirely.
|
|
134
|
+
|
|
135
|
+
Dictionary override: `--dictionary PATH` replaces (not extends) the bundled list.
|
|
136
|
+
|
|
137
|
+
### T5 — LLM (last resort, env-gated)
|
|
138
|
+
|
|
139
|
+
Runs only when **both** are true:
|
|
140
|
+
- T1–T4 produced zero placeholders.
|
|
141
|
+
- A provider is configured via env (read from `.env` in the working
|
|
142
|
+
directory or process env — process env wins).
|
|
143
|
+
|
|
144
|
+
Provider env vars (any one suffices):
|
|
145
|
+
- `ANTHROPIC_API_KEY` → Anthropic Messages API (default model: claude-sonnet-4-6).
|
|
146
|
+
- `OPENAI_API_KEY` → OpenAI Responses API (default model: gpt-4o-mini).
|
|
147
|
+
- `DRAFT_LLM_PROVIDER` + `DRAFT_LLM_API_KEY` (+ optional `DRAFT_LLM_MODEL`) → explicit override.
|
|
148
|
+
|
|
149
|
+
If no provider env is present, the cascade **stops at T4** and the CLI
|
|
150
|
+
errors with a clear message: `no placeholders detected by deterministic
|
|
151
|
+
tiers; set ANTHROPIC_API_KEY (or equivalent) in .env to enable LLM
|
|
152
|
+
detection, or pass --syntax mustache if your template uses {{...}}.`
|
|
153
|
+
|
|
154
|
+
The LLM call sends template text **only**, no params file, no user data.
|
|
155
|
+
Prompt asks for a JSON array of placeholder spans with `start`, `end`,
|
|
156
|
+
`suggested_key`. Result is validated against the same canonical-key
|
|
157
|
+
rules; invalid entries are dropped with a warning.
|
|
158
|
+
|
|
159
|
+
`--no-llm` disables T5 even when env is configured (the cascade ends at
|
|
160
|
+
T4). `--llm` asserts that an LLM provider should be available and
|
|
161
|
+
fail-fasts with a clear error if none is configured; it does **not**
|
|
162
|
+
override the sequential-with-stop semantics. Running T5 on top of an
|
|
163
|
+
earlier-tier hit (the "find missed generics in a bracketed template"
|
|
164
|
+
workflow) is a v2 candidate, not v1 behavior.
|
|
165
|
+
|
|
166
|
+
## 4. Key conventions
|
|
167
|
+
|
|
168
|
+
Three surfaces, one canonical key per parameter.
|
|
169
|
+
|
|
170
|
+
| Surface | Form | Example |
|
|
171
|
+
| ------------- | --------------------- | ---------------- |
|
|
172
|
+
| Match source | Title Case w/ spaces | `Party A Name` |
|
|
173
|
+
| Canonical key | snake_case | `party_a_name` |
|
|
174
|
+
| CLI flag | kebab-case | `--party-a-name` |
|
|
175
|
+
| JSON file key | snake_case | `"party_a_name"` |
|
|
176
|
+
|
|
177
|
+
Derivation when no schema is present: match text → lowercase, spaces → underscores. `Party A Name` → `party_a_name`. The original match text is preserved in output (case/spacing intact); we replace byte-for-byte.
|
|
178
|
+
|
|
179
|
+
Disallowed in placeholders (will error if found in a schema file): dots, slashes, leading digits, hyphens inside the match text. Reserved schema-file keys: `_meta`, `_aliases`, `_required`, `_defaults`.
|
|
180
|
+
|
|
181
|
+
## 5. Schema file: `<template>.params.json`
|
|
182
|
+
|
|
183
|
+
Sibling file, opt-in. If absent, placeholders are inferred by the active
|
|
184
|
+
cascade tier; every inferred placeholder is treated as required; keys are
|
|
185
|
+
auto-derived.
|
|
186
|
+
|
|
187
|
+
If present, the schema is **authoritative**: only declared parameters
|
|
188
|
+
substitute. Anything else the cascade detects is left untouched and
|
|
189
|
+
listed in `--why` as `unmapped`.
|
|
190
|
+
|
|
191
|
+
**Schema rescue (T1/T2 only).** When a schema declares a phrase that the
|
|
192
|
+
heuristic detection rule would reject (e.g. all-caps `[COMPANY]`, all-
|
|
193
|
+
underscore `[_____________]`, snake_case `[party_a]`), the schema's
|
|
194
|
+
alias list is consulted during detection and rescues that phrase.
|
|
195
|
+
Without this, a schema-declared alias would be silently dropped before
|
|
196
|
+
ever reaching the resolution step. The rescue applies only to bracket
|
|
197
|
+
(T1) and mustache (T2) tiers; T3/T4/T5 use text-based detection that
|
|
198
|
+
doesn't need rescuing.
|
|
199
|
+
|
|
200
|
+
### Short form (default in docs)
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"party_a": ["Party A", "Disclosing Party"],
|
|
205
|
+
"party_b": ["Party B", "Receiving Party"],
|
|
206
|
+
"effective_date": ["Effective Date"]
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Each value is a list of phrase forms (bracket inner text, mustache inner
|
|
211
|
+
text, or highlighted text). The canonical key is **NOT** implicitly in
|
|
212
|
+
its own alias list (Q3 locked) — list it explicitly if needed.
|
|
213
|
+
|
|
214
|
+
### Long form (with `_meta`)
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"_meta": { "schema_version": 1 },
|
|
219
|
+
"party_a": { "aliases": ["Party A"], "required": true },
|
|
220
|
+
"effective_date": { "aliases": ["Effective Date"], "required": false, "default": "the date first written above" }
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Parser selects long form iff a top-level `_meta` key is present. Short
|
|
225
|
+
and long are not mixable within one file.
|
|
226
|
+
|
|
227
|
+
### Orphan handling (Q4 locked)
|
|
228
|
+
|
|
229
|
+
Schema declares a key whose alias list matches no detected phrase →
|
|
230
|
+
**error**, exit 2. Catches drift early.
|
|
231
|
+
|
|
232
|
+
## 6. Precedence
|
|
233
|
+
|
|
234
|
+
CLI flag > JSON `--params` file > `--interactive` prompt > schema `default` > error.
|
|
235
|
+
|
|
236
|
+
- CLI flag present (even `""`) wins.
|
|
237
|
+
- JSON value present wins over prompt and default.
|
|
238
|
+
- `--interactive` set AND still missing → prompt.
|
|
239
|
+
- Schema `default` present AND still missing → use the default.
|
|
240
|
+
- Still missing → error, exit 2.
|
|
241
|
+
|
|
242
|
+
## 7. Validation, modes, errors
|
|
243
|
+
|
|
244
|
+
### `draft --validate <template> --params FILE`
|
|
245
|
+
|
|
246
|
+
Same lookup, never writes output. Exits 0 if every required key resolves;
|
|
247
|
+
2 otherwise. Honors all five cascade tiers and the schema if present.
|
|
248
|
+
|
|
249
|
+
### `draft --list-placeholders <template>`
|
|
250
|
+
|
|
251
|
+
Prints detected placeholders in first-appearance order, deduplicated.
|
|
252
|
+
With `--json`:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"template": "nda/house-mutual",
|
|
257
|
+
"tier": "bracket",
|
|
258
|
+
"placeholders": [
|
|
259
|
+
{ "key": "party_a", "first_seen_as": "Party A",
|
|
260
|
+
"aliases": ["Party A", "Disclosing Party"],
|
|
261
|
+
"required": true, "occurrences": 4, "tier": "bracket" }
|
|
262
|
+
],
|
|
263
|
+
"warnings": []
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Error shapes (stderr, red on TTY, honors `NO_COLOR`/`FORCE_COLOR`)
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
error: missing required parameter(s):
|
|
271
|
+
- party_a (matched: [Party A], [Disclosing Party])
|
|
272
|
+
supply --party-a or set "party_a" in --params
|
|
273
|
+
- effective_date (matched: [Effective Date])
|
|
274
|
+
supply --effective-date or set "effective_date" in --params
|
|
275
|
+
hint: run `draft --list-placeholders nda/house-mutual` to see all parameters.
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
error: mixed placeholder conventions in template (4 bracket, 2 mustache).
|
|
280
|
+
note: pass --syntax bracket or --syntax mustache; the other family is left untouched.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
error: schema declares "party_c" with aliases ["Party C","Third Party"],
|
|
285
|
+
but no matching phrase was detected by tier 'bracket'.
|
|
286
|
+
hint: remove the entry from the schema, or add the phrase to the template.
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
error: no placeholders detected by deterministic tiers (bracket, mustache,
|
|
291
|
+
docx-highlight, heuristic).
|
|
292
|
+
hint: set ANTHROPIC_API_KEY in .env to enable LLM detection,
|
|
293
|
+
or pass --syntax mustache if your template uses {{...}}.
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Exit codes: `0` success, `1` template/input I/O error, `2` validation
|
|
297
|
+
failure, `3` template-vault subprocess failure, `4` LLM tier failure
|
|
298
|
+
(network, auth, malformed response).
|
|
299
|
+
|
|
300
|
+
## 8. `--why` output
|
|
301
|
+
|
|
302
|
+
Structured stderr block (or stdout under `--json`):
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
draft: substituted 7 placeholders in nda/house-mutual → draft.md
|
|
306
|
+
why:
|
|
307
|
+
input = nda/house-mutual (via template-vault get)
|
|
308
|
+
tier = bracket
|
|
309
|
+
schema = nda/house-mutual.params.json (short form)
|
|
310
|
+
placeholders = 4 distinct, 12 occurrences
|
|
311
|
+
resolved = 4 (3 from CLI, 1 from --params, 0 interactive, 0 default)
|
|
312
|
+
defaulted = 0
|
|
313
|
+
unresolved = 0
|
|
314
|
+
unmapped = 1 ([See Section 4] — not in schema)
|
|
315
|
+
warnings = 0
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## 9. Out of scope for v1 (deferred — schema is forward-compatible)
|
|
319
|
+
|
|
320
|
+
- Computed placeholders (`[Effective Date + 2 years]`). Long-form schema reserves a future `"computed"` key.
|
|
321
|
+
- Typed parameters (`party`, `date`, `money`). Reserves a future `"type"` key.
|
|
322
|
+
- Cross-template parameter registry (`parties.json`). Additive; would layer underneath `--params` in precedence.
|
|
323
|
+
- `.docx` output round-trip.
|
|
324
|
+
- LLM-assisted suggestion *from a deal description* (current T5 only suggests from template text).
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Locked decisions (audit trail)
|
|
329
|
+
|
|
330
|
+
| ID | Question | Decision |
|
|
331
|
+
| --- | --------------------------------------- | -------- |
|
|
332
|
+
| Q1 | Cross-references like [See Section 4] | Subsumed: schema file disambiguates; T4/T5 expand coverage. |
|
|
333
|
+
| Q2 | Short-form vs long-form schema | Both supported; `_meta` selects long form. |
|
|
334
|
+
| Q3 | Canonical key implicit-alias | No — explicit list only. |
|
|
335
|
+
| Q4 | Orphan schema declarations | Error, exit 2. |
|
|
336
|
+
| D1 | Cascade semantics | Sequential-with-stop. |
|
|
337
|
+
| D2 | LLM default behavior | Env-gated auto-fallback at T4 boundary. |
|
|
338
|
+
| D3 | Heuristic safety gate | Warn-only, requires `--yes-heuristic` or interactive confirm. |
|
|
339
|
+
| D4 | .docx parsing | `jszip` + regex on `word/document.xml`. |
|
|
340
|
+
|
|
341
|
+
*End of contract. Code begins once approved.*
|
package/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# draft-cli
|
|
2
|
+
|
|
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
|
+
|
|
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)).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What it does
|
|
15
|
+
|
|
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.
|
|
21
|
+
|
|
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
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or via a params file:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
draft nda/house-mutual --params deal-acme.json --output draft.md
|
|
34
|
+
```
|
|
35
|
+
|
|
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.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install -g @drbaher/draft-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or run without installing:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npx @drbaher/draft-cli@latest --demo
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires Node.js ≥ 18. Tested on Ubuntu and macOS, Node 18 / 20 / 22.
|
|
56
|
+
|
|
57
|
+
### Shell completion
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
# bash
|
|
61
|
+
draft --completion bash >> ~/.bashrc
|
|
62
|
+
|
|
63
|
+
# zsh
|
|
64
|
+
draft --completion zsh > ~/.zsh/completions/_draft
|
|
65
|
+
# ensure ~/.zsh/completions is in fpath, then: autoload -U compinit && compinit
|
|
66
|
+
```
|
|
67
|
+
|
|
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.
|
|
90
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## End-to-end transcript
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
$ cat > nda.md <<'EOF'
|
|
107
|
+
This Agreement is between [Party A] and [Party B], effective [Effective Date].
|
|
108
|
+
[Party A] and [Party B] agree to keep confidential information confidential.
|
|
109
|
+
EOF
|
|
110
|
+
|
|
111
|
+
$ draft --list-placeholders nda.md
|
|
112
|
+
party_a (Party A) ×2 [tier=bracket]
|
|
113
|
+
party_b (Party B) ×2 [tier=bracket]
|
|
114
|
+
effective_date (Effective Date) ×1 [tier=bracket]
|
|
115
|
+
|
|
116
|
+
$ draft nda.md --party-a "Acme" --party-b "Vendor Inc." --effective-date 2026-06-01
|
|
117
|
+
This Agreement is between Acme and Vendor Inc., effective 2026-06-01.
|
|
118
|
+
Acme and Vendor Inc. agree to keep confidential information confidential.
|
|
119
|
+
|
|
120
|
+
$ cat > deal.json <<'EOF'
|
|
121
|
+
{"party_a": "Acme", "party_b": "Vendor Inc.", "effective_date": "2026-06-01"}
|
|
122
|
+
EOF
|
|
123
|
+
|
|
124
|
+
$ draft nda.md --params deal.json --output draft.md --why
|
|
125
|
+
draft: substituted 3 of 3 placeholders → draft.md
|
|
126
|
+
why:
|
|
127
|
+
input = nda.md
|
|
128
|
+
tier = bracket
|
|
129
|
+
schema = (none, inferred)
|
|
130
|
+
placeholders = 3 distinct, 5 occurrences
|
|
131
|
+
resolved = 3 (0 from CLI, 3 from --params, 0 interactive, 0 default)
|
|
132
|
+
defaulted = 0
|
|
133
|
+
unresolved = 0
|
|
134
|
+
unmapped = 0
|
|
135
|
+
warnings = 0
|
|
136
|
+
|
|
137
|
+
$ draft --validate nda.md --params deal.json && echo "ok"
|
|
138
|
+
ok: 3 parameter(s) resolved
|
|
139
|
+
ok
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Detection cascade
|
|
145
|
+
|
|
146
|
+
`draft-cli` finds placeholders by trying five strategies in order. The
|
|
147
|
+
**first non-empty tier wins** and the others are skipped.
|
|
148
|
+
|
|
149
|
+
| Tier | Strategy | When |
|
|
150
|
+
| ---- | -------------------- | ----------------------------------------- |
|
|
151
|
+
| 1 | `[Title Case]` | Default. Matches Common Paper / YC SAFE / Bonterms convention. |
|
|
152
|
+
| 2 | `{{Title Case}}` | Opt-in with `--syntax mustache`. |
|
|
153
|
+
| 3 | `.docx` highlights | Auto on `.docx` input. Yellow / green / cyan / magenta runs. |
|
|
154
|
+
| 4 | Heuristic dictionary | Bundled list of generic names (`Acme Corporation`, `John Doe`, `example@example.com`, etc.). Warn-only by default. |
|
|
155
|
+
| 5 | LLM | Last resort. Runs only when `.env` or process env configures `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `DRAFT_LLM_*`. |
|
|
156
|
+
|
|
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.
|
|
161
|
+
|
|
162
|
+
See [PARAM_SCHEMA.md](PARAM_SCHEMA.md) for the full contract.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Schema file (optional)
|
|
167
|
+
|
|
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`):
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"_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" }
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
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.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Command reference
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
draft <template> fill placeholders and emit the result
|
|
201
|
+
draft <category>/<name> pull via `template-vault get`
|
|
202
|
+
draft - template body on stdin
|
|
203
|
+
draft --demo bundled demo, no file needed
|
|
204
|
+
draft --list-placeholders <t> enumerate placeholders and exit
|
|
205
|
+
draft --validate <t> --params completeness check, no output
|
|
206
|
+
|
|
207
|
+
OPTIONS
|
|
208
|
+
--params FILE JSON file of param values (snake_case keys)
|
|
209
|
+
-o, --output PATH write to PATH (default: stdout)
|
|
210
|
+
--syntax bracket|mustache
|
|
211
|
+
-i, --interactive prompt for missing required parameters
|
|
212
|
+
--why structured explanation to stderr
|
|
213
|
+
--json machine-readable result on stdout
|
|
214
|
+
-q, --silent suppress all stderr (warnings, --why, notes)
|
|
215
|
+
--no-heuristic disable tier 4
|
|
216
|
+
--yes-heuristic substitute tier-4 matches without confirmation
|
|
217
|
+
--no-llm disable tier 5 even when env is configured
|
|
218
|
+
--llm assert that env is configured (fail-fast if not)
|
|
219
|
+
--check-llm one-token roundtrip to verify provider config
|
|
220
|
+
--diff show substitution table without writing output
|
|
221
|
+
--dictionary PATH override the bundled heuristic dictionary
|
|
222
|
+
--<param-name> VALUE set a parameter directly (kebab → snake_case)
|
|
223
|
+
-h, --help show full help
|
|
224
|
+
-V, --version show version
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Exit codes: `0` ok · `1` i/o · `2` validation · `3` template-vault failure
|
|
228
|
+
· `4` llm failure.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## LLM tier (env-gated, opt-in)
|
|
233
|
+
|
|
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).
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
echo 'ANTHROPIC_API_KEY=sk-ant-…' >> .env
|
|
240
|
+
draft some-freeform-draft.md # tier 5 auto-runs when 1-4 empty
|
|
241
|
+
```
|
|
242
|
+
|
|
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.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Composability
|
|
252
|
+
|
|
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:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
template-vault get nda/house-mutual \
|
|
260
|
+
| draft - --params deal-acme.json \
|
|
261
|
+
| nda-review review - --playbook house \
|
|
262
|
+
| docx2pdf - draft.pdf
|
|
263
|
+
```
|
|
264
|
+
|
|
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
|
+
---
|
|
291
|
+
|
|
292
|
+
## Documentation
|
|
293
|
+
|
|
294
|
+
- [GETTING_STARTED.md](GETTING_STARTED.md) — 10-minute walk-through of every flow.
|
|
295
|
+
- [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.
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT. See [LICENSE](LICENSE).
|