hedge-broker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to `hedge-broker` are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org) and
5
+ [Keep a Changelog](https://keepachangelog.com).
6
+
7
+ ## [0.1.0] - 2026-07-06
8
+
9
+ ### Added
10
+
11
+ - Initial release.
12
+ - OAuth 2.1 sign-in: device-code (default, works over SSH), browser loopback (`--browser`), with automatic token refresh and `0600` credential storage.
13
+ - `login`, `logout`, `whoami`.
14
+ - Submissions: `submit`, `upload`, `requirements`, `finalize`, `status`, `submissions`.
15
+ - Instant carrier quotes: `quotes`, `answer`, `request-quote`.
16
+ - `appetite`, `policies`, `payments`.
17
+ - Global `--json` output and `--staging` environment switch.
18
+ - Distributed three ways: npm (`hedge-broker`), Homebrew (`taventech/tap/hedge`), and a self-contained binary via the curl installer.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taven Insurance Services LLC (dba Hedge Specialty)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # hedge
2
+
3
+ The Hedge broker portal from your terminal. Submit risks, check appetite, run instant carrier quotes, and track status, policies, and payments.
4
+
5
+ ## Install
6
+
7
+ Three ways to install. Pick one.
8
+
9
+ ### npm (requires Node.js 18 or newer)
10
+
11
+ ```bash
12
+ npm install -g hedge-broker # provides `hedge`
13
+ ```
14
+
15
+ Or run without installing:
16
+
17
+ ```bash
18
+ npx hedge-broker appetite "roofing contractor" --state CA
19
+ ```
20
+
21
+ ### Homebrew (self-contained binary, no Node required)
22
+
23
+ ```bash
24
+ brew install taventech/tap/hedge
25
+ ```
26
+
27
+ ### curl (self-contained binary, no Node required)
28
+
29
+ ```bash
30
+ curl -fsSL https://github.com/taventech/hedge-cli/releases/latest/download/install.sh | sh
31
+ ```
32
+
33
+ The installer downloads the binary for your OS and CPU into `$HOME/.local/bin`. Add that directory to your `PATH` if it is not already there.
34
+
35
+ ## Quickstart
36
+
37
+ ```bash
38
+ hedge login # device-code sign in (prints a code + URL)
39
+ hedge appetite "hvac contractor" --state TX
40
+ hedge submit --insured "Acme HVAC" --narrative "Residential HVAC install and repair" --lob commercial_general_liability --state TX
41
+ hedge upload <submission-id> ./acord-125.pdf
42
+ hedge finalize <submission-id> # start marketing to carriers
43
+ hedge status <submission-id>
44
+ ```
45
+
46
+ ## Commands
47
+
48
+ | Command | What it does |
49
+ | --- | --- |
50
+ | `hedge appetite <class> [--state ST] [--lob slug]` | Which markets have appetite for a class of business. |
51
+ | `hedge submit --insured <name> --narrative <text> [--lob a,b] [--state ST] [--effective YYYY-MM-DD]` | Create a submission (does not market it yet). |
52
+ | `hedge upload <submissionId> <file.pdf> [--name label]` | Attach an ACORD, loss runs, or supplement. |
53
+ | `hedge requirements <submissionId>` | What the submission still needs (per market, forms, carrier questions). |
54
+ | `hedge finalize <submissionId>` | Start marketing the submission to carriers. |
55
+ | `hedge status <submissionId>` | Submission detail plus live marketing and quote status. |
56
+ | `hedge submissions [--status state] [--search q]` | List your brokerage's submissions. |
57
+ | `hedge quotes <submissionId>` | List instant-quote carrier sessions and their open questions. |
58
+ | `hedge answer <submissionId> <sessionId> --set k=v [--set k=v ...]` | Answer a carrier session's questions. |
59
+ | `hedge request-quote <submissionId> <sessionId>` | Close a carrier session, request an indication, then a quote. |
60
+ | `hedge policies` | List bound policies. |
61
+ | `hedge payments` | List payment and invoice status. |
62
+ | `hedge whoami` | Show the signed-in broker and brokerage. |
63
+ | `hedge login` / `hedge logout` | Sign in and out. |
64
+
65
+ Add `--json` to any command for the raw API response, so the CLI composes in scripts:
66
+
67
+ ```bash
68
+ hedge submissions --json | jq '.[] | select(.status_label=="Quoted") | .insured_name'
69
+ ```
70
+
71
+ Add `--staging` to any command (or set `HEDGE_ENV=staging`) to target the staging environment.
72
+
73
+ ## Example: a submission end to end
74
+
75
+ ```bash
76
+ hedge login
77
+ hedge appetite "roofing contractor" --state CA
78
+ hedge submit \
79
+ --insured "Peak Roofing LLC" \
80
+ --narrative "Residential re-roofing, no hot tar, no work over 3 stories" \
81
+ --lob commercial_general_liability \
82
+ --state CA \
83
+ --effective 2026-08-01
84
+ hedge upload <submission-id> ./acord-125.pdf --name "ACORD 125"
85
+ hedge requirements <submission-id>
86
+ hedge finalize <submission-id>
87
+ hedge status <submission-id>
88
+ ```
89
+
90
+ ## Signing in
91
+
92
+ The CLI signs in with OAuth 2.1, so there are no API keys to copy around for the interactive flow, and it works over SSH.
93
+
94
+ | Mode | How | When to use |
95
+ | --- | --- | --- |
96
+ | Device code (default) | `hedge login` | Anywhere, including headless servers and SSH. Prints a short code and a URL to approve in any browser. |
97
+ | Browser (loopback) | `hedge login --browser` | A local machine with a browser. Opens it and captures the redirect on `127.0.0.1`. |
98
+
99
+ 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`.
100
+
101
+ The CLI requests the `broker_mcp` and `broker_submit` scopes. Submitting on a brokerage's behalf requires that the brokerage has connected apps and programmatic submission enabled by Hedge. Read commands (appetite, submissions, status, requirements, policies, payments) work with either scope. `submit`, `upload`, `finalize`, and the carrier-quote commands require `broker_submit`.
102
+
103
+ ## License
104
+
105
+ MIT. See [LICENSE](LICENSE).
package/dist/index.js ADDED
@@ -0,0 +1,552 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/core/pkce.ts
7
+ import { createHash, randomBytes } from "crypto";
8
+ function b64url(buf) {
9
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
+ }
11
+ function createPkce() {
12
+ const verifier = b64url(randomBytes(32));
13
+ const challenge = b64url(createHash("sha256").update(verifier).digest());
14
+ return { verifier, challenge };
15
+ }
16
+ function randomState() {
17
+ return b64url(randomBytes(16));
18
+ }
19
+
20
+ // src/core/config.ts
21
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
22
+ import { homedir } from "os";
23
+ import { dirname, join } from "path";
24
+ function configDir() {
25
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
26
+ return join(base, "taven-cli");
27
+ }
28
+ function tokenPath(cfg) {
29
+ return join(configDir(), `${cfg.appName}.${cfg.profile}.json`);
30
+ }
31
+ function saveToken(cfg, token) {
32
+ const p = tokenPath(cfg);
33
+ mkdirSync(dirname(p), { recursive: true });
34
+ writeFileSync(p, JSON.stringify(token, null, 2), { mode: 384 });
35
+ chmodSync(p, 384);
36
+ }
37
+ function loadToken(cfg) {
38
+ const p = tokenPath(cfg);
39
+ if (!existsSync(p)) return null;
40
+ try {
41
+ return JSON.parse(readFileSync(p, "utf8"));
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function clearToken(cfg) {
47
+ const p = tokenPath(cfg);
48
+ if (existsSync(p)) rmSync(p);
49
+ }
50
+
51
+ // src/core/oauth.ts
52
+ import { createServer } from "http";
53
+ var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
54
+ async function discover(metadataUrl) {
55
+ const res = await fetch(metadataUrl);
56
+ if (!res.ok) throw new Error(`Could not load auth metadata (${res.status}) from ${metadataUrl}`);
57
+ return await res.json();
58
+ }
59
+ async function registerClient(registrationEndpoint, clientName, redirectUris) {
60
+ const res = await fetch(registrationEndpoint, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({ client_name: clientName, redirect_uris: redirectUris })
64
+ });
65
+ if (!res.ok) throw new Error(`Client registration failed (${res.status})`);
66
+ const body = await res.json();
67
+ if (!body.client_id) throw new Error("Registration returned no client_id");
68
+ return body.client_id;
69
+ }
70
+ async function deviceLogin(opts) {
71
+ const { meta, clientId } = opts;
72
+ if (!meta.device_authorization_endpoint) throw new Error("This server doesn't support device login");
73
+ const body = new URLSearchParams({ client_id: clientId });
74
+ if (opts.scope) body.set("scope", opts.scope);
75
+ const start = await fetch(meta.device_authorization_endpoint, { method: "POST", headers: FORM, body });
76
+ if (!start.ok) throw new Error(`Device authorization failed (${start.status})`);
77
+ const d = await start.json();
78
+ opts.onPrompt({
79
+ verificationUri: d.verification_uri,
80
+ verificationUriComplete: d.verification_uri_complete,
81
+ userCode: d.user_code
82
+ });
83
+ if (opts.openBrowser) opts.openBrowser(d.verification_uri_complete || d.verification_uri);
84
+ let interval = (d.interval || 5) * 1e3;
85
+ const deadline = Date.now() + (d.expires_in || 900) * 1e3;
86
+ while (Date.now() < deadline) {
87
+ await sleep(interval);
88
+ const poll = await fetch(meta.token_endpoint, {
89
+ method: "POST",
90
+ headers: FORM,
91
+ body: new URLSearchParams({
92
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
93
+ device_code: d.device_code,
94
+ client_id: clientId
95
+ })
96
+ });
97
+ const pb = await poll.json();
98
+ if (poll.ok) return pb;
99
+ if (pb.error === "authorization_pending") continue;
100
+ if (pb.error === "slow_down") {
101
+ interval += 5e3;
102
+ continue;
103
+ }
104
+ throw new Error(pb.error === "access_denied" ? "Access was denied" : pb.error || `Login failed (${poll.status})`);
105
+ }
106
+ throw new Error("Login timed out \u2014 run the command again");
107
+ }
108
+ async function loopbackLogin(opts) {
109
+ const { meta, clientId } = opts;
110
+ const { verifier, challenge } = createPkce();
111
+ const state = randomState();
112
+ return new Promise((resolve, reject) => {
113
+ const server = createServer(async (req, res) => {
114
+ try {
115
+ const url = new URL(req.url || "/", `http://127.0.0.1`);
116
+ if (!url.pathname.startsWith("/callback")) {
117
+ res.writeHead(404).end();
118
+ return;
119
+ }
120
+ const code = url.searchParams.get("code");
121
+ const gotState = url.searchParams.get("state");
122
+ const err = url.searchParams.get("error");
123
+ res.writeHead(200, { "Content-Type": "text/html" }).end(
124
+ "<html><body style='font-family:sans-serif;text-align:center;padding:3rem'>" + (err ? "You can close this tab." : "Signed in. You can close this tab and return to your terminal.") + "</body></html>"
125
+ );
126
+ server.close();
127
+ if (err) return reject(new Error(err));
128
+ if (!code || gotState !== state) return reject(new Error("Invalid callback"));
129
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
130
+ const tr = await fetch(meta.token_endpoint, {
131
+ method: "POST",
132
+ headers: FORM,
133
+ body: new URLSearchParams({
134
+ grant_type: "authorization_code",
135
+ code,
136
+ code_verifier: verifier,
137
+ client_id: clientId,
138
+ redirect_uri: redirectUri
139
+ })
140
+ });
141
+ const tb = await tr.json();
142
+ if (!tr.ok) return reject(new Error(tb.error || `Token exchange failed (${tr.status})`));
143
+ resolve(tb);
144
+ } catch (e) {
145
+ reject(e);
146
+ }
147
+ });
148
+ server.listen(0, "127.0.0.1", () => {
149
+ const addr = server.address();
150
+ if (!addr || typeof addr === "string") return reject(new Error("Could not bind loopback port"));
151
+ port = addr.port;
152
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
153
+ const authUrl = new URL(meta.authorization_endpoint);
154
+ authUrl.searchParams.set("response_type", "code");
155
+ authUrl.searchParams.set("client_id", clientId);
156
+ authUrl.searchParams.set("redirect_uri", redirectUri);
157
+ authUrl.searchParams.set("code_challenge", challenge);
158
+ authUrl.searchParams.set("code_challenge_method", "S256");
159
+ authUrl.searchParams.set("state", state);
160
+ if (opts.scope) authUrl.searchParams.set("scope", opts.scope);
161
+ opts.openBrowser(authUrl.toString());
162
+ });
163
+ let port = 0;
164
+ });
165
+ }
166
+ async function refresh(tokenEndpoint, clientId, refreshToken) {
167
+ const res = await fetch(tokenEndpoint, {
168
+ method: "POST",
169
+ headers: FORM,
170
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId })
171
+ });
172
+ const body = await res.json();
173
+ if (!res.ok) throw new Error(body.error || `Token refresh failed (${res.status})`);
174
+ return body;
175
+ }
176
+ function sleep(ms) {
177
+ return new Promise((r) => setTimeout(r, ms));
178
+ }
179
+
180
+ // src/core/http.ts
181
+ var ApiError = class extends Error {
182
+ constructor(status, message) {
183
+ super(message);
184
+ this.status = status;
185
+ }
186
+ status;
187
+ };
188
+ async function bearer(cfg) {
189
+ const tok = loadToken(cfg);
190
+ if (!tok) throw new ApiError(401, "Not signed in \u2014 run `login` first");
191
+ if (tok.expires_at - 60 > Math.floor(Date.now() / 1e3)) return tok.access_token;
192
+ if (!tok.refresh_token) throw new ApiError(401, "Session expired \u2014 run `login` again");
193
+ try {
194
+ const r = await refresh(tok.token_endpoint, tok.client_id, tok.refresh_token);
195
+ const updated = {
196
+ ...tok,
197
+ access_token: r.access_token,
198
+ refresh_token: r.refresh_token ?? tok.refresh_token,
199
+ expires_at: Math.floor(Date.now() / 1e3) + (r.expires_in ?? 3600),
200
+ scope: r.scope ?? tok.scope
201
+ };
202
+ saveToken(cfg, updated);
203
+ return updated.access_token;
204
+ } catch {
205
+ clearToken(cfg);
206
+ throw new ApiError(401, "Session expired \u2014 run `login` again");
207
+ }
208
+ }
209
+ async function apiRequest(opts, method, path, init) {
210
+ const url = new URL(opts.apiBase.replace(/\/$/, "") + path);
211
+ for (const [k, v] of Object.entries(init?.query ?? {})) if (v != null) url.searchParams.set(k, v);
212
+ const headers = { Accept: "application/json", ...init?.headers ?? {} };
213
+ if (opts.apiKey) {
214
+ headers[opts.apiKeyHeader ?? "X-Api-Key"] = opts.apiKey;
215
+ } else {
216
+ headers.Authorization = `Bearer ${await bearer(opts.cfg)}`;
217
+ }
218
+ let body;
219
+ if (init?.body !== void 0) {
220
+ headers["Content-Type"] = "application/json";
221
+ body = JSON.stringify(init.body);
222
+ }
223
+ const res = await fetch(url, { method, headers, body });
224
+ const text = await res.text();
225
+ let parsed = void 0;
226
+ if (text) {
227
+ try {
228
+ parsed = JSON.parse(text);
229
+ } catch {
230
+ parsed = text;
231
+ }
232
+ }
233
+ if (!res.ok) {
234
+ const detail = parsed && typeof parsed === "object" && "detail" in parsed && parsed.detail || parsed && typeof parsed === "object" && "error" in parsed && parsed.error || (typeof parsed === "string" ? parsed : `Request failed (${res.status})`);
235
+ throw new ApiError(res.status, String(detail));
236
+ }
237
+ return parsed;
238
+ }
239
+ async function multipartRequest(opts, method, path, form) {
240
+ const url = opts.apiBase.replace(/\/$/, "") + path;
241
+ const headers = { Accept: "application/json" };
242
+ if (opts.apiKey) headers[opts.apiKeyHeader ?? "X-Api-Key"] = opts.apiKey;
243
+ else headers.Authorization = `Bearer ${await bearer(opts.cfg)}`;
244
+ const res = await fetch(url, { method, headers, body: form });
245
+ const text = await res.text();
246
+ let parsed = text ? JSON.parse(text) : void 0;
247
+ if (!res.ok) {
248
+ const detail = parsed && typeof parsed === "object" && "detail" in parsed && parsed.detail || `Upload failed (${res.status})`;
249
+ throw new ApiError(res.status, String(detail));
250
+ }
251
+ return parsed;
252
+ }
253
+
254
+ // src/core/output.ts
255
+ function printJson(data) {
256
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
257
+ }
258
+ function table(rows, columns) {
259
+ if (rows.length === 0) return "(none)";
260
+ const cols = columns ?? Object.keys(rows[0]);
261
+ const widths = cols.map(
262
+ (c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length))
263
+ );
264
+ const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
265
+ const head = line(cols.map((c) => c.toUpperCase()));
266
+ const body = rows.map((r) => line(cols.map((c) => String(r[c] ?? ""))));
267
+ return [head, ...body].join("\n");
268
+ }
269
+ function kv(obj) {
270
+ const width = Math.max(...Object.keys(obj).map((k) => k.length));
271
+ return Object.entries(obj).map(([k, v]) => `${k.padEnd(width)} ${v ?? ""}`).join("\n");
272
+ }
273
+
274
+ // src/core/browser.ts
275
+ import { spawn } from "child_process";
276
+ function openBrowser(url) {
277
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
278
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
279
+ try {
280
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
281
+ child.on("error", () => {
282
+ });
283
+ child.unref();
284
+ } catch {
285
+ }
286
+ }
287
+
288
+ // src/core/login.ts
289
+ async function loginInteractive(opts) {
290
+ const meta = await discover(opts.metadataUrl);
291
+ const mode = opts.mode ?? "device";
292
+ const redirectUris = ["http://127.0.0.1/callback", "http://localhost/callback"];
293
+ const clientId = opts.staticClientId ?? (meta.registration_endpoint ? await registerClient(meta.registration_endpoint, opts.clientName, redirectUris) : (() => {
294
+ throw new Error("Server has no client registration; a client_id is required");
295
+ })());
296
+ const tr = mode === "browser" ? await loopbackLogin({ meta, clientId, scope: opts.scope, openBrowser }) : await deviceLogin({
297
+ meta,
298
+ clientId,
299
+ scope: opts.scope,
300
+ openBrowser,
301
+ onPrompt: (info) => {
302
+ opts.log("");
303
+ opts.log(" To connect, open: " + info.verificationUri);
304
+ opts.log(" and enter code: " + info.userCode);
305
+ if (info.verificationUriComplete)
306
+ opts.log(" (or just open: " + info.verificationUriComplete + ")");
307
+ opts.log("");
308
+ opts.log("Waiting for approval\u2026");
309
+ }
310
+ });
311
+ saveToken(opts.cfg, {
312
+ access_token: tr.access_token,
313
+ refresh_token: tr.refresh_token,
314
+ expires_at: Math.floor(Date.now() / 1e3) + (tr.expires_in ?? 3600),
315
+ scope: tr.scope,
316
+ token_endpoint: meta.token_endpoint,
317
+ client_id: clientId
318
+ });
319
+ }
320
+
321
+ // src/context.ts
322
+ function resolveEnv(staging) {
323
+ if (staging || process.env.HEDGE_ENV === "staging") {
324
+ return {
325
+ metadataUrl: "https://staging-api.hedgespecialty.com/.well-known/oauth-authorization-server",
326
+ apiBase: "https://staging-api.hedgespecialty.com/api/v1",
327
+ brokerPortalBase: "https://staging-brokers.hedgespecialty.com"
328
+ };
329
+ }
330
+ return {
331
+ metadataUrl: "https://api.hedgespecialty.com/.well-known/oauth-authorization-server",
332
+ apiBase: "https://api.hedgespecialty.com/api/v1",
333
+ brokerPortalBase: "https://brokers.hedgespecialty.com"
334
+ };
335
+ }
336
+ function makeCtx(opts) {
337
+ const env = resolveEnv(!!opts.staging);
338
+ const cfg = { appName: "hedge", profile: opts.staging ? "staging" : "prod" };
339
+ return { env, cfg, client: { cfg, apiBase: env.apiBase }, json: !!opts.json };
340
+ }
341
+
342
+ // src/commands/auth.ts
343
+ var SCOPE = "broker_mcp broker_submit";
344
+ function registerAuth(program2) {
345
+ program2.command("login").description("Sign in to your Hedge broker account").option("--browser", "use the browser (loopback) flow instead of a device code").action(async (opts) => {
346
+ const g = program2.opts();
347
+ const ctx = makeCtx(g);
348
+ await loginInteractive({
349
+ cfg: ctx.cfg,
350
+ metadataUrl: ctx.env.metadataUrl,
351
+ clientName: "Hedge CLI",
352
+ scope: SCOPE,
353
+ mode: opts.browser ? "browser" : "device",
354
+ log: (m) => process.stdout.write(m + "\n")
355
+ });
356
+ process.stdout.write("\nSigned in.\n");
357
+ });
358
+ program2.command("logout").description("Remove stored credentials").action(() => {
359
+ const ctx = makeCtx(program2.opts());
360
+ clearToken(ctx.cfg);
361
+ process.stdout.write("Signed out.\n");
362
+ });
363
+ program2.command("whoami").description("Show the signed-in broker + brokerage").action(async () => {
364
+ const ctx = makeCtx(program2.opts());
365
+ const me = await apiRequest(ctx.client, "GET", "/broker/me");
366
+ if (ctx.json) return printJson(me);
367
+ const b = me.brokerage ?? {};
368
+ process.stdout.write(
369
+ kv({
370
+ email: me.email,
371
+ name: me.full_name,
372
+ brokerage: b.name,
373
+ role: me.role
374
+ }) + "\n"
375
+ );
376
+ });
377
+ }
378
+
379
+ // src/commands/submissions.ts
380
+ import { readFileSync as readFileSync2 } from "fs";
381
+ import { basename } from "path";
382
+ function registerSubmissions(program2) {
383
+ program2.command("submit").description("Create a submission (does not market it \u2014 run `finalize` when ready)").requiredOption("--insured <name>", "insured business name").requiredOption("--narrative <text>", "operations description of the risk").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)").action(async (opts) => {
384
+ const ctx = makeCtx(program2.opts());
385
+ const body = {
386
+ applicant: { insured_name: opts.insured, ...opts.state ? { mailing_address: void 0 } : {} },
387
+ narrative: opts.narrative,
388
+ lines_of_business: opts.lob ? String(opts.lob).split(",").map((s) => s.trim()) : []
389
+ };
390
+ if (opts.effective) body.effective_date = opts.effective;
391
+ const res = await apiRequest(ctx.client, "POST", "/broker/submissions", { body });
392
+ if (ctx.json) return printJson(res);
393
+ process.stdout.write(kv({ submission_id: res.submission_id, state: res.state, status: res.status_label }) + "\n");
394
+ process.stdout.write("\nNext: hedge upload " + res.submission_id + " <file.pdf>, hedge requirements " + res.submission_id + ", hedge finalize " + res.submission_id + "\n");
395
+ });
396
+ 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) => {
397
+ const ctx = makeCtx(program2.opts());
398
+ const bytes = readFileSync2(pdf);
399
+ const form = new FormData();
400
+ form.append("file", new Blob([bytes], { type: "application/pdf" }), basename(pdf));
401
+ if (opts.name) form.append("display_name", opts.name);
402
+ const res = await multipartRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/documents`, form);
403
+ if (ctx.json) return printJson(res);
404
+ process.stdout.write(kv({ document_id: res.id, name: res.display_name, kind: res.source_label ?? res.source }) + "\n");
405
+ });
406
+ program2.command("requirements <submissionId>").description("What this submission still needs (per-market, forms, carrier questions)").action(async (submissionId) => {
407
+ const ctx = makeCtx(program2.opts());
408
+ const r = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/requirements`);
409
+ if (ctx.json) return printJson(r);
410
+ process.stdout.write(kv({ insured: r.insured_name, lines: (r.lines || []).join(", "), state: r.state, forms: (r.forms || []).join(", ") }) + "\n");
411
+ if (r.markets?.length) {
412
+ process.stdout.write("\nMarkets:\n" + table(r.markets.map((m) => ({
413
+ market: m.market_name,
414
+ ready: m.ready ? "yes" : "no",
415
+ needs_from_you: (m.needs_from_you || []).join("; ")
416
+ })), ["market", "ready", "needs_from_you"]) + "\n");
417
+ }
418
+ if (r.carrier_api_sessions?.length) {
419
+ process.stdout.write("\nInstant-quote carriers:\n" + table(r.carrier_api_sessions.map((s) => ({
420
+ carrier: s.carrier_slug,
421
+ program: s.program,
422
+ status: s.status,
423
+ missing: (s.missing_questions || []).length,
424
+ quote: s.has_quote ? "yes" : ""
425
+ })), ["carrier", "program", "status", "missing", "quote"]) + "\n");
426
+ }
427
+ });
428
+ program2.command("status <submissionId>").description("Submission detail + live marketing/quote status").action(async (submissionId) => {
429
+ const ctx = makeCtx(program2.opts());
430
+ const s = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}`);
431
+ if (ctx.json) return printJson(s);
432
+ process.stdout.write(kv({ insured: s.insured_name, lines: (s.lines || []).join(", "), state: s.state, status: s.status_label, premium: s.premium }) + "\n");
433
+ });
434
+ program2.command("submissions").description("List your brokerage's submissions").option("--status <state>", "filter by state").option("--search <q>", "filter by insured name").action(async (opts) => {
435
+ const ctx = makeCtx(program2.opts());
436
+ const rows = await apiRequest(ctx.client, "GET", "/broker/submissions", {
437
+ query: { status: opts.status, search: opts.search }
438
+ });
439
+ if (ctx.json) return printJson(rows);
440
+ process.stdout.write(table(rows.map((r) => ({
441
+ id: r.id,
442
+ insured: r.insured_name,
443
+ lines: (r.lines || []).join(","),
444
+ status: r.status_label,
445
+ premium: r.premium ?? ""
446
+ })), ["id", "insured", "lines", "status", "premium"]) + "\n");
447
+ });
448
+ program2.command("finalize <submissionId>").description("Start marketing the submission to carriers").action(async (submissionId) => {
449
+ const ctx = makeCtx(program2.opts());
450
+ const r = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/finalize`);
451
+ if (ctx.json) return printJson(r);
452
+ process.stdout.write("Marketing started. Track with: hedge status " + submissionId + "\n");
453
+ });
454
+ program2.command("policies").description("List bound policies").action(async () => {
455
+ const ctx = makeCtx(program2.opts());
456
+ const rows = await apiRequest(ctx.client, "GET", "/broker/policies");
457
+ if (ctx.json) return printJson(rows);
458
+ process.stdout.write(table(rows.map((r) => ({ insured: r.insured_name, carrier: r.carrier_name, policy: r.policy_number, premium: r.premium ?? "", status: r.status_label ?? r.status })), ["insured", "carrier", "policy", "premium", "status"]) + "\n");
459
+ });
460
+ program2.command("payments").description("List payment / invoice status").action(async () => {
461
+ const ctx = makeCtx(program2.opts());
462
+ const rows = await apiRequest(ctx.client, "GET", "/broker/payments");
463
+ if (ctx.json) return printJson(rows);
464
+ process.stdout.write(table(rows.map((r) => ({ insured: r.insured_name, status: r.status_label ?? r.status, premium: r.premium ?? "" })), ["insured", "status", "premium"]) + "\n");
465
+ });
466
+ }
467
+
468
+ // src/commands/appetite.ts
469
+ function registerAppetite(program2) {
470
+ program2.command("appetite <class>").description('Which markets have appetite for a class, e.g. hedge appetite "roofing contractor" --state CA').option("--state <ST>", "2-letter state").option("--lob <slug>", "line of business filter").action(async (klass, opts) => {
471
+ const ctx = makeCtx(program2.opts());
472
+ const res = await apiRequest(ctx.client, "GET", "/broker/appetite", {
473
+ query: { q: klass, state: opts.state, lob: opts.lob }
474
+ });
475
+ if (ctx.json) return printJson(res);
476
+ const rows = (res.results || []).map((m) => ({
477
+ market: m.name,
478
+ lines: (m.lines || []).map((l) => l.slug ?? l).join(","),
479
+ programs: (m.matched_program_names || []).join("; "),
480
+ turnaround: m.turnaround?.median_hours != null ? `${Math.round(m.turnaround.median_hours)}h median` : ""
481
+ }));
482
+ process.stdout.write(table(rows, ["market", "lines", "programs", "turnaround"]) + "\n");
483
+ });
484
+ }
485
+
486
+ // src/commands/quotes.ts
487
+ function registerQuotes(program2) {
488
+ program2.command("quotes <submissionId>").description("List instant-quote carrier sessions and their open questions").action(async (submissionId) => {
489
+ const ctx = makeCtx(program2.opts());
490
+ const rows = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/api-quotes/sessions`);
491
+ if (ctx.json) return printJson(rows);
492
+ process.stdout.write(table(rows.map((s) => ({
493
+ session: s.id,
494
+ carrier: s.carrier_slug,
495
+ program: s.program_display_name ?? s.program_identifier,
496
+ status: s.status,
497
+ outcome: s.outcome ?? "",
498
+ missing: (s.missing_required_questions_json || []).length
499
+ })), ["session", "carrier", "program", "status", "outcome", "missing"]) + "\n");
500
+ });
501
+ program2.command("answer <submissionId> <sessionId>").description("Answer a carrier session's questions, e.g. --set years_in_business=8 --set employees=12").option("--set <kv...>", "key=value pairs").action(async (submissionId, sessionId, opts) => {
502
+ const ctx = makeCtx(program2.opts());
503
+ const answers = {};
504
+ for (const pair of opts.set || []) {
505
+ const i = String(pair).indexOf("=");
506
+ if (i > 0) answers[String(pair).slice(0, i)] = String(pair).slice(i + 1);
507
+ }
508
+ const res = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/api-quotes/sessions/${sessionId}/answers`, {
509
+ body: { answers, merge: true }
510
+ });
511
+ if (ctx.json) return printJson(res);
512
+ process.stdout.write(`status: ${res.status} | remaining questions: ${(res.missing_required_questions_json || []).length}
513
+ `);
514
+ });
515
+ program2.command("request-quote <submissionId> <sessionId>").description("Close a carrier session \u2014 request an indication then a quote").action(async (submissionId, sessionId) => {
516
+ const ctx = makeCtx(program2.opts());
517
+ const res = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/api-quotes/sessions/${sessionId}/close`);
518
+ if (ctx.json) return printJson(res);
519
+ process.stdout.write(`outcome: ${res.outcome} | status: ${res.status}${res.quote_pdf_url ? " | quote: " + res.quote_pdf_url : ""}
520
+ `);
521
+ if ((res.missing_required_questions_json || []).length)
522
+ process.stdout.write(`still needs ${(res.missing_required_questions_json || []).length} answers \u2014 use \`hedge answer\`
523
+ `);
524
+ });
525
+ }
526
+
527
+ // src/index.ts
528
+ var program = new Command();
529
+ program.name("hedge").description("Submit risks to Hedge and track them from your terminal.").version("0.1.0").option("--staging", "use the staging environment").option("--json", "output raw JSON (for scripting)");
530
+ registerAuth(program);
531
+ registerSubmissions(program);
532
+ registerAppetite(program);
533
+ registerQuotes(program);
534
+ program.hook("preAction", () => {
535
+ });
536
+ async function main() {
537
+ try {
538
+ await program.parseAsync(process.argv);
539
+ } catch (err) {
540
+ if (err instanceof ApiError && err.status === 401) {
541
+ process.stderr.write(`
542
+ ${err.message}
543
+ `);
544
+ process.exit(1);
545
+ }
546
+ process.stderr.write(`
547
+ Error: ${err instanceof Error ? err.message : String(err)}
548
+ `);
549
+ process.exit(1);
550
+ }
551
+ }
552
+ main();
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "hedge-broker",
3
+ "version": "0.1.0",
4
+ "description": "Command-line tool for the Hedge broker portal. Submit risks, check appetite, track quotes and policies.",
5
+ "type": "module",
6
+ "bin": { "hedge": "dist/index.js" },
7
+ "files": ["dist", "README.md", "CHANGELOG.md"],
8
+ "scripts": {
9
+ "build": "tsup",
10
+ "prepublishOnly": "tsup"
11
+ },
12
+ "engines": { "node": ">=18" },
13
+ "license": "MIT",
14
+ "author": "Hedge Specialty Insurance",
15
+ "homepage": "https://github.com/taventech/hedge-cli#readme",
16
+ "repository": { "type": "git", "url": "git+https://github.com/taventech/hedge-cli.git" },
17
+ "bugs": { "url": "https://github.com/taventech/hedge-cli/issues" },
18
+ "keywords": ["hedge", "insurance", "broker", "cli", "submissions", "commercial-insurance", "appetite"],
19
+ "publishConfig": { "access": "public" },
20
+ "dependencies": {
21
+ "commander": "^12.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "tsup": "^8",
25
+ "typescript": "^5",
26
+ "@types/node": "^22"
27
+ }
28
+ }