Skip to main content

contract_cli/commands/
agent_info.rs

1// Canonical agent-info manifest per agent-cli-framework
2// schemas/agent-info.schema.json: `commands` keys are routable paths, values
3// are {description, aliases?, args, options}. Every key must satisfy
4// `contract <key> --help` — conformance walks them verbatim.
5
6use serde_json::json;
7
8use crate::error::Result;
9use crate::output::{print_raw, Ctx};
10
11fn arg(name: &str, ty: &str, required: bool, desc: &str) -> serde_json::Value {
12    json!({ "name": name, "kind": "positional", "type": ty, "required": required, "description": desc })
13}
14
15fn opt(name: &str, ty: &str, desc: &str) -> serde_json::Value {
16    json!({ "name": name, "type": ty, "required": false, "description": desc })
17}
18
19fn req_opt(name: &str, ty: &str, desc: &str) -> serde_json::Value {
20    json!({ "name": name, "type": ty, "required": true, "description": desc })
21}
22
23fn cmd(desc: &str, aliases: &[&str], args: Vec<serde_json::Value>, options: Vec<serde_json::Value>) -> serde_json::Value {
24    let mut v = json!({ "description": desc, "args": args, "options": options });
25    if !aliases.is_empty() {
26        v["aliases"] = json!(aliases);
27    }
28    v
29}
30
31pub fn run(_ctx: Ctx) -> Result<()> {
32    let config_path = crate::config::config_path()?.display().to_string();
33    let state_dir = crate::config::state_path()?.display().to_string();
34    let database = crate::config::db_path()?.display().to_string();
35
36    let slug = |d: &str| vec![arg("slug", "string", true, d)];
37    let number = || vec![arg("number", "string", true, "Contract number, e.g. NDA-acme-2026-0001")];
38
39    let entity_opts = |contract_side: bool| {
40        let mut o = vec![
41            opt("--name", "string", "Display name"),
42            opt("--legal-name", "string", "Legal entity name used on contracts"),
43            opt("--company-no", "string", "Registration / company number"),
44            opt("--jurisdiction", "string", "Jurisdiction (issuers: sg|uk|us|eu; clients: free text)"),
45            opt("--address", "string", "Address lines separated by \\n"),
46            opt("--email", "string", "Contact email"),
47        ];
48        if contract_side {
49            o.push(opt("--tax-id", "string", "Tax / VAT id"));
50            o.push(opt("--phone", "string", "Phone"));
51            o.push(opt("--logo", "string", "Path to logo image for the contract header"));
52            o.push(opt("--output-dir", "string", "Default output dir for render"));
53        } else {
54            o.push(opt("--attn", "string", "Attention line"));
55            o.push(opt("--country", "string", "ISO country code"));
56            o.push(opt("--notes", "string", "Free-form notes"));
57        }
58        o
59    };
60
61    let new_opts = vec![
62        req_opt("--kind", "string", "nda | ncnda | consulting | msa | sow | service | loan"),
63        opt("--as", "string", "Issuer slug (your side); falls back to client default, then config"),
64        req_opt("--client", "string", "Client slug (counterparty)"),
65        opt("--title", "string", "Contract title (defaults per kind)"),
66        opt("--effective", "string", "Effective date YYYY-MM-DD (default today)"),
67        opt("--end", "string", "End date YYYY-MM-DD (mutually exclusive with --term-months)"),
68        opt("--term-months", "int", "Term length in months"),
69        opt("--term-years", "int", "Term length in years (sugar for months × 12)"),
70        opt("--governing-law", "string", "Governing law, e.g. 'England and Wales'"),
71        opt("--venue", "string", "Court venue override"),
72        opt("--fee", "string", "type:amount:currency — fixed:8400:SGD | hourly:200:SGD | daily:1500:SGD | retainer:5000:SGD"),
73        opt("--fee-schedule", "string", "on-completion | monthly | on-milestone | upon-invoice"),
74        opt("--mutuality", "string", "nda/ncnda: mutual | unilateral"),
75        opt("--disclosing-side", "string", "nda: us | them | both"),
76        opt("--purpose", "string", "Purpose / scope summary"),
77        opt("--deliverable", "string", "Deliverable line (repeatable)"),
78        opt("--ip-assignment", "string", "client | consultant | shared"),
79        opt("--termination-notice-days", "int", "Notice days for termination for convenience"),
80        opt("--term", "string", "Arbitrary term key=value for pack {{vars}} (repeatable), e.g. principal_text='£10,000 (ten thousand pounds)'"),
81        opt("--pack", "string", "Clause pack slug (default: standard)"),
82        opt("--include", "string", "Extra pack clause slug to include (repeatable)"),
83        opt("--exclude", "string", "Default pack clause slug to drop (repeatable)"),
84        opt("--template", "string", "Default render template for this contract"),
85        opt("--notes", "string", "Internal notes (never rendered)"),
86    ];
87
88    let render_opts = vec![
89        opt("--template", "string", "Template override (see: template list)"),
90        opt("--out", "string", "Output PDF path"),
91        opt("--open", "bool", "Open the PDF after rendering"),
92        opt("--draft", "bool", "Force the DRAFT watermark"),
93        opt("--final", "bool", "Force a clean copy (no watermark)"),
94    ];
95
96    let commands = json!({
97        "issuer add": cmd("Register an issuer (your side; shared with invoice-cli)", &["issuer new"], slug("Issuer slug"), entity_opts(true)),
98        "issuer edit": cmd("Update issuer fields", &[], slug("Issuer slug"), {
99            let mut o = entity_opts(true);
100            o.push(opt("--logo-clear", "bool", "Remove the stored logo"));
101            o
102        }),
103        "issuer list": cmd("List issuers (shared with invoice-cli)", &["issuer ls"], vec![], vec![]),
104        "issuer show": cmd("Show one issuer", &["issuer get"], slug("Issuer slug"), vec![]),
105        "issuer delete": cmd("Delete an issuer", &["issuer rm"], slug("Issuer slug"), vec![]),
106        "clients add": cmd("Register a counterparty (shared with invoice-cli)", &["client add", "clients new"], slug("Client slug"), entity_opts(false)),
107        "clients edit": cmd("Update client fields (legal-name/company-no/jurisdiction feed the party block)", &["client edit"], slug("Client slug"), entity_opts(false)),
108        "clients list": cmd("List clients (shared with invoice-cli)", &["clients ls"], vec![], vec![]),
109        "clients show": cmd("Show one client", &["clients get"], slug("Client slug"), vec![]),
110        "clients delete": cmd("Delete a client", &["clients rm"], slug("Client slug"), vec![]),
111        "new": cmd("Create a contract from a clause pack", &["contracts new", "contracts create"], vec![], new_opts),
112        "list": cmd("List contracts", &["ls", "contracts list"], vec![], vec![
113            opt("--kind", "string", "Filter by kind"),
114            opt("--status", "string", "Filter by status"),
115            opt("--as", "string", "Filter by issuer slug"),
116        ]),
117        "show": cmd("Show one contract + clause list", &["get", "contracts show"], number(), vec![]),
118        "edit": cmd("Edit DRAFT contract metadata (sent/signed are immutable)", &["contracts edit"], number(), vec![
119            opt("--client", "string", "Re-point at a different client slug"),
120            opt("--title", "string", "New title"),
121            opt("--effective", "string", "Effective date YYYY-MM-DD"),
122            opt("--end", "string", "End date (clears term-months)"),
123            opt("--term-months", "int", "Term months (clears end date)"),
124            opt("--governing-law", "string", "Governing law"),
125            opt("--venue", "string", "Venue"),
126            opt("--fee", "string", "type:amount:currency"),
127            opt("--fee-schedule", "string", "on-completion | monthly | on-milestone | upon-invoice"),
128            opt("--term", "string", "Arbitrary term key=value (repeatable)"),
129            opt("--notes", "string", "Internal notes"),
130            opt("--template", "string", "Default template"),
131        ]),
132        "render": cmd("Render to PDF (DRAFT watermark unless executed or --final)", &["contracts render"], number(), render_opts),
133        "mark": cmd("Update status: draft|sent|signed|active|expired|terminated (forward-only once executed)", &["contracts mark"], vec![
134            arg("number", "string", true, "Contract number"),
135            arg("status", "string", true, "Target status"),
136        ], vec![]),
137        "sign": cmd("Record one party's signature; auto-bumps to signed when both sides sign", &["contracts sign"], number(), vec![
138            req_opt("--side", "string", "us | them"),
139            req_opt("--name", "string", "Signer full name"),
140            opt("--title", "string", "Signer title"),
141            opt("--date", "string", "Signature date YYYY-MM-DD (default today)"),
142            opt("--force", "bool", "Overwrite an already-recorded signature"),
143        ]),
144        "contracts clauses list": cmd("Show clauses attached to a contract", &["contracts clauses ls"], number(), vec![]),
145        "contracts clauses add": cmd("Add a pack clause, or a custom clause via --body/--from-file", &[], vec![
146            arg("number", "string", true, "Contract number"),
147            arg("slug", "string", true, "Clause slug"),
148        ], vec![
149            opt("--heading", "string", "Override heading"),
150            opt("--body", "string", "Custom body markdown"),
151            opt("--from-file", "string", "Custom body markdown from file"),
152            opt("--position", "int", "Zero-indexed insert position (default: end)"),
153        ]),
154        "contracts clauses edit": cmd("Override heading/body of an attached clause", &[], vec![
155            arg("number", "string", true, "Contract number"),
156            arg("slug", "string", true, "Clause slug"),
157        ], vec![
158            opt("--heading", "string", "New heading"),
159            opt("--body", "string", "New body markdown"),
160            opt("--from-file", "string", "New body markdown from file"),
161        ]),
162        "contracts clauses remove": cmd("Remove a clause", &["contracts clauses rm"], vec![
163            arg("number", "string", true, "Contract number"),
164            arg("slug", "string", true, "Clause slug"),
165        ], vec![]),
166        "contracts clauses move": cmd("Re-order a clause", &[], vec![
167            arg("number", "string", true, "Contract number"),
168            arg("slug", "string", true, "Clause slug"),
169            arg("position", "int", true, "New zero-indexed position"),
170        ], vec![]),
171        "contracts clauses reset": cmd("Reset the clause set to pack default", &[], number(), vec![]),
172        "duplicate": cmd("Clone a contract as a fresh draft (end date cleared, term kept)", &["clone", "contracts duplicate"], number(), vec![
173            opt("--client", "string", "Retarget to another client"),
174            opt("--as", "string", "Retarget to another issuer"),
175        ]),
176        "delete": cmd("Delete a contract (draft only unless --force)", &["rm", "contracts delete"], number(), vec![
177            opt("--force", "bool", "Allow deleting a non-draft contract"),
178        ]),
179        "pack list": cmd("List kind/pack combinations", &["pack ls", "packs list"], vec![], vec![]),
180        "pack show": cmd("Show available clauses for a kind+pack", &["pack get"], vec![arg("kind", "string", true, "Contract kind")], vec![
181            opt("--pack", "string", "Pack slug (default: standard)"),
182        ]),
183        "template list": cmd("List PDF templates with descriptions, moods, tags, fonts", &["template ls", "templates list"], vec![], vec![]),
184        "template find": cmd("Rank templates against a free-text description of the look (local scoring; caller picks)", &["template suggest"], vec![
185            arg("query", "string", true, "e.g. 'magazine masthead' or 'swiss minimal'"),
186        ], vec![]),
187        "template preview": cmd("Render a sample contract PDF with synthetic data", &[], vec![arg("name", "string", true, "Template name")], vec![
188            opt("--kind", "string", "Contract kind to preview (default consulting)"),
189            opt("--out", "string", "Output path"),
190        ]),
191        "kinds list": cmd("List contract kinds with descriptions and trigger tags", &["kinds ls", "kind list"], vec![], vec![]),
192        "kinds find": cmd("Rank contract kinds against a free-text description of the need (local scoring; caller picks)", &["kinds suggest"], vec![
193            arg("query", "string", true, "e.g. 'stop them going around me to my contact'"),
194        ], vec![]),
195        "config show": cmd("Display current configuration", &[], vec![], vec![]),
196        "config path": cmd("Print the config file path", &[], vec![], vec![]),
197        "config set": cmd("Set a config key", &[], vec![
198            arg("key", "string", true, "default_issuer | default_template | open_pdf | self_update"),
199            arg("value", "string", true, "New value"),
200        ], vec![]),
201        "agent-info": cmd("This manifest", &["info"], vec![], vec![]),
202        "doctor": cmd("Check typst, DB, packs, templates, shared entities", &[], vec![], vec![]),
203        "skill install": cmd("Install the embedded skill for Claude/Codex/Gemini", &[], vec![], vec![]),
204        "skill status": cmd("Report where the skill is installed and whether it is current", &[], vec![], vec![]),
205        "update": cmd("Distribution-aware update via brew or cargo", &[], vec![], vec![
206            opt("--check", "bool", "Check only — never mutates"),
207        ]),
208    });
209
210    let templates = crate::typst_assets::list_template_meta().unwrap_or_default();
211    let packs = crate::clauses::list_packs();
212    let kinds: Vec<serde_json::Value> = crate::kinds::KINDS
213        .iter()
214        .map(|k| json!({ "kind": k.kind, "description": k.description, "tags": k.tags }))
215        .collect();
216
217    let manifest = json!({
218        "name": "contract",
219        "version": env!("CARGO_PKG_VERSION"),
220        "description": env!("CARGO_PKG_DESCRIPTION"),
221        "commands": commands,
222        "global_flags": {
223            "--json": { "description": "Force JSON envelope output (auto-enabled when piped)", "type": "bool", "default": false },
224            "--quiet": { "description": "Suppress informational human output", "type": "bool", "default": false },
225        },
226        "exit_codes": {
227            "0": "Success",
228            "1": "Transient error (IO, render) — retry",
229            "2": "Config error — fix setup",
230            "3": "Bad input / not found / ambiguous — fix arguments",
231            "4": "Rate limited — wait and retry",
232        },
233        "envelope": {
234            "version": "1",
235            "success": "{ version, status, data }",
236            "error": "{ version, status, error: { code, message, suggestion } }",
237        },
238        "config": {
239            "path": config_path,
240            "env_prefix": "PAPERFOOT_",
241        },
242        "auto_json_when_piped": true,
243        "state_dir": state_dir,
244        "database": database,
245        "templates": templates,
246        "packs": packs.into_iter().map(|(k, p)| json!({"kind": k, "pack": p})).collect::<Vec<_>>(),
247        "kinds": kinds,
248        "fee_spec": "type:amount:currency — e.g. fixed:8400:SGD | hourly:200:SGD | daily:1500:SGD | retainer:5000:SGD",
249        "first_run": [
250            "contract doctor --json",
251            "contract issuer list --json     # CHECK FIRST — shared with invoice-cli",
252            "contract clients list --json    # CHECK FIRST — shared with invoice-cli",
253            "# Only if a needed issuer/client is missing, then:",
254            "contract issuer add <slug> --name <display-name> --jurisdiction sg|uk|us|eu --address \"line1\\nline2\"",
255            "contract clients add <slug> --name <client-name> --legal-name <legal> --address \"line1\\nline2\"",
256            "contract new --kind nda --as <issuer> --client <client> --purpose 'description'",
257            "contract render <number> --open",
258        ],
259        "shared_state": {
260            "database": database,
261            "shared_with": ["invoice-cli (binary: invoice)"],
262            "shared_tables": ["issuers", "clients", "number_series"],
263            "discovery_workflow": "Before creating a new issuer or client, ALWAYS run `contract issuer list --json` and `contract clients list --json` (or the invoice-cli equivalents). The accounting suite's whole point is one source of truth — duplicating entities here pollutes invoicing too. If the entity exists under a different slug, prefer using the existing slug over creating a new one.",
264            "legal_fields_on_clients": "Three columns added in V7 are contract-specific: `legal_name`, `company_no`, `legal_jurisdiction`. If an existing client (from invoice-cli) is missing them, fill via: contract clients edit <slug> --legal-name X --company-no Y --jurisdiction Z."
265        },
266        "examples": [
267            { "goal": "Quick mutual NDA",
268              "command": "contract new --kind nda --as acme --client meridian --purpose 'evaluation of a joint product' --term-years 3" },
269            { "goal": "Non-circumvention agreement protecting an introduction",
270              "command": "contract new --kind ncnda --as boris --client partner --purpose 'introduction to prospective lenders for the transaction' --term-years 2" },
271            { "goal": "Consulting agreement with fixed fee",
272              "command": "contract new --kind consulting --as acme --client meridian --purpose 'design a dashboard' --fee fixed:8400:SGD --term-months 3 --deliverable 'Design' --deliverable 'Build'" },
273            { "goal": "Interest-free loan with fixed repayment date",
274              "command": "contract new --kind loan --as boris --client friend --term principal_text='£10,000 (ten thousand pounds sterling)' --term repayment_date=2026-12-01 --term interest_text='interest-free'" },
275            { "goal": "Pick a template by describing the look",
276              "command": "contract template find \"magazine masthead serif\"" },
277            { "goal": "Render with no watermark",
278              "command": "contract render NDA-acme-2026-0001 --final --open" },
279        ],
280        "guardrails": [
281            "BEFORE creating any issuer or client, run `contract issuer list` and `contract clients list` — the DB is shared with invoice-cli; duplicates pollute both tools.",
282            "Run doctor before first use.",
283            "Use --json for agents; stdout is data, stderr is diagnostics.",
284            "Drafts render with a DRAFT watermark by default. Use --final to suppress before sending out for signature.",
285            "Once a contract is sent/signed, only the status / signature columns can change; executed contracts only move forward.",
286            "These clauses are practical plain-English starting points, not legal advice. Have a lawyer review for material engagements.",
287        ],
288    });
289    print_raw(&manifest);
290    Ok(())
291}