hedge-broker 0.3.1 → 0.4.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/CHANGELOG.md +47 -0
- package/README.md +83 -55
- package/dist/index.js +642 -34
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to `hedge-broker` are documented here. This project follows
|
|
|
4
4
|
[Semantic Versioning](https://semver.org) and
|
|
5
5
|
[Keep a Changelog](https://keepachangelog.com).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-09-19
|
|
8
|
+
|
|
9
|
+
The Hedge broker API rework: creating a submission now starts the run, markets
|
|
10
|
+
come back in three categories, the conversation with Hedge is readable and
|
|
11
|
+
answerable over the API, and binds happen over the API with explicit
|
|
12
|
+
attestation of the assumptions.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `intake [files...] --text|--text-file [--insured] [--lob] [--state] [--effective] [--producer-email] [--hold]`: send Hedge a risk as free text and/or PDFs (multipart `POST /broker/intake`). Sends an `Idempotency-Key` per run (`--idempotency-key` to supply your own).
|
|
17
|
+
- `markets <submissionId> [--wait]`: `GET /broker/submissions/{id}/markets` rendered in the three categories (Hedge Instant Quote, Hedge Binding, Hedge Specialty) with each lane's status, `needs_from_you`, assumptions (`[ ]` standing / `[x]` confirmed) and released quotes.
|
|
18
|
+
- `thread <submissionId> [--since] [--limit] [--one-page]`: the conversation with Hedge, oldest first, following `next_cursor` to the tail; `--json` returns `{messages, next_cursor, last_cursor}`.
|
|
19
|
+
- `reply <submissionId> <text|-> [--attach ids] [--producer-email]`: reply to Hedge on the thread with an `Idempotency-Key`.
|
|
20
|
+
- `answer-asks <submissionId> [--set k=v ...]`: batch answers to `outstanding-requirements`; without `--set` lists the items and keys. Values are typed from the item's `input` (number, bool) before sending.
|
|
21
|
+
- `withdraw <submissionId>`.
|
|
22
|
+
- `bind <submissionId> --quote <quoteId> [--payment in_full|monthly] [--attest | --attest-keys k1,k2]`: create a bind request. A 409 `assumptions_unconfirmed` prints every assumption and the exact re-run to attest (exit 1; `--json` prints the 409 detail).
|
|
23
|
+
- `bind-status <submissionId> [bindRequestId]`, `bind-upload <submissionId> <bindRequestId> <contingencyId> <file.pdf> [--notes]`, `bind-submit <submissionId> <bindRequestId> [--attest | --attest-keys]`.
|
|
24
|
+
- `programs [--category] [--lob] [--state]` and `program-schema <programId> [--lob] [--state]`: the program catalog with categories and the application question schema for instant-quote programs.
|
|
25
|
+
- `login --client-id <id> --client-secret <secret>` (or `HEDGE_CLIENT_SECRET`): machine sign-in with the `client_credentials` grant (scope `broker_mcp broker_submit`). The token is cached with its expiry and renewed by re-exchange; the secret is never printed.
|
|
26
|
+
- `submit --hold`: keep the draft instead of starting the run.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `submit` now starts the run on create (Hedge matches appetite, emails the producer the clearance, opens the lanes and quotes the instant-quote markets), matching the API. Its output shows `marketing_status` and `next_step` and points at `hedge markets`.
|
|
31
|
+
- `finalize` is now "release a held submission": it starts the run for a submission created with `--hold` and is idempotent on one already running.
|
|
32
|
+
- `requirements`, `status` and `finalize --wait` hints point at `hedge markets` and describe held drafts instead of "not yet finalized".
|
|
33
|
+
- `ApiError` carries the parsed response body so commands can act on structured errors.
|
|
34
|
+
|
|
35
|
+
## [0.3.2] - 2026-07-23
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- `finalize --wait` polls the requirements view (whose `marketing_status` flips the moment lanes attach) instead of the submission detail, which only lists markets Hedge has already contacted.
|
|
40
|
+
|
|
41
|
+
## [0.3.1] - 2026-07-23
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- `forms [search]` and `form <formKey> [-o file]`: search the blank application-form catalog and download a blank by its key (the keys `hedge requirements` lists).
|
|
46
|
+
|
|
47
|
+
## [0.3.0] - 2026-07-23
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `finalize --wait [--timeout minutes]`: poll until markets attach, then print them.
|
|
52
|
+
- `requirements` and `status` explain an empty markets list using the server's `marketing_status` (matching vs. not started vs. no appetite) instead of leaving it ambiguous.
|
|
53
|
+
|
|
7
54
|
## [0.2.0] - 2026-07-06
|
|
8
55
|
|
|
9
56
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# hedge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Send Hedge a risk from your terminal, the way you would email it, and track it to bind. `hedge intake` takes free text and PDFs; Hedge extracts the risk, matches appetite, emails the producer the clearance and opens the market lanes. `hedge markets` shows the three market categories with what each lane still needs from you, the assumptions Hedge made and the released quotes. `hedge thread` is the conversation with Hedge; `hedge reply` answers it. `hedge bind` requests a bind with explicit attestation of the assumptions.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -35,42 +35,66 @@ The installer downloads the binary for your OS and CPU into `$HOME/.local/bin`.
|
|
|
35
35
|
## Quickstart
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
hedge login
|
|
39
|
-
hedge
|
|
40
|
-
|
|
41
|
-
hedge
|
|
42
|
-
hedge
|
|
43
|
-
hedge
|
|
38
|
+
hedge login # device-code sign in (prints a code + URL)
|
|
39
|
+
hedge intake ./acord-125.pdf ./loss-runs.pdf \
|
|
40
|
+
--text "Peak Roofing LLC, residential re-roofing in CA, 12 employees, GL and property, eff 10/1"
|
|
41
|
+
hedge markets <submission-id> --wait # the three categories, asks, assumptions, quotes
|
|
42
|
+
hedge thread <submission-id> # the clearance email, questions, quote deliveries
|
|
43
|
+
hedge reply <submission-id> "Payroll is 900k, no work over 3 stories"
|
|
44
|
+
hedge bind <submission-id> --quote <quote-id> # 409 lists the assumptions to attest
|
|
45
|
+
hedge bind <submission-id> --quote <quote-id> --attest # after the agent reviewed each one
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
Creating a submission starts the run. There is no separate "go shop it" step: `hedge intake` and `hedge submit` both start marketing as soon as the create commits, exactly like emailing Hedge the risk. Pass `--hold` to keep the draft (attach documents, answer requirements), then `hedge finalize <id>` releases it.
|
|
49
|
+
|
|
50
|
+
## The three market categories
|
|
51
|
+
|
|
52
|
+
`hedge markets` always returns all three, in this order:
|
|
53
|
+
|
|
54
|
+
| Category | Label | What it is |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `instant_quote` | Hedge Instant Quote | Connected carrier APIs, quoted on recorded favorable assumptions. Each assumption is a pre-bind attestation the producer must confirm. |
|
|
57
|
+
| `binding` | Hedge Binding | Markets Hedge quotes for you. |
|
|
58
|
+
| `specialty` | Hedge Specialty | Email markets that need the completed ACORDs, program requirements and loss runs. |
|
|
59
|
+
|
|
60
|
+
Each lane carries its status, `submit_ready`, `needs_from_you` (only while the lane is gapped), `assumptions` (`[ ]` still standing, `[x]` confirmed) and released firm `quotes` with their `quote_id`.
|
|
61
|
+
|
|
46
62
|
## Commands
|
|
47
63
|
|
|
48
64
|
| Command | What it does |
|
|
49
65
|
| --- | --- |
|
|
50
|
-
| `hedge
|
|
51
|
-
| `hedge
|
|
52
|
-
| `hedge
|
|
53
|
-
| `hedge
|
|
54
|
-
| `hedge
|
|
55
|
-
| `hedge
|
|
66
|
+
| `hedge intake [files...] [--text <text>\|--text-file <path\|->] [--insured <name>] [--lob <slugs>] [--state <ST>] [--effective <date>] [--producer-email <email>] [--hold]` | Send Hedge a risk as text and/or PDFs (up to 20, 15 MB each). Creates the submission and starts the run. Requires `--text`/`--text-file` or `--insured`. |
|
|
67
|
+
| `hedge submit --insured <name> --narrative <text> [flags] [--hold]` | Create a submission from structured fields and start the run. `--body <file\|->` sends a full JSON body, with explicit flags taking precedence; see `hedge submit --help`. |
|
|
68
|
+
| `hedge markets <submissionId> [--wait] [--timeout min]` | The markets being tried in the three categories, with status, `needs_from_you`, assumptions and quotes. `--wait` polls until a lane attaches (usually ~5 minutes after create). |
|
|
69
|
+
| `hedge thread <submissionId> [--since cursor] [--limit n] [--one-page]` | The conversation with Hedge, oldest first. Follows `next_cursor` to the tail; `--json` returns `{messages, next_cursor, last_cursor}`. |
|
|
70
|
+
| `hedge reply <submissionId> <text\|-> [--attach ids] [--producer-email]` | Reply to Hedge exactly as you would reply to its email: answer a question, send a revision, authorize a bind. Hedge answers on the thread within a couple of minutes. |
|
|
71
|
+
| `hedge answer-asks <submissionId> [--set k=v ...] [--producer-email]` | Answer the outstanding items the matched markets still need, in one batch. Without `--set` it lists the items and their keys; values are typed per the item's input. |
|
|
72
|
+
| `hedge withdraw <submissionId> [--producer-email]` | Withdraw: a held draft is deleted, a live placement is pulled from every engaged market. |
|
|
73
|
+
| `hedge bind <submissionId> --quote <quoteId> [--payment in_full\|monthly] [--attest \| --attest-keys k1,k2] [--producer-email]` | Request to bind a released quote. While assumptions stand unconfirmed the API answers 409 `assumptions_unconfirmed`; the CLI prints them and the exact re-run with `--attest`. |
|
|
74
|
+
| `hedge bind-status <submissionId> [bindRequestId]` | Bind requests on a submission, or one in full: status, unconfirmed assumptions, contingencies with upload paths, next steps. |
|
|
75
|
+
| `hedge bind-upload <submissionId> <bindRequestId> <contingencyId> <file.pdf> [--notes]` | Attach a PDF to a pre-bind contingency while the request is drafted. |
|
|
76
|
+
| `hedge bind-submit <submissionId> <bindRequestId> [--attest \| --attest-keys]` | Submit the drafted bind request to Hedge for placement. Same 409 handling as `bind`. |
|
|
77
|
+
| `hedge programs [--category c] [--lob slug] [--state ST]` | The carrier programs your brokerage can quote, one row per program with its category. |
|
|
78
|
+
| `hedge program-schema <programId> [--lob slug] [--state ST]` | The application question schema for an instant-quote program, for your own application builder. |
|
|
79
|
+
| `hedge upload <submissionId> <file.pdf> [--name label]` | Attach an ACORD, loss runs, or supplement. Documents uploaded after create flow into the open lanes. |
|
|
80
|
+
| `hedge requirements <submissionId>` | The gaps view: what the submission still needs (per market, forms, carrier questions). |
|
|
81
|
+
| `hedge finalize <submissionId> [--wait]` | Release a held submission (one created with `--hold`). Idempotent on a submission whose run already started. |
|
|
56
82
|
| `hedge status <submissionId>` | Submission detail plus the live per-carrier marketing table, status roll-up, and documents on file. |
|
|
57
83
|
| `hedge submissions [--status state] [--search q] [--limit n] [--offset n] [--updated-since iso]` | List your brokerage's submissions, with filters and paging. |
|
|
58
|
-
| `hedge
|
|
59
|
-
| `hedge
|
|
60
|
-
| `hedge
|
|
61
|
-
| `hedge
|
|
62
|
-
| `hedge request-quote <submissionId> <sessionId>` |
|
|
63
|
-
| `hedge policies` |
|
|
64
|
-
| `hedge
|
|
65
|
-
| `hedge policy-doc <policyId> <kind> [-o file]` | Download a policy PDF; kind is `binder`, `policy`, or `declarations`. |
|
|
66
|
-
| `hedge payments` | List payment and invoice status, with invoice links. |
|
|
84
|
+
| `hedge appetite <class> [--state ST] [--lob slug]` | Which markets have appetite for a class of business. |
|
|
85
|
+
| `hedge market-requirements <marketId> --lob <slug> [--state ST] [--programs keys]` | What a market needs to quote a line. Market ids come from `hedge appetite --json`. |
|
|
86
|
+
| `hedge forms [search]` / `hedge form <formKey> [-o file]` | Search the blank application-form catalog and download a blank. |
|
|
87
|
+
| `hedge documents <submissionId>` / `hedge download <documentId> [-o file]` | List a submission's finalized documents and download one. |
|
|
88
|
+
| `hedge quotes <submissionId>` / `hedge answer <submissionId> <sessionId> --set k=v` / `hedge request-quote <submissionId> <sessionId>` | Work an instant-quote carrier session directly. |
|
|
89
|
+
| `hedge policies` / `hedge policy <policyId>` / `hedge policy-doc <policyId> <kind> [-o file]` | Bound policies, one policy's detail, and its `binder`, `policy` or `declarations` PDF. |
|
|
90
|
+
| `hedge payments` | Payment and invoice status, with invoice links. |
|
|
67
91
|
| `hedge whoami` | Show the signed-in broker and brokerage. |
|
|
68
|
-
| `hedge login` / `hedge logout` | Sign in and out. |
|
|
92
|
+
| `hedge login [--browser] [--client-id <id> --client-secret <secret>]` / `hedge logout` | Sign in (device code, browser, or a machine credential) and out. |
|
|
69
93
|
|
|
70
94
|
Add `--json` to any command for the raw API response, so the CLI composes in scripts:
|
|
71
95
|
|
|
72
96
|
```bash
|
|
73
|
-
hedge
|
|
97
|
+
hedge markets <submission-id> --json | jq '.categories[] | select(.category=="instant_quote") | .lanes[] | {market_name, status, quotes}'
|
|
74
98
|
```
|
|
75
99
|
|
|
76
100
|
Add `--staging` to any command (or set `HEDGE_ENV=staging`) to target the staging environment.
|
|
@@ -79,55 +103,59 @@ Add `--staging` to any command (or set `HEDGE_ENV=staging`) to target the stagin
|
|
|
79
103
|
|
|
80
104
|
```bash
|
|
81
105
|
hedge login
|
|
82
|
-
hedge
|
|
83
|
-
hedge
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
--tiv 250000 --payroll 900000 --vehicles 3
|
|
100
|
-
hedge upload <submission-id> ./acord-125.pdf --name "ACORD 125"
|
|
101
|
-
hedge requirements <submission-id>
|
|
102
|
-
hedge finalize <submission-id>
|
|
103
|
-
hedge status <submission-id>
|
|
104
|
-
hedge documents <submission-id>
|
|
105
|
-
hedge download <document-id> -o quote.pdf
|
|
106
|
+
hedge intake ./acord-125.pdf --text "Peak Roofing LLC, residential re-roofing, no hot tar, no work over 3 stories. 12 employees, payroll 900k, 3 vehicles. GL + property, effective 2026-10-01, Oakland CA." --producer-email dana@youragency.com
|
|
107
|
+
hedge markets <submission-id> --wait
|
|
108
|
+
# Hedge Instant Quote: lanes quoted on assumptions ([ ] = still to confirm)
|
|
109
|
+
# Hedge Binding: markets Hedge quotes for you, with needs_from_you
|
|
110
|
+
# Hedge Specialty: email markets waiting on completed ACORDs / loss runs
|
|
111
|
+
hedge answer-asks <submission-id> # list the outstanding asks and their keys
|
|
112
|
+
hedge answer-asks <submission-id> --set years_in_business=8 --set prior_losses=false
|
|
113
|
+
hedge upload <submission-id> ./supplemental.pdf --name "Roofing supplement"
|
|
114
|
+
hedge thread <submission-id> # the clearance email and Hedge's questions
|
|
115
|
+
hedge reply <submission-id> "No hot-tar work; the 3-story limit is firm."
|
|
116
|
+
hedge bind <submission-id> --quote <quote-id> --payment in_full
|
|
117
|
+
# 409: assumptions unconfirmed -> lists each one and the re-run
|
|
118
|
+
hedge bind <submission-id> --quote <quote-id> --payment in_full --attest
|
|
119
|
+
hedge bind-upload <submission-id> <bind-request-id> <contingency-id> ./signed-app.pdf
|
|
120
|
+
hedge bind-submit <submission-id> <bind-request-id>
|
|
121
|
+
hedge bind-status <submission-id> <bind-request-id>
|
|
122
|
+
hedge policies
|
|
106
123
|
```
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Prefer building the request in a file (or another program)? Send a full JSON body. Explicit flags still win: top-level flag values replace matching body keys, applicant flags merge over the body's applicant, and address flags merge into its mailing_address:
|
|
125
|
+
Structured creates still work. `hedge submit` takes the full applicant, contact, address and exposure flags (see `hedge submit --help`), or a JSON body:
|
|
111
126
|
|
|
112
127
|
```bash
|
|
113
128
|
hedge submit --body ./submission.json
|
|
114
|
-
cat submission.json | hedge submit --body - --effective 2026-09-01
|
|
129
|
+
cat submission.json | hedge submit --body - --effective 2026-09-01 --hold
|
|
130
|
+
hedge finalize <submission-id> --wait # release the held draft
|
|
115
131
|
```
|
|
116
132
|
|
|
117
|
-
Every `submit` sends an `Idempotency-Key` header (a random UUID each run).
|
|
133
|
+
Every `intake`, `submit` and `reply` sends an `Idempotency-Key` header (a random UUID each run). A scripted retry that must not double-create should pass its own key with `--idempotency-key <key>`.
|
|
134
|
+
|
|
135
|
+
## Binding and attestation
|
|
136
|
+
|
|
137
|
+
Instant quotes are obtained on recorded assumptions: values Hedge defaulted on the carrier application because the file did not state them. They print on the signable application, so the retail agent must confirm them before the order reaches the carrier. `hedge markets` lists them per lane; `hedge bind` (and `hedge bind-submit`) answer 409 `assumptions_unconfirmed` while any stand, keep the draft, and the CLI prints every statement plus the exact command to confirm:
|
|
138
|
+
|
|
139
|
+
- `--attest` confirms every standing assumption on the quote's carrier application.
|
|
140
|
+
- `--attest-keys k1,k2` confirms only those keys.
|
|
141
|
+
|
|
142
|
+
Send either only after the agent has reviewed each statement; the confirmation is recorded on the bind request under the producing broker. Nothing is sent to a carrier until `hedge bind-submit`.
|
|
118
143
|
|
|
119
144
|
## Signing in
|
|
120
145
|
|
|
121
|
-
The CLI signs in with OAuth 2.1
|
|
146
|
+
The CLI signs in with OAuth 2.1 and works over SSH. Three modes:
|
|
122
147
|
|
|
123
148
|
| Mode | How | When to use |
|
|
124
149
|
| --- | --- | --- |
|
|
125
150
|
| Device code (default) | `hedge login` | Anywhere, including headless servers and SSH. Prints a short code and a URL to approve in any browser. |
|
|
126
151
|
| Browser (loopback) | `hedge login --browser` | A local machine with a browser. Opens it and captures the redirect on `127.0.0.1`. |
|
|
152
|
+
| Machine credential | `hedge login --client-id bac_... --client-secret bas_...` | Scripts and automation. Uses the `client_credentials` grant with an API key created under **Settings → API keys** in the broker portal. Prefer `HEDGE_CLIENT_SECRET` in the environment over the flag. The token is cached with its expiry and renewed by re-exchange; the secret is never printed. |
|
|
153
|
+
|
|
154
|
+
Machine credentials carry no broker identity, so writes made with one must name the producing broker: pass `--producer-email` on `intake`, `submit`, `reply`, `answer-asks`, `withdraw`, `bind`, `bind-upload` and `bind-submit`.
|
|
127
155
|
|
|
128
156
|
Credentials are stored per profile at `~/.config/taven-cli/hedge.<profile>.json` with `0600` permissions. Access tokens are refreshed automatically. Sign out with `hedge logout`.
|
|
129
157
|
|
|
130
|
-
The CLI requests the `broker_mcp` and `broker_submit` scopes.
|
|
158
|
+
The CLI requests the `broker_mcp` and `broker_submit` scopes. Read commands work with either scope; `intake`, `submit`, `upload`, `finalize`, `reply`, `answer-asks`, `withdraw`, `bind*`, `answer` and `request-quote` require `broker_submit`.
|
|
131
159
|
|
|
132
160
|
## License
|
|
133
161
|
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,21 @@ async function loopbackLogin(opts) {
|
|
|
163
163
|
let port = 0;
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
|
+
async function clientCredentialsLogin(tokenEndpoint, clientId, clientSecret, scope) {
|
|
167
|
+
const body = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId, client_secret: clientSecret });
|
|
168
|
+
if (scope) body.set("scope", scope);
|
|
169
|
+
const res = await fetch(tokenEndpoint, { method: "POST", headers: FORM, body });
|
|
170
|
+
let tb = { access_token: "" };
|
|
171
|
+
try {
|
|
172
|
+
tb = await res.json();
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
if (!res.ok || !tb.access_token) {
|
|
176
|
+
const why = tb.error_description || tb.error || `Token request failed (${res.status})`;
|
|
177
|
+
throw new Error(`Machine credential sign-in failed: ${why}`);
|
|
178
|
+
}
|
|
179
|
+
return tb;
|
|
180
|
+
}
|
|
166
181
|
async function refresh(tokenEndpoint, clientId, refreshToken) {
|
|
167
182
|
const res = await fetch(tokenEndpoint, {
|
|
168
183
|
method: "POST",
|
|
@@ -180,11 +195,13 @@ function sleep(ms) {
|
|
|
180
195
|
// src/core/http.ts
|
|
181
196
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
182
197
|
var ApiError = class extends Error {
|
|
183
|
-
constructor(status, message) {
|
|
198
|
+
constructor(status, message, body) {
|
|
184
199
|
super(message);
|
|
185
200
|
this.status = status;
|
|
201
|
+
this.body = body;
|
|
186
202
|
}
|
|
187
203
|
status;
|
|
204
|
+
body;
|
|
188
205
|
};
|
|
189
206
|
function renderErrorDetail(detail, fallback) {
|
|
190
207
|
if (detail == null) return fallback;
|
|
@@ -210,12 +227,27 @@ function apiErrorFrom(status, parsed, fallback) {
|
|
|
210
227
|
} else if (typeof parsed === "string") {
|
|
211
228
|
detail = parsed;
|
|
212
229
|
}
|
|
213
|
-
return new ApiError(status, renderErrorDetail(detail, fallback));
|
|
230
|
+
return new ApiError(status, renderErrorDetail(detail, fallback), parsed);
|
|
214
231
|
}
|
|
215
232
|
async function bearer(cfg) {
|
|
216
233
|
const tok = loadToken(cfg);
|
|
217
234
|
if (!tok) throw new ApiError(401, "Not signed in. Run `login` first");
|
|
218
235
|
if (tok.expires_at - 60 > Math.floor(Date.now() / 1e3)) return tok.access_token;
|
|
236
|
+
if (tok.grant === "client_credentials" && tok.client_secret) {
|
|
237
|
+
try {
|
|
238
|
+
const r = await clientCredentialsLogin(tok.token_endpoint, tok.client_id, tok.client_secret, tok.scope);
|
|
239
|
+
const updated = {
|
|
240
|
+
...tok,
|
|
241
|
+
access_token: r.access_token,
|
|
242
|
+
expires_at: Math.floor(Date.now() / 1e3) + (r.expires_in ?? 3600),
|
|
243
|
+
scope: r.scope ?? tok.scope
|
|
244
|
+
};
|
|
245
|
+
saveToken(cfg, updated);
|
|
246
|
+
return updated.access_token;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
throw new ApiError(401, `${e instanceof Error ? e.message : String(e)}. Check the key under Settings \u2192 API keys or run \`login --client-id ... --client-secret ...\` again`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
219
251
|
if (!tok.refresh_token) throw new ApiError(401, "Session expired. Run `login` again");
|
|
220
252
|
try {
|
|
221
253
|
const r = await refresh(tok.token_endpoint, tok.client_id, tok.refresh_token);
|
|
@@ -260,9 +292,9 @@ async function apiRequest(opts, method, path, init) {
|
|
|
260
292
|
if (!res.ok) throw apiErrorFrom(res.status, parsed, `Request failed (${res.status})`);
|
|
261
293
|
return parsed;
|
|
262
294
|
}
|
|
263
|
-
async function multipartRequest(opts, method, path, form) {
|
|
295
|
+
async function multipartRequest(opts, method, path, form, extraHeaders) {
|
|
264
296
|
const url = opts.apiBase.replace(/\/$/, "") + path;
|
|
265
|
-
const headers = { Accept: "application/json" };
|
|
297
|
+
const headers = { Accept: "application/json", ...extraHeaders ?? {} };
|
|
266
298
|
if (opts.apiKey) headers[opts.apiKeyHeader ?? "X-Api-Key"] = opts.apiKey;
|
|
267
299
|
else headers.Authorization = `Bearer ${await bearer(opts.cfg)}`;
|
|
268
300
|
const res = await fetch(url, { method, headers, body: form });
|
|
@@ -388,6 +420,20 @@ async function loginInteractive(opts) {
|
|
|
388
420
|
client_id: clientId
|
|
389
421
|
});
|
|
390
422
|
}
|
|
423
|
+
async function loginClientCredentials(opts) {
|
|
424
|
+
const meta = await discover(opts.metadataUrl);
|
|
425
|
+
const tr = await clientCredentialsLogin(meta.token_endpoint, opts.clientId, opts.clientSecret, opts.scope);
|
|
426
|
+
saveToken(opts.cfg, {
|
|
427
|
+
access_token: tr.access_token,
|
|
428
|
+
expires_at: Math.floor(Date.now() / 1e3) + (tr.expires_in ?? 3600),
|
|
429
|
+
scope: tr.scope,
|
|
430
|
+
token_endpoint: meta.token_endpoint,
|
|
431
|
+
client_id: opts.clientId,
|
|
432
|
+
grant: "client_credentials",
|
|
433
|
+
client_secret: opts.clientSecret
|
|
434
|
+
});
|
|
435
|
+
return { scope: tr.scope, expiresIn: tr.expires_in };
|
|
436
|
+
}
|
|
391
437
|
|
|
392
438
|
// src/context.ts
|
|
393
439
|
function resolveEnv(staging) {
|
|
@@ -413,9 +459,33 @@ function makeCtx(opts) {
|
|
|
413
459
|
// src/commands/auth.ts
|
|
414
460
|
var SCOPE = "broker_mcp broker_submit";
|
|
415
461
|
function registerAuth(program2) {
|
|
416
|
-
program2.command("login").description("Sign in to your Hedge broker account").option("--browser", "use the browser (loopback) flow instead of a device code").
|
|
462
|
+
program2.command("login").description("Sign in to your Hedge broker account (device code by default; --client-id for a machine credential)").option("--browser", "use the browser (loopback) flow instead of a device code").option(
|
|
463
|
+
"--client-id <id>",
|
|
464
|
+
"sign in with a brokerage machine credential (client_credentials grant) for scripts and automation; the client id (bac_...) from Settings \u2192 API keys. Pair with --client-secret"
|
|
465
|
+
).option(
|
|
466
|
+
"--client-secret <secret>",
|
|
467
|
+
"the machine credential's secret (bas_...). Prefer HEDGE_CLIENT_SECRET in the environment so the secret stays out of shell history"
|
|
468
|
+
).action(async (opts) => {
|
|
417
469
|
const g = program2.opts();
|
|
418
470
|
const ctx = makeCtx(g);
|
|
471
|
+
if (opts.clientId || opts.clientSecret) {
|
|
472
|
+
if (opts.browser) throw new Error("--browser and --client-id are different sign-in modes; pass one");
|
|
473
|
+
const clientId = String(opts.clientId ?? "").trim();
|
|
474
|
+
const clientSecret = String(opts.clientSecret ?? process.env.HEDGE_CLIENT_SECRET ?? "").trim();
|
|
475
|
+
if (!clientId) throw new Error("--client-id is required with --client-secret");
|
|
476
|
+
if (!clientSecret) throw new Error("--client-secret (or HEDGE_CLIENT_SECRET) is required with --client-id");
|
|
477
|
+
const r = await loginClientCredentials({
|
|
478
|
+
cfg: ctx.cfg,
|
|
479
|
+
metadataUrl: ctx.env.metadataUrl,
|
|
480
|
+
clientId,
|
|
481
|
+
clientSecret,
|
|
482
|
+
scope: SCOPE
|
|
483
|
+
});
|
|
484
|
+
process.stdout.write(
|
|
485
|
+
`Signed in with machine credential ${clientId}` + (r.scope ? ` (scope: ${r.scope})` : "") + ".\nWrites made with this credential must name the producing broker: pass --producer-email on submit, intake, reply, answer-asks, withdraw and bind.\n"
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
419
489
|
await loginInteractive({
|
|
420
490
|
cfg: ctx.cfg,
|
|
421
491
|
metadataUrl: ctx.env.metadataUrl,
|
|
@@ -448,16 +518,227 @@ function registerAuth(program2) {
|
|
|
448
518
|
}
|
|
449
519
|
|
|
450
520
|
// src/commands/submissions.ts
|
|
521
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
522
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
523
|
+
import { basename as basename2 } from "path";
|
|
524
|
+
|
|
525
|
+
// src/commands/intake.ts
|
|
451
526
|
import { randomUUID } from "crypto";
|
|
452
527
|
import { readFileSync as readFileSync2 } from "fs";
|
|
453
528
|
import { basename } from "path";
|
|
529
|
+
var CATEGORY_LABELS = {
|
|
530
|
+
instant_quote: "Hedge Instant Quote",
|
|
531
|
+
binding: "Hedge Binding",
|
|
532
|
+
specialty: "Hedge Specialty"
|
|
533
|
+
};
|
|
534
|
+
function nextAfterCreate(submissionId, marketingStatus) {
|
|
535
|
+
if (marketingStatus === "awaiting_finalization") {
|
|
536
|
+
return `Held. Attach documents (hedge upload ${submissionId} <file.pdf>), check hedge requirements ${submissionId}, then release it with hedge finalize ${submissionId}`;
|
|
537
|
+
}
|
|
538
|
+
return `The run started: Hedge is matching appetite and will email the producer the clearance.
|
|
539
|
+
Next: hedge markets ${submissionId} (matching usually lands within ~5 minutes; add --wait to poll)
|
|
540
|
+
hedge thread ${submissionId} (the conversation with Hedge)`;
|
|
541
|
+
}
|
|
542
|
+
function printWarnings(warnings) {
|
|
543
|
+
if (Array.isArray(warnings) && warnings.length) {
|
|
544
|
+
process.stdout.write("\nWarnings:\n" + warnings.map((w) => " - " + String(w)).join("\n") + "\n");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function readTextSource(source) {
|
|
548
|
+
return source === "-" ? readFileSync2(0, "utf8") : readFileSync2(source, "utf8");
|
|
549
|
+
}
|
|
550
|
+
function fmtAssumption(a) {
|
|
551
|
+
const mark = a.confirmed ? "[x]" : "[ ]";
|
|
552
|
+
const value = a.value === void 0 || a.value === null ? "" : ` = ${typeof a.value === "string" ? a.value : JSON.stringify(a.value)}`;
|
|
553
|
+
return `${mark} ${a.key}: ${a.label ?? ""}${value}`;
|
|
554
|
+
}
|
|
555
|
+
function renderMarkets(view) {
|
|
556
|
+
const out = [];
|
|
557
|
+
for (const group of view.categories ?? []) {
|
|
558
|
+
const label = group.label ?? CATEGORY_LABELS[group.category] ?? group.category;
|
|
559
|
+
out.push(`${label} (${group.category})${group.description ? " - " + group.description : ""}`);
|
|
560
|
+
const lanes = group.lanes ?? [];
|
|
561
|
+
if (!lanes.length) {
|
|
562
|
+
out.push(" (no lanes)");
|
|
563
|
+
out.push("");
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const rows = lanes.map((l) => ({
|
|
567
|
+
lane: l.lane_id,
|
|
568
|
+
market: l.market_name ?? "",
|
|
569
|
+
program: l.program ?? "",
|
|
570
|
+
lines: (l.lines || []).join(","),
|
|
571
|
+
status: l.status_label ?? l.status,
|
|
572
|
+
ready: l.submit_ready ? "yes" : "no",
|
|
573
|
+
quote: (l.quotes || []).map((q) => q.premium).join(" / "),
|
|
574
|
+
bound: l.bound ? "yes" : ""
|
|
575
|
+
}));
|
|
576
|
+
out.push(table(rows, ["lane", "market", "program", "lines", "status", "ready", "quote", "bound"]).split("\n").map((x) => " " + x).join("\n"));
|
|
577
|
+
for (const l of lanes) {
|
|
578
|
+
const detail = [];
|
|
579
|
+
if (l.needs_from_you?.length) detail.push(" needs from you: " + l.needs_from_you.join("; "));
|
|
580
|
+
if (l.assumptions?.length) {
|
|
581
|
+
detail.push(" assumptions (pre-bind attestations; [ ] = still to confirm):");
|
|
582
|
+
for (const a of l.assumptions) detail.push(" " + fmtAssumption(a));
|
|
583
|
+
}
|
|
584
|
+
if (l.quotes?.length) {
|
|
585
|
+
for (const q of l.quotes) {
|
|
586
|
+
detail.push(` quote ${q.quote_id}: ${q.premium}${q.lines?.length ? " (" + q.lines.join(", ") + ")" : ""}${q.is_revision ? " [revision]" : ""}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (detail.length) out.push(` ${l.market_name ?? l.lane_id}${l.program ? " / " + l.program : ""}:
|
|
590
|
+
` + detail.join("\n"));
|
|
591
|
+
}
|
|
592
|
+
out.push("");
|
|
593
|
+
}
|
|
594
|
+
return out.join("\n").trimEnd();
|
|
595
|
+
}
|
|
596
|
+
function laneCount(view) {
|
|
597
|
+
return (view?.categories ?? []).reduce((n, g) => n + (g.lanes?.length ?? 0), 0);
|
|
598
|
+
}
|
|
599
|
+
function registerIntake(program2) {
|
|
600
|
+
program2.command("intake [files...]").description(
|
|
601
|
+
"Send Hedge a risk the way you would email it: free text and/or PDFs (ACORDs, loss runs, supplements). Creates the submission and STARTS the run unless --hold. Hedge reads the insured out of the text when --insured is omitted"
|
|
602
|
+
).option("--text <text>", "the risk as you would describe it in an email (required unless --insured or --text-file)").option("--text-file <path|->", "read the text from a file, or - for stdin").option("--insured <name>", "insured business name (skips the extractor for the name)").option("--lob <slugs>", "comma-separated lines of business: canonical slugs or common aliases, e.g. gl,property").option("--state <ST>", "primary risk state, 2-letter").option("--effective <date>", "effective date, YYYY-MM-DD").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").option("--hold", "keep the draft; release it later with hedge finalize <id>").option(
|
|
603
|
+
"--idempotency-key <key>",
|
|
604
|
+
"Idempotency-Key header value (default: a random UUID per invocation). Re-send the same key to replay the saved submission instead of creating a duplicate"
|
|
605
|
+
).action(async (files, opts) => {
|
|
606
|
+
const ctx = makeCtx(program2.opts());
|
|
607
|
+
if (opts.text && opts.textFile) throw new Error("pass --text or --text-file, not both");
|
|
608
|
+
const text = opts.textFile ? readTextSource(opts.textFile) : opts.text;
|
|
609
|
+
if (!text?.trim() && !opts.insured) throw new Error("--text (or --text-file) or --insured is required");
|
|
610
|
+
if (files.length > 20) throw new Error(`up to 20 files per intake (got ${files.length})`);
|
|
611
|
+
const form = new FormData();
|
|
612
|
+
if (text?.trim()) form.append("text", text);
|
|
613
|
+
for (const f of files) {
|
|
614
|
+
const bytes = readFileSync2(f);
|
|
615
|
+
if (bytes.byteLength > 15 * 1024 * 1024) throw new Error(`${f} is over the 15 MB per-file limit`);
|
|
616
|
+
form.append("files", new Blob([bytes], { type: "application/pdf" }), basename(f));
|
|
617
|
+
}
|
|
618
|
+
if (opts.insured) form.append("insured_name", opts.insured);
|
|
619
|
+
if (opts.effective) form.append("effective_date", opts.effective);
|
|
620
|
+
if (opts.lob) form.append("lines_of_business", String(opts.lob).split(",").map((s) => s.trim()).filter(Boolean).join(","));
|
|
621
|
+
if (opts.state) form.append("primary_state", String(opts.state).trim().toUpperCase());
|
|
622
|
+
if (opts.producerEmail) form.append("producer_email", opts.producerEmail);
|
|
623
|
+
if (opts.hold) form.append("hold", "true");
|
|
624
|
+
const res = await multipartRequest(ctx.client, "POST", "/broker/intake", form, {
|
|
625
|
+
"Idempotency-Key": opts.idempotencyKey ?? randomUUID()
|
|
626
|
+
});
|
|
627
|
+
if (ctx.json) return printJson(res);
|
|
628
|
+
const applicant = res.applicant ?? {};
|
|
629
|
+
process.stdout.write(kv({
|
|
630
|
+
submission_id: res.submission_id,
|
|
631
|
+
insured: applicant.insured_name ?? "",
|
|
632
|
+
status: res.status_label,
|
|
633
|
+
marketing: res.marketing_status,
|
|
634
|
+
documents: (res.documents || []).map((d) => d.display_name).join(", "),
|
|
635
|
+
portal: res.portal_url
|
|
636
|
+
}) + "\n");
|
|
637
|
+
printWarnings(res.warnings);
|
|
638
|
+
process.stdout.write("\n" + nextAfterCreate(String(res.submission_id), res.marketing_status) + "\n");
|
|
639
|
+
});
|
|
640
|
+
program2.command("markets <submissionId>").description("The markets being tried, in the three Hedge categories (Instant Quote, Binding, Specialty): status, what each lane needs from you, the assumptions Hedge made, released quotes").option("--wait", "poll until at least one lane attaches (usually ~5 minutes after create)").option("--timeout <minutes>", "how long --wait polls before giving up", "8").action(async (submissionId, opts) => {
|
|
641
|
+
const ctx = makeCtx(program2.opts());
|
|
642
|
+
const path = `/broker/submissions/${submissionId}/markets`;
|
|
643
|
+
let view = await apiRequest(ctx.client, "GET", path);
|
|
644
|
+
if (opts.wait && laneCount(view) === 0) {
|
|
645
|
+
const timeoutMin = Number.parseInt(String(opts.timeout), 10);
|
|
646
|
+
if (!Number.isFinite(timeoutMin) || timeoutMin < 1) throw new Error("--timeout expects a whole number of minutes");
|
|
647
|
+
const deadline = Date.now() + timeoutMin * 6e4;
|
|
648
|
+
if (!ctx.json) process.stdout.write("Waiting for markets to match (usually ~5 minutes)");
|
|
649
|
+
while (Date.now() < deadline && laneCount(view) === 0) {
|
|
650
|
+
await new Promise((r) => setTimeout(r, 15e3));
|
|
651
|
+
if (!ctx.json) process.stdout.write(".");
|
|
652
|
+
view = await apiRequest(ctx.client, "GET", path);
|
|
653
|
+
}
|
|
654
|
+
if (!ctx.json) process.stdout.write("\n");
|
|
655
|
+
}
|
|
656
|
+
if (ctx.json) return printJson(view);
|
|
657
|
+
if (laneCount(view) === 0) {
|
|
658
|
+
process.stdout.write(
|
|
659
|
+
renderMarkets(view) + `
|
|
660
|
+
|
|
661
|
+
No lanes yet. Matching usually lands within ~5 minutes of create; an empty view during matching is not "no appetite". If the submission is held, release it with hedge finalize ${submissionId}.
|
|
662
|
+
`
|
|
663
|
+
);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
process.stdout.write(renderMarkets(view) + "\n");
|
|
667
|
+
process.stdout.write(`
|
|
668
|
+
Bind a released quote: hedge bind ${submissionId} --quote <quoteId> Reply to Hedge: hedge reply ${submissionId} "..."
|
|
669
|
+
`);
|
|
670
|
+
});
|
|
671
|
+
program2.command("answer-asks <submissionId>").description("Answer the outstanding items the matched markets still need, in one batch (keys from the list this prints when run without --set)").option("--set <kv...>", "key=value pairs; values are typed per the item's input (number, bool, select option)").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").action(async (submissionId, opts) => {
|
|
672
|
+
const ctx = makeCtx(program2.opts());
|
|
673
|
+
const view = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/outstanding-requirements`);
|
|
674
|
+
const items = view.items ?? [];
|
|
675
|
+
if (!opts.set?.length) {
|
|
676
|
+
if (ctx.json) return printJson(view);
|
|
677
|
+
if (!items.length) {
|
|
678
|
+
process.stdout.write("Nothing outstanding for the matched markets.\n");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
process.stdout.write(table(items.map((i) => ({
|
|
682
|
+
key: i.key,
|
|
683
|
+
label: i.label,
|
|
684
|
+
input: i.input ?? "",
|
|
685
|
+
options: (i.options || []).map((o) => o.value).join("|"),
|
|
686
|
+
markets: (i.markets || []).join("; ")
|
|
687
|
+
})), ["key", "label", "input", "options", "markets"]) + "\n");
|
|
688
|
+
process.stdout.write(`
|
|
689
|
+
Answer with: hedge answer-asks ${submissionId} --set key=value --set key2=value2
|
|
690
|
+
`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const inputs = new Map(items.map((i) => [String(i.key), String(i.input ?? "")]));
|
|
694
|
+
const answers = [];
|
|
695
|
+
for (const pair of opts.set) {
|
|
696
|
+
const i = String(pair).indexOf("=");
|
|
697
|
+
if (i <= 0) throw new Error(`--set expects key=value, got "${pair}"`);
|
|
698
|
+
const key = String(pair).slice(0, i);
|
|
699
|
+
const raw = String(pair).slice(i + 1);
|
|
700
|
+
const input = inputs.get(key) ?? "";
|
|
701
|
+
let value = raw;
|
|
702
|
+
if (/^(number|integer|currency|percent)$/i.test(input)) {
|
|
703
|
+
const n = Number(raw.replace(/[,$%\s]/g, ""));
|
|
704
|
+
if (!Number.isFinite(n)) throw new Error(`${key} expects a number, got "${raw}"`);
|
|
705
|
+
value = n;
|
|
706
|
+
} else if (/^(bool|boolean|checkbox|yes_no)$/i.test(input)) {
|
|
707
|
+
if (/^(true|yes|y|1)$/i.test(raw)) value = true;
|
|
708
|
+
else if (/^(false|no|n|0)$/i.test(raw)) value = false;
|
|
709
|
+
else throw new Error(`${key} expects yes/no, got "${raw}"`);
|
|
710
|
+
}
|
|
711
|
+
answers.push({ key, value });
|
|
712
|
+
}
|
|
713
|
+
const body = { answers };
|
|
714
|
+
if (opts.producerEmail) body.producer_email = opts.producerEmail;
|
|
715
|
+
const res = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/outstanding-requirements/answers`, { body });
|
|
716
|
+
if (ctx.json) return printJson(res);
|
|
717
|
+
process.stdout.write(`applied: ${(res.applied || []).join(", ") || "(none)"}
|
|
718
|
+
`);
|
|
719
|
+
for (const s of res.skipped || []) process.stdout.write(`skipped: ${s.key} - ${s.reason}
|
|
720
|
+
`);
|
|
721
|
+
if (res.routed) process.stdout.write(`routed to ${res.routed} carrier session(s)
|
|
722
|
+
`);
|
|
723
|
+
});
|
|
724
|
+
program2.command("withdraw <submissionId>").description("Withdraw a submission: a held draft is deleted; a live placement is pulled from every engaged market and closed as withdrawn").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").action(async (submissionId, opts) => {
|
|
725
|
+
const ctx = makeCtx(program2.opts());
|
|
726
|
+
const res = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/withdraw`, {
|
|
727
|
+
body: opts.producerEmail ? { producer_email: opts.producerEmail } : void 0
|
|
728
|
+
});
|
|
729
|
+
if (ctx.json) return printJson(res);
|
|
730
|
+
process.stdout.write(kv({ outcome: res.outcome, detail: res.detail }) + "\n");
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/commands/submissions.ts
|
|
454
735
|
var POLICY_DOC_KINDS = ["binder", "policy", "declarations"];
|
|
455
736
|
function intFlag(value, flag) {
|
|
456
737
|
if (!/^\d+$/.test(String(value).trim())) throw new Error(`${flag} expects a whole number, got "${value}"`);
|
|
457
738
|
return Number.parseInt(String(value).trim(), 10);
|
|
458
739
|
}
|
|
459
740
|
function readJsonBody(source) {
|
|
460
|
-
const raw = source === "-" ?
|
|
741
|
+
const raw = source === "-" ? readFileSync3(0, "utf8") : readFileSync3(source, "utf8");
|
|
461
742
|
let parsed;
|
|
462
743
|
try {
|
|
463
744
|
parsed = JSON.parse(raw);
|
|
@@ -475,7 +756,9 @@ function formatBytes(size) {
|
|
|
475
756
|
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
476
757
|
}
|
|
477
758
|
function registerSubmissions(program2) {
|
|
478
|
-
program2.command("submit").description(
|
|
759
|
+
program2.command("submit").description(
|
|
760
|
+
"Create a submission from structured fields and START the run: Hedge matches appetite, emails the producer the clearance, opens the lanes and quotes the instant-quote markets. --hold keeps the draft until `hedge finalize`. For free text and PDFs use `hedge intake`"
|
|
761
|
+
).option("--hold", "defer the run: keep the draft until `hedge finalize <id>` releases it (attach documents and answer requirements first)").option("--insured <name>", "insured business name (required unless --body supplies applicant.insured_name)").option("--narrative <text>", "operations description of the risk (required unless --body supplies narrative)").option("--lob <slugs>", "comma-separated lines of business, e.g. commercial_general_liability,workers_compensation").option("--effective <date>", "effective date, YYYY-MM-DD").option("--state <ST>", "primary state (2-letter); with any address flag it becomes the mailing address state instead").option("--website <url>", "insured website").option("--fein <id>", "FEIN (or SSN for a sole proprietor)").option("--entity-type <type>", "legal entity type, e.g. llc, corporation").option("--naics <code>", "NAICS code").option("--business-phone <phone>", "insured business phone").option("--business-email <email>", "insured business email").option("--contact-first <name>", "primary contact first name").option("--contact-last <name>", "primary contact last name").option("--contact-email <email>", "primary contact email").option("--contact-phone <phone>", "primary contact phone").option("--address <line1>", "mailing address line 1").option("--address2 <line2>", "mailing address line 2").option("--city <city>", "mailing address city").option("--zip <zip>", "mailing address ZIP code").option("--tiv <int>", "total insured property value in dollars (property_tiv_total)").option("--vehicles <int>", "number of vehicles (auto_vehicle_count)").option("--payroll <int>", "total annual payroll in dollars (wc_total_annual_payroll)").option("--insured-id <uuid>", "start from an existing insured in your book").option("--producer-email <email>", "producing broker to attribute (required for brokerage API-client credentials)").option(
|
|
479
762
|
"--body <file|->",
|
|
480
763
|
"full JSON request body from a file, or - for stdin. Precedence: flags win; a top-level flag value replaces the matching body key, applicant flags merge over the body's applicant, and address flags merge into its mailing_address"
|
|
481
764
|
).option(
|
|
@@ -536,6 +819,7 @@ function registerSubmissions(program2) {
|
|
|
536
819
|
if (opts.payroll != null) body.wc_total_annual_payroll = intFlag(opts.payroll, "--payroll");
|
|
537
820
|
if (opts.insuredId) body.insured_id = opts.insuredId;
|
|
538
821
|
if (opts.producerEmail) body.producer_email = opts.producerEmail;
|
|
822
|
+
if (opts.hold) body.hold = true;
|
|
539
823
|
const finalApplicant = body.applicant;
|
|
540
824
|
if (!finalApplicant?.insured_name) throw new Error("--insured is required (or supply applicant.insured_name via --body)");
|
|
541
825
|
if (!body.narrative) throw new Error("--narrative is required (or supply narrative via --body)");
|
|
@@ -544,17 +828,24 @@ function registerSubmissions(program2) {
|
|
|
544
828
|
// Random per invocation unless the caller supplies a key. The API
|
|
545
829
|
// replays the original response when the same key is re-sent within
|
|
546
830
|
// 24h (per brokerage), so scripted retries should pass their own key.
|
|
547
|
-
headers: { "Idempotency-Key": opts.idempotencyKey ??
|
|
831
|
+
headers: { "Idempotency-Key": opts.idempotencyKey ?? randomUUID2() }
|
|
548
832
|
});
|
|
549
833
|
if (ctx.json) return printJson(res);
|
|
550
|
-
process.stdout.write(kv({
|
|
551
|
-
|
|
834
|
+
process.stdout.write(kv({
|
|
835
|
+
submission_id: res.submission_id,
|
|
836
|
+
state: res.state,
|
|
837
|
+
status: res.status_label,
|
|
838
|
+
marketing: res.marketing_status ?? "",
|
|
839
|
+
next_step: res.next_step ?? ""
|
|
840
|
+
}) + "\n");
|
|
841
|
+
printWarnings(res.warnings);
|
|
842
|
+
process.stdout.write("\n" + nextAfterCreate(String(res.submission_id), res.marketing_status) + "\n");
|
|
552
843
|
});
|
|
553
844
|
program2.command("upload <submissionId> <pdf>").description("Attach a PDF (ACORD, loss runs, supplement) to a submission").option("--name <label>", "display name for the document").action(async (submissionId, pdf, opts) => {
|
|
554
845
|
const ctx = makeCtx(program2.opts());
|
|
555
|
-
const bytes =
|
|
846
|
+
const bytes = readFileSync3(pdf);
|
|
556
847
|
const form = new FormData();
|
|
557
|
-
form.append("file", new Blob([bytes], { type: "application/pdf" }),
|
|
848
|
+
form.append("file", new Blob([bytes], { type: "application/pdf" }), basename2(pdf));
|
|
558
849
|
if (opts.name) form.append("display_name", opts.name);
|
|
559
850
|
const res = await multipartRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/documents`, form);
|
|
560
851
|
if (ctx.json) return printJson(res);
|
|
@@ -577,9 +868,9 @@ function registerSubmissions(program2) {
|
|
|
577
868
|
} else {
|
|
578
869
|
const hint = typeof r.marketing_hint === "string" && r.marketing_hint ? r.marketing_hint : null;
|
|
579
870
|
if (r.marketing_status === "matching") {
|
|
580
|
-
process.stdout.write("\nNo markets yet - " + (hint ?? "Hedge is matching carrier markets now (usually ~5 minutes after
|
|
871
|
+
process.stdout.write("\nNo markets yet - " + (hint ?? "Hedge is matching carrier markets now (usually ~5 minutes after create); check hedge markets " + submissionId + " in a few minutes.") + "\n");
|
|
581
872
|
} else if (r.marketing_status === "not_started") {
|
|
582
|
-
process.stdout.write("\nNo markets yet - " + (hint ?? "run hedge finalize " + submissionId + " to
|
|
873
|
+
process.stdout.write("\nNo markets yet - " + (hint ?? "this submission is held; run hedge finalize " + submissionId + " to release it.") + "\n");
|
|
583
874
|
} else if (r.marketing_status === "no_markets_matched") {
|
|
584
875
|
process.stdout.write("\nNo markets attached - " + (hint ?? "Hedge is reviewing options for this risk and will follow up.") + "\n");
|
|
585
876
|
} else {
|
|
@@ -609,7 +900,7 @@ function registerSubmissions(program2) {
|
|
|
609
900
|
quote: l.quote_premium ?? ""
|
|
610
901
|
}))), ["carrier", "line", "status", "quote"]) + "\n");
|
|
611
902
|
} else {
|
|
612
|
-
process.stdout.write("\nNo markets attached yet. Matching usually completes within ~5 minutes of
|
|
903
|
+
process.stdout.write("\nNo markets attached yet. Matching usually completes within ~5 minutes of create - re-run this shortly (or check hedge markets " + submissionId + ").\n");
|
|
613
904
|
}
|
|
614
905
|
const byStatus = s.status_summary?.by_status;
|
|
615
906
|
if (byStatus && Object.keys(byStatus).length) {
|
|
@@ -633,7 +924,7 @@ function registerSubmissions(program2) {
|
|
|
633
924
|
premium: r.premium ?? ""
|
|
634
925
|
})), ["id", "insured", "lines", "status", "premium"]) + "\n");
|
|
635
926
|
});
|
|
636
|
-
program2.command("finalize <submissionId>").description("
|
|
927
|
+
program2.command("finalize <submissionId>").description("Release a held submission: start the run for a submission created with --hold (idempotent on one already running)").option("--wait", "poll until markets attach (usually ~5 minutes), then print them").option("--timeout <minutes>", "how long --wait polls before giving up", "8").action(async (submissionId, opts) => {
|
|
637
928
|
const ctx = makeCtx(program2.opts());
|
|
638
929
|
const r = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/finalize`);
|
|
639
930
|
if (ctx.json && !opts.wait) return printJson(r);
|
|
@@ -641,43 +932,57 @@ function registerSubmissions(program2) {
|
|
|
641
932
|
const wait = Number(r.typical_wait_seconds) > 0 ? Math.round(Number(r.typical_wait_seconds) / 60) : 5;
|
|
642
933
|
process.stdout.write(
|
|
643
934
|
`Marketing started. Markets usually attach within ~${wait} minutes.
|
|
644
|
-
Track with: hedge
|
|
935
|
+
Track with: hedge markets ${submissionId} (or use finalize --wait next time)
|
|
645
936
|
`
|
|
646
937
|
);
|
|
647
938
|
return;
|
|
648
939
|
}
|
|
649
940
|
const timeoutMin = intFlag(opts.timeout, "--timeout");
|
|
650
941
|
const deadline = Date.now() + Math.max(1, timeoutMin) * 6e4;
|
|
651
|
-
process.stdout.write("Marketing started - waiting for markets to
|
|
652
|
-
let
|
|
942
|
+
process.stdout.write("Marketing started - waiting for markets to match (usually ~5 minutes)");
|
|
943
|
+
let req = null;
|
|
944
|
+
let noMarkets = false;
|
|
653
945
|
while (Date.now() < deadline) {
|
|
654
946
|
await new Promise((resolve) => setTimeout(resolve, 15e3));
|
|
655
947
|
process.stdout.write(".");
|
|
656
|
-
const
|
|
657
|
-
if (
|
|
658
|
-
|
|
948
|
+
const view = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/requirements`);
|
|
949
|
+
if (view.marketing_status === "matched" || view.markets?.length) {
|
|
950
|
+
req = view;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (view.marketing_status === "no_markets_matched") {
|
|
954
|
+
req = view;
|
|
955
|
+
noMarkets = true;
|
|
659
956
|
break;
|
|
660
957
|
}
|
|
661
958
|
}
|
|
662
959
|
process.stdout.write("\n");
|
|
663
|
-
if (!
|
|
960
|
+
if (!req) {
|
|
664
961
|
process.stdout.write(
|
|
665
962
|
`Still matching after ${timeoutMin} minutes - this can occasionally take longer.
|
|
666
|
-
Check in with: hedge
|
|
963
|
+
Check in with: hedge requirements ${submissionId}
|
|
667
964
|
`
|
|
668
965
|
);
|
|
669
966
|
return;
|
|
670
967
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
if (ctx.json) return printJson({ finalize: r,
|
|
677
|
-
|
|
968
|
+
if (noMarkets) {
|
|
969
|
+
if (ctx.json) return printJson({ finalize: r, requirements: req });
|
|
970
|
+
process.stdout.write((req.marketing_hint ?? "No carrier lanes attached - Hedge is reviewing options for this risk and will follow up.") + "\n");
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (ctx.json) return printJson({ finalize: r, requirements: req });
|
|
974
|
+
const rows = (req.markets || []).map((m) => ({
|
|
975
|
+
market: m.market_name,
|
|
976
|
+
ready: m.ready ? "yes" : "no",
|
|
977
|
+
needs_from_you: (m.needs_from_you || []).join("; ")
|
|
978
|
+
}));
|
|
979
|
+
process.stdout.write(`Matched ${rows.length} market(s):
|
|
678
980
|
|
|
679
|
-
` + table(rows, ["
|
|
680
|
-
|
|
981
|
+
` + table(rows, ["market", "ready", "needs_from_you"]) + "\n");
|
|
982
|
+
if (req.forms?.length) {
|
|
983
|
+
process.stdout.write("\nForms Hedge prepares: " + req.forms.join(", ") + "\n(download blanks with: hedge form <form_key>)\n");
|
|
984
|
+
}
|
|
985
|
+
process.stdout.write("\nThe three market categories, asks, assumptions and quotes: hedge markets " + submissionId + "\n");
|
|
681
986
|
});
|
|
682
987
|
program2.command("forms [search]").description('Search the blank application-form catalog, e.g. hedge forms "liquor"').action(async (search) => {
|
|
683
988
|
const ctx = makeCtx(program2.opts());
|
|
@@ -773,6 +1078,305 @@ Check in with: hedge status ${submissionId} or hedge requirements ${submission
|
|
|
773
1078
|
});
|
|
774
1079
|
}
|
|
775
1080
|
|
|
1081
|
+
// src/commands/thread.ts
|
|
1082
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1083
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1084
|
+
function who(m) {
|
|
1085
|
+
if (m.direction === "outbound") return "Hedge -> you";
|
|
1086
|
+
return "you -> Hedge";
|
|
1087
|
+
}
|
|
1088
|
+
function renderMessage(m) {
|
|
1089
|
+
const when = String(m.occurred_at ?? "").replace("T", " ").slice(0, 16);
|
|
1090
|
+
const head = `-- ${when} ${who(m)} [${m.channel}/${m.kind}]${m.subject ? " " + m.subject : ""}`;
|
|
1091
|
+
const lines = [head];
|
|
1092
|
+
if (m.from) lines.push(` from: ${m.from}${m.to?.length ? " to: " + m.to.join(", ") : ""}`);
|
|
1093
|
+
lines.push(String(m.body_text ?? "").trimEnd());
|
|
1094
|
+
if (m.attachments?.length) {
|
|
1095
|
+
lines.push(" attachments: " + m.attachments.map((a) => a.name + (a.download_path ? ` (${a.download_path})` : "")).join(", "));
|
|
1096
|
+
}
|
|
1097
|
+
return lines.join("\n");
|
|
1098
|
+
}
|
|
1099
|
+
function registerThread(program2) {
|
|
1100
|
+
program2.command("thread <submissionId>").description(
|
|
1101
|
+
"The conversation with Hedge on a submission, oldest first: the clearance email, questions, market-plan updates, quote deliveries, your replies. Follows next_cursor to the tail"
|
|
1102
|
+
).option("--since <cursor>", "start strictly after this cursor (a next_cursor/last_cursor from an earlier run); omit to read from the beginning").option("--limit <n>", "messages per page, 1-200", "50").option("--one-page", "read a single page instead of following next_cursor to the tail").action(async (submissionId, opts) => {
|
|
1103
|
+
const ctx = makeCtx(program2.opts());
|
|
1104
|
+
const messages = [];
|
|
1105
|
+
let since = opts.since;
|
|
1106
|
+
let lastCursor = opts.since ?? null;
|
|
1107
|
+
let nextCursor = null;
|
|
1108
|
+
for (; ; ) {
|
|
1109
|
+
const page = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/thread`, {
|
|
1110
|
+
query: { since, limit: String(opts.limit) }
|
|
1111
|
+
});
|
|
1112
|
+
messages.push(...page.messages ?? []);
|
|
1113
|
+
nextCursor = page.next_cursor ?? null;
|
|
1114
|
+
if (nextCursor) lastCursor = nextCursor;
|
|
1115
|
+
if (!nextCursor || opts.onePage) break;
|
|
1116
|
+
since = nextCursor;
|
|
1117
|
+
}
|
|
1118
|
+
if (ctx.json) return printJson({ messages, next_cursor: nextCursor, last_cursor: lastCursor });
|
|
1119
|
+
if (!messages.length) {
|
|
1120
|
+
process.stdout.write("No messages yet. Hedge opens the thread with the clearance email once the run starts.\n");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
process.stdout.write(messages.map(renderMessage).join("\n\n") + "\n");
|
|
1124
|
+
if (nextCursor) process.stdout.write(`
|
|
1125
|
+
More may follow. Continue with: hedge thread ${submissionId} --since ${nextCursor}
|
|
1126
|
+
`);
|
|
1127
|
+
else if (lastCursor) process.stdout.write(`
|
|
1128
|
+
At the tail. Resume later with: hedge thread ${submissionId} --since ${lastCursor} (dedupe by id)
|
|
1129
|
+
`);
|
|
1130
|
+
process.stdout.write(`Reply with: hedge reply ${submissionId} "..."
|
|
1131
|
+
`);
|
|
1132
|
+
});
|
|
1133
|
+
program2.command("reply <submissionId> <text>").description("Reply to Hedge on a submission exactly as you would reply to its email: answer a question, send a revision, authorize a bind. Use - to read the message from stdin").option("--attach <documentIds>", "comma-separated ids of documents already uploaded to this submission (hedge upload) to attach").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").option("--idempotency-key <key>", "Idempotency-Key header (default: a random UUID per invocation); a retry with the same key never sends the message twice").action(async (submissionId, text, opts) => {
|
|
1134
|
+
const ctx = makeCtx(program2.opts());
|
|
1135
|
+
const body = text === "-" ? readFileSync4(0, "utf8") : String(text);
|
|
1136
|
+
if (!body.trim()) throw new Error("the reply is empty");
|
|
1137
|
+
if (body.length > 2e4) throw new Error("the reply is over 20000 characters");
|
|
1138
|
+
const payload = { body };
|
|
1139
|
+
if (opts.attach) payload.document_ids = String(opts.attach).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1140
|
+
if (opts.producerEmail) payload.producer_email = opts.producerEmail;
|
|
1141
|
+
const res = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/thread`, {
|
|
1142
|
+
body: payload,
|
|
1143
|
+
headers: { "Idempotency-Key": opts.idempotencyKey ?? randomUUID3() }
|
|
1144
|
+
});
|
|
1145
|
+
if (ctx.json) return printJson(res);
|
|
1146
|
+
process.stdout.write(kv({ message_id: res.message_id, status: res.status, detail: res.detail }) + "\n");
|
|
1147
|
+
process.stdout.write(`
|
|
1148
|
+
Hedge answers on the thread, typically within a couple of minutes: hedge thread ${submissionId}
|
|
1149
|
+
`);
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/commands/bind.ts
|
|
1154
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1155
|
+
import { basename as basename3 } from "path";
|
|
1156
|
+
var PAYMENT_OPTIONS = ["in_full", "monthly"];
|
|
1157
|
+
function attestValue(opts) {
|
|
1158
|
+
if (opts.attest && opts.attestKeys) throw new Error("pass --attest or --attest-keys, not both");
|
|
1159
|
+
if (opts.attest) return true;
|
|
1160
|
+
if (opts.attestKeys) {
|
|
1161
|
+
const keys = String(opts.attestKeys).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1162
|
+
if (!keys.length) throw new Error("--attest-keys needs at least one key");
|
|
1163
|
+
return keys;
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
function fmtAssumption2(a) {
|
|
1168
|
+
const value = a.value === void 0 || a.value === null ? "" : ` = ${typeof a.value === "string" ? a.value : JSON.stringify(a.value)}`;
|
|
1169
|
+
return ` - ${a.key}: ${a.label ?? ""}${value}`;
|
|
1170
|
+
}
|
|
1171
|
+
function unconfirmedFrom(err) {
|
|
1172
|
+
if (!(err instanceof ApiError) || err.status !== 409) return null;
|
|
1173
|
+
const body = err.body;
|
|
1174
|
+
const detail = body?.detail;
|
|
1175
|
+
if (detail && typeof detail === "object" && detail.code === "assumptions_unconfirmed") return detail;
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
function printUnconfirmed(detail, howToAttest, json) {
|
|
1179
|
+
if (json) {
|
|
1180
|
+
printJson(detail);
|
|
1181
|
+
} else {
|
|
1182
|
+
process.stdout.write(
|
|
1183
|
+
`Assumptions unconfirmed (bind request ${detail.bind_request_id} kept as a draft; nothing was sent to the carrier).
|
|
1184
|
+
|
|
1185
|
+
Hedge obtained this quote on these assumptions. The retail agent must review each statement and confirm it:
|
|
1186
|
+
` + (detail.assumptions || []).map(fmtAssumption2).join("\n") + `
|
|
1187
|
+
|
|
1188
|
+
Once reviewed, confirm and continue with:
|
|
1189
|
+
${howToAttest} --attest
|
|
1190
|
+
${howToAttest} --attest-keys ${(detail.assumptions || []).map((a) => a.key).join(",") || "<key,key>"} (only the keys the agent confirmed)
|
|
1191
|
+
`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
process.exitCode = 1;
|
|
1195
|
+
}
|
|
1196
|
+
function printBindRequest(r) {
|
|
1197
|
+
process.stdout.write(kv({
|
|
1198
|
+
bind_request_id: r.bind_request_id,
|
|
1199
|
+
status: `${r.status}${r.status_label ? " (" + r.status_label + ")" : ""}`,
|
|
1200
|
+
quote_id: r.quote_id ?? "",
|
|
1201
|
+
payment: r.payment_option ?? "",
|
|
1202
|
+
requested_at: r.requested_at ?? "",
|
|
1203
|
+
requested_by: r.requested_by ?? "",
|
|
1204
|
+
effective: r.effective_date ?? "",
|
|
1205
|
+
policy_id: r.policy_id ?? ""
|
|
1206
|
+
}) + "\n");
|
|
1207
|
+
if (r.unconfirmed_assumptions?.length) {
|
|
1208
|
+
process.stdout.write("\nUnconfirmed assumptions (create/submit answer 409 until attested):\n" + r.unconfirmed_assumptions.map(fmtAssumption2).join("\n") + "\n");
|
|
1209
|
+
}
|
|
1210
|
+
if (r.contingencies?.length) {
|
|
1211
|
+
process.stdout.write("\nContingencies:\n" + table(r.contingencies.map((c) => ({
|
|
1212
|
+
id: c.id,
|
|
1213
|
+
kind: c.kind,
|
|
1214
|
+
label: c.label,
|
|
1215
|
+
status: c.status,
|
|
1216
|
+
required: c.required ? "yes" : "no",
|
|
1217
|
+
esign: c.esign_status ?? "",
|
|
1218
|
+
upload: c.upload_path ? "hedge bind-upload ... " + c.id + " <file.pdf>" : ""
|
|
1219
|
+
})), ["id", "kind", "label", "status", "required", "esign", "upload"]) + "\n");
|
|
1220
|
+
}
|
|
1221
|
+
if (r.next_steps?.length) {
|
|
1222
|
+
process.stdout.write("\nNext steps:\n" + r.next_steps.map((s) => " - " + s).join("\n") + "\n");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function registerBind(program2) {
|
|
1226
|
+
program2.command("bind <submissionId>").description(
|
|
1227
|
+
"Request to bind a released quote (the broker's bind authority). Drafts the bind request with one slot per carrier contingency; answers 409 with the assumptions to attest while any stand unconfirmed"
|
|
1228
|
+
).requiredOption("--quote <quoteId>", "the released quote to bind: a quote_id from hedge markets (indications cannot be bound)").option("--payment <option>", `payment plan: ${PAYMENT_OPTIONS.join(" or ")}`, "in_full").option("--attest", "confirm EVERY standing assumption on the quote's carrier application. Only after the retail agent has reviewed each statement; the confirmation is recorded under the producing broker").option("--attest-keys <keys>", "confirm only these assumption keys, comma-separated").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").action(async (submissionId, opts) => {
|
|
1229
|
+
if (!PAYMENT_OPTIONS.includes(opts.payment)) throw new Error(`--payment must be ${PAYMENT_OPTIONS.join(" or ")}`);
|
|
1230
|
+
const ctx = makeCtx(program2.opts());
|
|
1231
|
+
const body = { quote_id: opts.quote, payment_option: opts.payment, attest_assumptions: attestValue(opts) };
|
|
1232
|
+
if (opts.producerEmail) body.producer_email = opts.producerEmail;
|
|
1233
|
+
let r;
|
|
1234
|
+
try {
|
|
1235
|
+
r = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/bind-requests`, { body });
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
const detail = unconfirmedFrom(err);
|
|
1238
|
+
if (!detail) throw err;
|
|
1239
|
+
printUnconfirmed(detail, `hedge bind ${submissionId} --quote ${opts.quote} --payment ${opts.payment}${opts.producerEmail ? " --producer-email " + opts.producerEmail : ""}`, ctx.json);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (ctx.json) return printJson(r);
|
|
1243
|
+
printBindRequest(r);
|
|
1244
|
+
if (r.status === "drafted") {
|
|
1245
|
+
process.stdout.write(
|
|
1246
|
+
`
|
|
1247
|
+
Upload contingency documents with hedge bind-upload ${submissionId} ${r.bind_request_id} <contingencyId> <file.pdf>, then send it to Hedge for placement: hedge bind-submit ${submissionId} ${r.bind_request_id}
|
|
1248
|
+
`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
program2.command("bind-status <submissionId> [bindRequestId]").description("Bind requests on a submission (newest first), or one request in full: status, unconfirmed assumptions, contingencies, next steps").action(async (submissionId, bindRequestId) => {
|
|
1253
|
+
const ctx = makeCtx(program2.opts());
|
|
1254
|
+
if (bindRequestId) {
|
|
1255
|
+
const r = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/bind-requests/${bindRequestId}`);
|
|
1256
|
+
if (ctx.json) return printJson(r);
|
|
1257
|
+
printBindRequest(r);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const rows = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/bind-requests`);
|
|
1261
|
+
if (ctx.json) return printJson(rows);
|
|
1262
|
+
if (!rows.length) {
|
|
1263
|
+
process.stdout.write(`No bind requests yet. Start one with: hedge bind ${submissionId} --quote <quoteId>
|
|
1264
|
+
`);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
process.stdout.write(table(rows.map((r) => ({
|
|
1268
|
+
bind_request_id: r.bind_request_id,
|
|
1269
|
+
status: r.status_label ?? r.status,
|
|
1270
|
+
quote_id: r.quote_id ?? "",
|
|
1271
|
+
requested_at: String(r.requested_at ?? "").slice(0, 16).replace("T", " "),
|
|
1272
|
+
unconfirmed: (r.unconfirmed_assumptions || []).length,
|
|
1273
|
+
pending: (r.contingencies || []).filter((c) => c.status === "pending").length,
|
|
1274
|
+
policy_id: r.policy_id ?? ""
|
|
1275
|
+
})), ["bind_request_id", "status", "quote_id", "requested_at", "unconfirmed", "pending", "policy_id"]) + "\n");
|
|
1276
|
+
process.stdout.write(`
|
|
1277
|
+
Detail: hedge bind-status ${submissionId} <bindRequestId>
|
|
1278
|
+
`);
|
|
1279
|
+
});
|
|
1280
|
+
program2.command("bind-upload <submissionId> <bindRequestId> <contingencyId> <pdf>").description("Attach a PDF to a pre-bind contingency (accepted while the request is drafted); marks the item satisfied").option("--notes <text>", "notes for the underwriter").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").action(async (submissionId, bindRequestId, contingencyId, pdf, opts) => {
|
|
1281
|
+
const ctx = makeCtx(program2.opts());
|
|
1282
|
+
const form = new FormData();
|
|
1283
|
+
form.append("file", new Blob([readFileSync5(pdf)], { type: "application/pdf" }), basename3(pdf));
|
|
1284
|
+
if (opts.notes) form.append("notes", opts.notes);
|
|
1285
|
+
if (opts.producerEmail) form.append("producer_email", opts.producerEmail);
|
|
1286
|
+
const r = await multipartRequest(
|
|
1287
|
+
ctx.client,
|
|
1288
|
+
"POST",
|
|
1289
|
+
`/broker/submissions/${submissionId}/bind-requests/${bindRequestId}/contingencies/${contingencyId}/document`,
|
|
1290
|
+
form
|
|
1291
|
+
);
|
|
1292
|
+
if (ctx.json) return printJson(r);
|
|
1293
|
+
printBindRequest(r);
|
|
1294
|
+
});
|
|
1295
|
+
program2.command("bind-submit <submissionId> <bindRequestId>").description("Submit the drafted bind request to Hedge for placement with the carrier; answers 409 with the assumptions to attest while any stand unconfirmed").option("--attest", "confirm EVERY standing assumption and submit in one call (only after the retail agent has reviewed each statement)").option("--attest-keys <keys>", "confirm only these assumption keys, comma-separated").option("--producer-email <email>", "producing broker to attribute (required with a machine credential)").action(async (submissionId, bindRequestId, opts) => {
|
|
1296
|
+
const ctx = makeCtx(program2.opts());
|
|
1297
|
+
const attest = attestValue(opts);
|
|
1298
|
+
const body = {};
|
|
1299
|
+
if (attest !== false) body.attest_assumptions = attest;
|
|
1300
|
+
if (opts.producerEmail) body.producer_email = opts.producerEmail;
|
|
1301
|
+
let r;
|
|
1302
|
+
try {
|
|
1303
|
+
r = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/bind-requests/${bindRequestId}/submit`, {
|
|
1304
|
+
body: Object.keys(body).length ? body : void 0
|
|
1305
|
+
});
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
const detail = unconfirmedFrom(err);
|
|
1308
|
+
if (!detail) throw err;
|
|
1309
|
+
printUnconfirmed(detail, `hedge bind-submit ${submissionId} ${bindRequestId}${opts.producerEmail ? " --producer-email " + opts.producerEmail : ""}`, ctx.json);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (ctx.json) return printJson(r);
|
|
1313
|
+
printBindRequest(r);
|
|
1314
|
+
process.stdout.write(`
|
|
1315
|
+
Hedge is placing the bind. Track it with hedge bind-status ${submissionId} ${bindRequestId}; a bound event follows on the events feed and webhooks.
|
|
1316
|
+
`);
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/commands/programs.ts
|
|
1321
|
+
var CATEGORIES = ["instant_quote", "binding", "specialty"];
|
|
1322
|
+
function renderQuestions(questions, depth) {
|
|
1323
|
+
const pad = " ".repeat(depth);
|
|
1324
|
+
const out = [];
|
|
1325
|
+
for (const q of questions ?? []) {
|
|
1326
|
+
const flags = [q.required ? "required" : "", q.repeatable ? "repeatable" : "", q.depends_on?.length ? `depends on ${q.depends_on.join(",")}` : ""].filter(Boolean);
|
|
1327
|
+
const options = q.options?.length ? " options: " + q.options.map((o) => o.value === void 0 ? o.label : o.value).join("|") : "";
|
|
1328
|
+
out.push(`${pad}${q.key} [${q.type}${flags.length ? "; " + flags.join("; ") : ""}] ${q.label ?? ""}${options}`);
|
|
1329
|
+
if (q.help) out.push(`${pad} ${q.help}`);
|
|
1330
|
+
if (q.questions?.length) out.push(...renderQuestions(q.questions, depth + 1));
|
|
1331
|
+
}
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1334
|
+
function registerPrograms(program2) {
|
|
1335
|
+
program2.command("programs").description("The carrier programs your brokerage can quote through Hedge, one row per program with its category (instant_quote, binding, specialty)").option("--category <category>", `filter: ${CATEGORIES.join(", ")}`).option("--lob <slug>", "filter by line of business (canonical slug or alias, e.g. gl)").option("--state <ST>", "filter by 2-letter state").action(async (opts) => {
|
|
1336
|
+
if (opts.category && !CATEGORIES.includes(opts.category)) throw new Error(`--category must be one of ${CATEGORIES.join(", ")}`);
|
|
1337
|
+
const ctx = makeCtx(program2.opts());
|
|
1338
|
+
const rows = await apiRequest(ctx.client, "GET", "/broker/programs", {
|
|
1339
|
+
query: { category: opts.category, lob: opts.lob, state: opts.state ? String(opts.state).toUpperCase() : void 0 }
|
|
1340
|
+
});
|
|
1341
|
+
if (ctx.json) return printJson(rows);
|
|
1342
|
+
process.stdout.write(table(rows.map((p) => ({
|
|
1343
|
+
program_id: p.program_id,
|
|
1344
|
+
market: p.market_name,
|
|
1345
|
+
program: p.name,
|
|
1346
|
+
category: p.program_category ?? "",
|
|
1347
|
+
lines: (p.lines || []).join(","),
|
|
1348
|
+
states: p.states == null ? "all" : p.states.join(","),
|
|
1349
|
+
enabled: p.enabled ? "yes" : "no",
|
|
1350
|
+
schema: p.application_schema_available ? "yes" : ""
|
|
1351
|
+
})), ["program_id", "market", "program", "category", "lines", "states", "enabled", "schema"]) + "\n");
|
|
1352
|
+
process.stdout.write("\nQuestions an instant-quote program asks: hedge program-schema <programId>\n");
|
|
1353
|
+
});
|
|
1354
|
+
program2.command("program-schema <programId>").description("The application question schema for an instant-quote program: sections of questions with stable keys, types, options and dependencies, for your own application builder").option("--lob <slug>", "check the program lists this line").option("--state <ST>", "check the program is available in this state").action(async (programId, opts) => {
|
|
1355
|
+
const ctx = makeCtx(program2.opts());
|
|
1356
|
+
const s = await apiRequest(ctx.client, "GET", `/broker/programs/${programId}/application-schema`, {
|
|
1357
|
+
query: { lob: opts.lob, state: opts.state ? String(opts.state).toUpperCase() : void 0 }
|
|
1358
|
+
});
|
|
1359
|
+
if (ctx.json) return printJson(s);
|
|
1360
|
+
process.stdout.write(kv({
|
|
1361
|
+
program: s.name,
|
|
1362
|
+
program_id: s.program_id,
|
|
1363
|
+
market_id: s.market_id,
|
|
1364
|
+
category: s.program_category,
|
|
1365
|
+
contract: s.contract_version,
|
|
1366
|
+
fingerprint: s.schema_fingerprint,
|
|
1367
|
+
title: s.title ?? ""
|
|
1368
|
+
}) + "\n");
|
|
1369
|
+
if (s.description) process.stdout.write(s.description + "\n");
|
|
1370
|
+
for (const section of s.sections ?? []) {
|
|
1371
|
+
process.stdout.write(`
|
|
1372
|
+
## ${section.label} (${section.key})
|
|
1373
|
+
`);
|
|
1374
|
+
process.stdout.write(renderQuestions(section.questions, 1).join("\n") + "\n");
|
|
1375
|
+
}
|
|
1376
|
+
process.stdout.write("\nSave answers against these keys with the application-workspace checkpoint (see docs.hedgespecialty.com).\n");
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
776
1380
|
// src/commands/appetite.ts
|
|
777
1381
|
function requirementRows(items) {
|
|
778
1382
|
return items.map((q) => ({
|
|
@@ -873,9 +1477,13 @@ function registerQuotes(program2) {
|
|
|
873
1477
|
|
|
874
1478
|
// src/index.ts
|
|
875
1479
|
var program = new Command();
|
|
876
|
-
program.name("hedge").description("
|
|
1480
|
+
program.name("hedge").description("Send Hedge a risk from your terminal and track it to bind: intake, markets, thread, bind.").version("0.4.0").option("--staging", "use the staging environment").option("--json", "output raw JSON (for scripting)");
|
|
877
1481
|
registerAuth(program);
|
|
1482
|
+
registerIntake(program);
|
|
878
1483
|
registerSubmissions(program);
|
|
1484
|
+
registerThread(program);
|
|
1485
|
+
registerBind(program);
|
|
1486
|
+
registerPrograms(program);
|
|
879
1487
|
registerAppetite(program);
|
|
880
1488
|
registerQuotes(program);
|
|
881
1489
|
program.hook("preAction", () => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hedge-broker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Command-line tool for the Hedge
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Command-line tool for Hedge. Send a risk (text and PDFs), read the markets Hedge is trying, follow the thread, and bind, from your terminal or a script.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "hedge": "dist/index.js" },
|
|
7
7
|
"files": ["dist", "README.md", "CHANGELOG.md"],
|