@primitivedotdev/sdk 0.10.0 → 0.11.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.
@@ -7,80 +7,126 @@ function flagName(parameterName) {
7
7
  function flagDescription(parameter) {
8
8
  return parameter.description ?? parameter.name;
9
9
  }
10
- /**
11
- * Render a one-shot description of a JSON Schema's top-level
12
- * properties so an agent reading `<command> --help` can build a
13
- * valid `--body` payload without probing the server. Pulled from
14
- * the resolved request schema embedded on the manifest.
15
- *
16
- * Format prioritizes scanability over completeness: required
17
- * fields first, each line is `<name> <type> [- description]`
18
- * truncated to a reasonable width. Callers who need the full
19
- * schema can run `primitive list-operations | jq` and read
20
- * `requestSchema` directly.
21
- */
22
- function renderRequestSchemaSummary(schema) {
10
+ function extractBodyFields(schema) {
23
11
  if (!schema || typeof schema !== "object")
24
- return null;
12
+ return [];
25
13
  const properties = schema.properties;
26
14
  if (!properties || typeof properties !== "object")
27
- return null;
15
+ return [];
28
16
  const requiredArr = Array.isArray(schema.required)
29
17
  ? schema.required.filter((k) => typeof k === "string")
30
18
  : [];
31
19
  const required = new Set(requiredArr);
32
- const entries = Object.entries(properties)
33
- .map(([name, raw]) => {
20
+ const fields = [];
21
+ for (const [name, raw] of Object.entries(properties)) {
34
22
  const propSchema = raw && typeof raw === "object" ? raw : {};
35
- let type = "any";
36
23
  const t = propSchema.type;
24
+ let displayType = "any";
25
+ let kind = "complex";
37
26
  if (typeof t === "string") {
38
- type = t;
39
- if (type === "array") {
27
+ displayType = t;
28
+ if (t === "string")
29
+ kind = "string";
30
+ else if (t === "integer" || t === "number")
31
+ kind = "integer";
32
+ else if (t === "boolean")
33
+ kind = "boolean";
34
+ else if (t === "array") {
40
35
  const items = propSchema.items;
41
36
  if (items && typeof items === "object") {
42
37
  const itemType = items.type;
43
38
  if (typeof itemType === "string") {
44
- type = `array<${itemType}>`;
39
+ displayType = `array<${itemType}>`;
45
40
  }
46
41
  }
42
+ kind = "complex";
43
+ }
44
+ else {
45
+ kind = "complex";
47
46
  }
48
47
  }
49
48
  else if (Array.isArray(t)) {
50
- // Nullable shorthand the codegen normalizes to e.g. ["string","null"].
49
+ // Nullable shorthand the codegen normalizes to e.g.
50
+ // ["string","null"]. If exactly one non-null member, surface
51
+ // it as that scalar with a trailing `?`.
51
52
  const nonNull = t.filter((s) => s !== "null");
52
- type = nonNull.length === 1 ? `${nonNull[0]}?` : nonNull.join("|");
53
+ if (nonNull.length === 1) {
54
+ const single = nonNull[0];
55
+ displayType = `${single}?`;
56
+ if (single === "string")
57
+ kind = "string";
58
+ else if (single === "integer" || single === "number")
59
+ kind = "integer";
60
+ else if (single === "boolean")
61
+ kind = "boolean";
62
+ else
63
+ kind = "complex";
64
+ }
65
+ else {
66
+ displayType = nonNull.join("|");
67
+ kind = "complex";
68
+ }
53
69
  }
54
70
  const description = typeof propSchema.description === "string"
55
71
  ? propSchema.description.split("\n")[0].trim()
56
72
  : "";
57
- return {
73
+ const enumRaw = propSchema.enum;
74
+ const enumValues = kind === "string" && Array.isArray(enumRaw)
75
+ ? enumRaw.filter((e) => typeof e === "string")
76
+ : undefined;
77
+ fields.push({
58
78
  name,
59
- type,
60
79
  description,
61
80
  required: required.has(name),
62
- };
63
- })
64
- .sort((a, b) => {
81
+ displayType,
82
+ kind,
83
+ ...(enumValues && enumValues.length > 0 ? { enumValues } : {}),
84
+ });
85
+ }
86
+ return fields.sort((a, b) => {
65
87
  if (a.required !== b.required)
66
88
  return a.required ? -1 : 1;
67
89
  return a.name.localeCompare(b.name);
68
90
  });
69
- if (entries.length === 0)
91
+ }
92
+ /**
93
+ * Render a "Body fields" summary for the per-command help.
94
+ *
95
+ * Most scalar fields are exposed as individual `--flag` flags,
96
+ * which oclif auto-renders in the FLAGS section above. To avoid
97
+ * duplicating that, the summary here only documents fields that
98
+ * MUST go through `--body` (complex types: arrays, objects,
99
+ * mixed-non-nullable). When an operation has only scalars, the
100
+ * summary is omitted entirely and oclif's FLAGS section is the
101
+ * full story.
102
+ *
103
+ * For operations with mixed scalar and complex fields, we also
104
+ * include a short header pointing the agent at the flag form so
105
+ * the natural reading is "use the flags above; --body for the
106
+ * leftovers below."
107
+ */
108
+ function renderRequestSchemaSummary(schema) {
109
+ const fields = extractBodyFields(schema);
110
+ if (fields.length === 0)
70
111
  return null;
71
- const nameWidth = Math.min(24, Math.max(...entries.map((e) => e.name.length)));
72
- const lines = ["Body fields (JSON --body):"];
112
+ const complex = fields.filter((f) => f.kind === "complex");
113
+ if (complex.length === 0)
114
+ return null;
115
+ const nameWidth = Math.min(24, Math.max(...complex.map((f) => f.name.length)));
73
116
  const descMax = 78;
74
- for (const e of entries) {
75
- const flag = e.required ? " *" : " ";
76
- const padName = e.name.padEnd(nameWidth);
77
- const trimmedDesc = e.description.length > descMax
78
- ? `${e.description.slice(0, descMax - 3)}...`
79
- : e.description;
117
+ const lines = [
118
+ "Body fields requiring --body JSON (these are not exposed as flags):",
119
+ ];
120
+ for (const f of complex) {
121
+ const marker = f.required ? " *" : " ";
122
+ const padName = f.name.padEnd(nameWidth);
123
+ const trimmedDesc = f.description.length > descMax
124
+ ? `${f.description.slice(0, descMax - 3)}...`
125
+ : f.description;
80
126
  const desc = trimmedDesc ? ` ${trimmedDesc}` : "";
81
- lines.push(`${flag} ${padName} ${e.type}${desc}`);
127
+ lines.push(`${marker} ${padName} ${f.displayType}${desc}`);
82
128
  }
83
- lines.push("(* = required)");
129
+ lines.push("(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)");
84
130
  return lines.join("\n");
85
131
  }
86
132
  export function flagForParameter(parameter) {
@@ -196,6 +242,44 @@ export function formatErrorPayload(payload) {
196
242
  }
197
243
  return JSON.stringify(payload, null, 2);
198
244
  }
245
+ // Reserved flag names the body-field expander must never overwrite.
246
+ // `--body` and `--body-file` are the JSON escape hatches.
247
+ // `--api-key`, `--base-url`, `--output` are infra. Path and query
248
+ // params get added before body fields and take precedence.
249
+ const RESERVED_FLAG_NAMES = new Set([
250
+ "api-key",
251
+ "base-url",
252
+ "body",
253
+ "body-file",
254
+ "output",
255
+ ]);
256
+ function bodyFieldFlag(field) {
257
+ // Flag descriptions cap at 80 chars so oclif's --help output
258
+ // stays readable; the schema's full description is also visible
259
+ // via `primitive list-operations | jq`.
260
+ const descMax = 80;
261
+ const trimmedDesc = field.description.length > descMax
262
+ ? `${field.description.slice(0, descMax - 3)}...`
263
+ : field.description;
264
+ // Field-flag UX choice: do NOT mark scalar body fields as
265
+ // required at the oclif level even when the JSON Schema marks
266
+ // them required. Reason: a caller can satisfy the requirement
267
+ // either via the individual flag OR via --body / --body-file.
268
+ // Marking the flag required would force the individual-flag
269
+ // form. The runtime body merger validates the final assembled
270
+ // body against the same server-side schema either way.
271
+ const common = {
272
+ description: trimmedDesc || field.name,
273
+ };
274
+ if (field.kind === "boolean")
275
+ return Flags.boolean(common);
276
+ if (field.kind === "integer")
277
+ return Flags.integer(common);
278
+ if (field.enumValues) {
279
+ return Flags.string({ ...common, options: field.enumValues });
280
+ }
281
+ return Flags.string(common);
282
+ }
199
283
  function buildFlags(operation) {
200
284
  const flags = {
201
285
  "api-key": Flags.string({
@@ -210,18 +294,67 @@ function buildFlags(operation) {
210
294
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) {
211
295
  flags[flagName(parameter.name)] = flagForParameter(parameter);
212
296
  }
297
+ const bodyFieldFlagToProperty = new Map();
213
298
  if (operation.hasJsonBody) {
214
- flags.body = Flags.string({ description: "JSON request body" });
299
+ flags.body = Flags.string({
300
+ description: "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
301
+ });
215
302
  flags["body-file"] = Flags.string({
216
- description: "Path to a JSON file used as the request body",
303
+ description: "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
217
304
  });
305
+ // Expand top-level scalar body fields into individual flags so
306
+ // `primitive sending:send-email --to alice@x --from support@x
307
+ // --body-text "hi"` works without constructing JSON. Driven by
308
+ // the requestSchema embedded on the manifest. Skip flags that
309
+ // collide with reserved names or with path/query params already
310
+ // added above; those collisions fall back to --body.
311
+ //
312
+ // Collisions are tracked in the returned map so the run()
313
+ // handler doesn't misread a path/query param's value as a
314
+ // body-field override. (A naive "look up parsedFlags[name]"
315
+ // pass would happily pick up the path param's value and
316
+ // silently write it into the body.)
317
+ const bodyFields = extractBodyFields(operation.requestSchema);
318
+ for (const field of bodyFields) {
319
+ if (field.kind === "complex")
320
+ continue;
321
+ const name = flagName(field.name);
322
+ if (RESERVED_FLAG_NAMES.has(name))
323
+ continue;
324
+ if (flags[name] !== undefined)
325
+ continue;
326
+ flags[name] = bodyFieldFlag(field);
327
+ bodyFieldFlagToProperty.set(name, field.name);
328
+ }
218
329
  }
219
330
  if (operation.binaryResponse) {
220
331
  flags.output = Flags.string({
221
332
  description: "Write binary response bytes to a file",
222
333
  });
223
334
  }
224
- return flags;
335
+ return { flags, bodyFieldFlagToProperty };
336
+ }
337
+ // Pull body field values out of the parsed CLI flags. Returns
338
+ // only fields the user actually supplied (omits undefined). Used
339
+ // to override / extend the JSON --body when both forms are
340
+ // present (per-field flags take precedence on key conflicts).
341
+ //
342
+ // The `bodyFieldFlagToProperty` allowlist comes from buildFlags and
343
+ // records ONLY the flags actually registered as body-field flags.
344
+ // Without it, this function would naively read parsedFlags by
345
+ // kebab-cased field name and pick up values from a colliding path
346
+ // or query param flag, silently writing them into the body under
347
+ // the body-field key. The allowlist keeps the merge honest: only
348
+ // flags this CLI generator owns end up in the body.
349
+ function collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty) {
350
+ const result = {};
351
+ for (const [flag, property] of bodyFieldFlagToProperty) {
352
+ const value = parsedFlags[flag];
353
+ if (value === undefined)
354
+ continue;
355
+ result[property] = value;
356
+ }
357
+ return result;
225
358
  }
226
359
  function collectValues(parameters, flags) {
227
360
  const values = {};
@@ -234,7 +367,7 @@ function collectValues(parameters, flags) {
234
367
  return values;
235
368
  }
236
369
  export function createOperationCommand(operation) {
237
- const flags = buildFlags(operation);
370
+ const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
238
371
  // Append a "Body fields" summary to the description so agents
239
372
  // running `<command> --help` learn the JSON shape immediately.
240
373
  // Without this, `--help` only said "JSON request body" and agents
@@ -262,11 +395,48 @@ export function createOperationCommand(operation) {
262
395
  ? parsedFlags["base-url"]
263
396
  : undefined,
264
397
  });
265
- const body = operation.hasJsonBody
266
- ? readJsonBody(parsedFlags)
267
- : undefined;
398
+ // Two body sources, merged: explicit JSON via --body /
399
+ // --body-file (the base) plus per-field flags (the
400
+ // overrides). Per-field flag values take precedence on key
401
+ // conflicts so a caller can pass a base payload via --body
402
+ // and override one field on the command line.
403
+ let body;
404
+ if (operation.hasJsonBody) {
405
+ const explicit = readJsonBody(parsedFlags);
406
+ const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
407
+ if (Object.keys(overrides).length > 0) {
408
+ if (explicit === undefined) {
409
+ body = overrides;
410
+ }
411
+ else if (explicit !== null &&
412
+ typeof explicit === "object" &&
413
+ !Array.isArray(explicit)) {
414
+ body = { ...explicit, ...overrides };
415
+ }
416
+ else {
417
+ // Caller passed --body as null, an array, or a
418
+ // primitive AND also passed per-field flags. We can't
419
+ // merge per-field overrides into a non-object body
420
+ // shape, and silently dropping either source would
421
+ // leave the caller's actual intent unclear. Refuse
422
+ // loudly so the next attempt is unambiguous.
423
+ const explicitKind = explicit === null
424
+ ? "null"
425
+ : Array.isArray(explicit)
426
+ ? "array"
427
+ : typeof explicit;
428
+ const overrideFlags = Object.keys(overrides)
429
+ .map((p) => `--${flagName(p)}`)
430
+ .join(", ");
431
+ throw new Errors.CLIError(`--body must be a JSON object when also passing per-field flags (got ${explicitKind}); supplied per-field flags: ${overrideFlags}. Either drop --body and rely on the per-field flags, or move every field into the JSON --body and drop the flags.`);
432
+ }
433
+ }
434
+ else {
435
+ body = explicit;
436
+ }
437
+ }
268
438
  if (operation.bodyRequired && body === undefined) {
269
- throw new Errors.CLIError(`Operation ${operation.operationId} requires --body or --body-file`);
439
+ throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --body / --body-file.`);
270
440
  }
271
441
  const operationFn = operations[operation.sdkName];
272
442
  const result = await operationFn({
@@ -0,0 +1,159 @@
1
+ import { Command, Errors, Flags } from "@oclif/core";
2
+ import { listDomains, sendEmail } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, formatErrorPayload } from "../api-command.js";
5
+ // `primitive send` is the agent-grade shortcut for the most common
6
+ // case: send a fresh outbound email. It wraps `sending:send-email`
7
+ // with two ergonomic defaults that the underlying operation can't
8
+ // express through manifest-driven flag generation alone:
9
+ //
10
+ // 1. `--from` defaults to `agent@<first-verified-domain>` when
11
+ // omitted. Most agents don't know which domains their org has
12
+ // verified for outbound; making them list-domains first to
13
+ // derive a from-address is exactly the kind of email-ops cruft
14
+ // this command exists to hide. Customers with multiple
15
+ // domains, or who want a different local-part, pass --from
16
+ // explicitly.
17
+ // 2. `--subject` defaults to the first non-empty line of the body
18
+ // (capped). Empty subjects get spam-scored, so we always emit
19
+ // something. Callers who want full control pass --subject.
20
+ //
21
+ // `--body` here is the message body (text). The full `send-email`
22
+ // operation distinguishes `body_text` and `body_html`; this
23
+ // shortcut keeps it simple by exposing `--body` for text and
24
+ // `--html` for the HTML alternative. Users who need both can pass
25
+ // both flags or fall back to `sending:send-email` for the full
26
+ // flag list.
27
+ //
28
+ // Compared to `swaks` (which agents likely have in their training
29
+ // data): this is `swaks`-shaped on purpose so an agent
30
+ // pattern-matching from there lands in the happy path. We just
31
+ // don't need swaks's `--server` / `--auth-*` flags because the
32
+ // HTTPS API key is the auth and the server is implicit.
33
+ const SUBJECT_MAX_LENGTH = 70;
34
+ function deriveSubject(body) {
35
+ for (const line of body.split("\n")) {
36
+ const trimmed = line.trim();
37
+ if (!trimmed)
38
+ continue;
39
+ return trimmed.length > SUBJECT_MAX_LENGTH
40
+ ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...`
41
+ : trimmed;
42
+ }
43
+ return "Message";
44
+ }
45
+ function isVerifiedDomain(domain) {
46
+ return domain.is_active === true;
47
+ }
48
+ async function pickDefaultFromAddress(apiClient) {
49
+ const result = await listDomains({
50
+ client: apiClient.client,
51
+ responseStyle: "fields",
52
+ });
53
+ if (result.error) {
54
+ const errorPayload = extractErrorPayload(result.error);
55
+ throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
56
+ }
57
+ const envelope = result.data;
58
+ const first = envelope?.data?.find(isVerifiedDomain);
59
+ if (!first) {
60
+ throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains:add-domain` and verify it.");
61
+ }
62
+ // Local-part: "agent". Any local-part is accepted on managed
63
+ // *.primitive.email subdomains, so this works out of the box for
64
+ // the auto-issued domain pool. For customers with BYO domains
65
+ // and their own MX, "agent@" may or may not be a routable
66
+ // mailbox; if you have a specific address you want to use, pass
67
+ // --from explicitly.
68
+ return `agent@${first.domain}`;
69
+ }
70
+ class SendCommand extends Command {
71
+ static description = `Send an outbound email. Agent-grade shortcut for sending:send-email with sensible defaults.
72
+
73
+ --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
74
+ --subject defaults to the first line of the body when omitted.
75
+
76
+ For the full flag set (custom message-id threading on the wire,
77
+ references arrays, etc.), use \`primitive sending:send-email\`.`;
78
+ static summary = "Send an email (simplified, agent-friendly)";
79
+ static examples = [
80
+ "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
81
+ "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
82
+ "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
83
+ "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
84
+ ];
85
+ static flags = {
86
+ "api-key": Flags.string({
87
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
88
+ env: "PRIMITIVE_API_KEY",
89
+ }),
90
+ "base-url": Flags.string({
91
+ description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
92
+ env: "PRIMITIVE_API_URL",
93
+ }),
94
+ to: Flags.string({
95
+ description: "Recipient address (e.g. alice@example.com).",
96
+ required: true,
97
+ }),
98
+ from: Flags.string({
99
+ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>.",
100
+ }),
101
+ subject: Flags.string({
102
+ description: "Subject line. Defaults to the first line of --body / --html when omitted.",
103
+ }),
104
+ body: Flags.string({
105
+ description: "Plain-text message body. Either --body or --html (or both) is required.",
106
+ }),
107
+ html: Flags.string({
108
+ description: "HTML message body. Either --body or --html (or both) is required.",
109
+ }),
110
+ "in-reply-to": Flags.string({
111
+ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive sending:reply-to-email --id <inbound-id>`.",
112
+ }),
113
+ wait: Flags.boolean({
114
+ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery.",
115
+ }),
116
+ "wait-timeout-ms": Flags.integer({
117
+ description: "Maximum time to wait when --wait is set. Defaults to 30000ms.",
118
+ }),
119
+ };
120
+ async run() {
121
+ const { flags } = await this.parse(SendCommand);
122
+ if (!flags.body && !flags.html) {
123
+ throw new Errors.CLIError("Either --body or --html (or both) is required.");
124
+ }
125
+ const apiClient = new PrimitiveApiClient({
126
+ apiKey: flags["api-key"],
127
+ baseUrl: flags["base-url"],
128
+ });
129
+ const from = flags.from ?? (await pickDefaultFromAddress(apiClient));
130
+ const subject = flags.subject ?? (flags.body ? deriveSubject(flags.body) : "Message");
131
+ const result = await sendEmail({
132
+ body: {
133
+ from,
134
+ to: flags.to,
135
+ subject,
136
+ ...(flags.body !== undefined ? { body_text: flags.body } : {}),
137
+ ...(flags.html !== undefined ? { body_html: flags.html } : {}),
138
+ ...(flags["in-reply-to"] !== undefined
139
+ ? { in_reply_to: flags["in-reply-to"] }
140
+ : {}),
141
+ ...(flags.wait !== undefined ? { wait: flags.wait } : {}),
142
+ ...(flags["wait-timeout-ms"] !== undefined
143
+ ? { wait_timeout_ms: flags["wait-timeout-ms"] }
144
+ : {}),
145
+ },
146
+ client: apiClient.client,
147
+ responseStyle: "fields",
148
+ });
149
+ if (result.error) {
150
+ const errorPayload = extractErrorPayload(result.error);
151
+ process.stderr.write(`${formatErrorPayload(errorPayload)}\n`);
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+ const envelope = result.data;
156
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
157
+ }
158
+ }
159
+ export default SendCommand;
@@ -1,6 +1,7 @@
1
1
  import { Args, Command } from "@oclif/core";
2
2
  import { operationManifest, } from "../openapi/index.js";
3
3
  import { createOperationCommand } from "./api-command.js";
4
+ import SendCommand from "./commands/send.js";
4
5
  import { renderFishCompletion } from "./fish-completion.js";
5
6
  class ListOperationsCommand extends Command {
6
7
  static description = "List all generated API operations";
@@ -38,5 +39,10 @@ const generatedCommands = Object.fromEntries(operationManifest.map((operation) =
38
39
  export const COMMANDS = {
39
40
  completion: CompletionCommand,
40
41
  "list-operations": ListOperationsCommand,
42
+ // `send` is the agent-grade shortcut for sending:send-email with
43
+ // sensible defaults (auto from-address, auto subject). The full
44
+ // operation stays available under sending:send-email for callers
45
+ // who want every flag.
46
+ send: SendCommand,
41
47
  ...generatedCommands,
42
48
  };
@@ -42,6 +42,100 @@
42
42
  "summary": "List all generated API operations",
43
43
  "enableJsonFlag": false
44
44
  },
45
+ "send": {
46
+ "aliases": [],
47
+ "args": {},
48
+ "description": "Send an outbound email. Agent-grade shortcut for sending:send-email with sensible defaults.\n\n --from defaults to agent@<your-first-verified-outbound-domain> when omitted.\n --subject defaults to the first line of the body when omitted.\n\n For the full flag set (custom message-id threading on the wire,\n references arrays, etc.), use `primitive sending:send-email`.",
49
+ "examples": [
50
+ "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
51
+ "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
52
+ "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
53
+ "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait"
54
+ ],
55
+ "flags": {
56
+ "api-key": {
57
+ "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
58
+ "env": "PRIMITIVE_API_KEY",
59
+ "name": "api-key",
60
+ "hasDynamicHelp": false,
61
+ "multiple": false,
62
+ "type": "option"
63
+ },
64
+ "base-url": {
65
+ "description": "API base URL (defaults to PRIMITIVE_API_URL or production)",
66
+ "env": "PRIMITIVE_API_URL",
67
+ "name": "base-url",
68
+ "hasDynamicHelp": false,
69
+ "multiple": false,
70
+ "type": "option"
71
+ },
72
+ "to": {
73
+ "description": "Recipient address (e.g. alice@example.com).",
74
+ "name": "to",
75
+ "required": true,
76
+ "hasDynamicHelp": false,
77
+ "multiple": false,
78
+ "type": "option"
79
+ },
80
+ "from": {
81
+ "description": "Sender address. Defaults to agent@<your-first-verified-outbound-domain>.",
82
+ "name": "from",
83
+ "hasDynamicHelp": false,
84
+ "multiple": false,
85
+ "type": "option"
86
+ },
87
+ "subject": {
88
+ "description": "Subject line. Defaults to the first line of --body / --html when omitted.",
89
+ "name": "subject",
90
+ "hasDynamicHelp": false,
91
+ "multiple": false,
92
+ "type": "option"
93
+ },
94
+ "body": {
95
+ "description": "Plain-text message body. Either --body or --html (or both) is required.",
96
+ "name": "body",
97
+ "hasDynamicHelp": false,
98
+ "multiple": false,
99
+ "type": "option"
100
+ },
101
+ "html": {
102
+ "description": "HTML message body. Either --body or --html (or both) is required.",
103
+ "name": "html",
104
+ "hasDynamicHelp": false,
105
+ "multiple": false,
106
+ "type": "option"
107
+ },
108
+ "in-reply-to": {
109
+ "description": "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive sending:reply-to-email --id <inbound-id>`.",
110
+ "name": "in-reply-to",
111
+ "hasDynamicHelp": false,
112
+ "multiple": false,
113
+ "type": "option"
114
+ },
115
+ "wait": {
116
+ "description": "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery.",
117
+ "name": "wait",
118
+ "allowNo": false,
119
+ "type": "boolean"
120
+ },
121
+ "wait-timeout-ms": {
122
+ "description": "Maximum time to wait when --wait is set. Defaults to 30000ms.",
123
+ "name": "wait-timeout-ms",
124
+ "hasDynamicHelp": false,
125
+ "multiple": false,
126
+ "type": "option"
127
+ }
128
+ },
129
+ "hasDynamicHelp": false,
130
+ "hiddenAliases": [],
131
+ "id": "send",
132
+ "pluginAlias": "@primitivedotdev/sdk",
133
+ "pluginName": "@primitivedotdev/sdk",
134
+ "pluginType": "core",
135
+ "strict": true,
136
+ "summary": "Send an email (simplified, agent-friendly)",
137
+ "enableJsonFlag": false
138
+ },
45
139
  "account:get-account": {
46
140
  "aliases": [],
47
141
  "args": {},
@@ -173,7 +267,7 @@
173
267
  "account:update-account": {
174
268
  "aliases": [],
175
269
  "args": {},
176
- "description": "PATCH /account\n\nBody fields (JSON --body):\n discard_content_on_webhook_confirmed boolean Whether to discard email content after the webhook endpoint confirms receipt.\n spam_threshold number? Global spam score threshold (0-15). Emails scoring above this are rejected....\n(* = required)",
270
+ "description": "PATCH /account",
177
271
  "flags": {
178
272
  "api-key": {
179
273
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -192,18 +286,31 @@
192
286
  "type": "option"
193
287
  },
194
288
  "body": {
195
- "description": "JSON request body",
289
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
196
290
  "name": "body",
197
291
  "hasDynamicHelp": false,
198
292
  "multiple": false,
199
293
  "type": "option"
200
294
  },
201
295
  "body-file": {
202
- "description": "Path to a JSON file used as the request body",
296
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
203
297
  "name": "body-file",
204
298
  "hasDynamicHelp": false,
205
299
  "multiple": false,
206
300
  "type": "option"
301
+ },
302
+ "discard-content-on-webhook-confirmed": {
303
+ "description": "Whether to discard email content after the webhook endpoint confirms receipt.",
304
+ "name": "discard-content-on-webhook-confirmed",
305
+ "allowNo": false,
306
+ "type": "boolean"
307
+ },
308
+ "spam-threshold": {
309
+ "description": "Global spam score threshold (0-15). Emails scoring above this are rejected. S...",
310
+ "name": "spam-threshold",
311
+ "hasDynamicHelp": false,
312
+ "multiple": false,
313
+ "type": "option"
207
314
  }
208
315
  },
209
316
  "hasDynamicHelp": false,
@@ -219,7 +326,7 @@
219
326
  "domains:add-domain": {
220
327
  "aliases": [],
221
328
  "args": {},
222
- "description": "Creates an unverified domain claim. You will receive a\n`verification_token` to add as a DNS TXT record before\ncalling the verify endpoint.\n\n\nBody fields (JSON --body):\n * domain string The domain name to claim (e.g. \"example.com\")\n(* = required)",
329
+ "description": "Creates an unverified domain claim. You will receive a\n`verification_token` to add as a DNS TXT record before\ncalling the verify endpoint.\n",
223
330
  "flags": {
224
331
  "api-key": {
225
332
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -238,18 +345,25 @@
238
345
  "type": "option"
239
346
  },
240
347
  "body": {
241
- "description": "JSON request body",
348
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
242
349
  "name": "body",
243
350
  "hasDynamicHelp": false,
244
351
  "multiple": false,
245
352
  "type": "option"
246
353
  },
247
354
  "body-file": {
248
- "description": "Path to a JSON file used as the request body",
355
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
249
356
  "name": "body-file",
250
357
  "hasDynamicHelp": false,
251
358
  "multiple": false,
252
359
  "type": "option"
360
+ },
361
+ "domain": {
362
+ "description": "The domain name to claim (e.g. \"example.com\")",
363
+ "name": "domain",
364
+ "hasDynamicHelp": false,
365
+ "multiple": false,
366
+ "type": "option"
253
367
  }
254
368
  },
255
369
  "hasDynamicHelp": false,
@@ -337,7 +451,7 @@
337
451
  "domains:update-domain": {
338
452
  "aliases": [],
339
453
  "args": {},
340
- "description": "Update a verified domain's settings. Only verified domains can be\nupdated. Per-domain spam thresholds require a Pro plan.\n\n\nBody fields (JSON --body):\n is_active boolean Whether the domain accepts incoming emails\n spam_threshold number? Per-domain spam threshold override (Pro plan required)\n(* = required)",
454
+ "description": "Update a verified domain's settings. Only verified domains can be\nupdated. Per-domain spam thresholds require a Pro plan.\n",
341
455
  "flags": {
342
456
  "api-key": {
343
457
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -364,18 +478,31 @@
364
478
  "type": "option"
365
479
  },
366
480
  "body": {
367
- "description": "JSON request body",
481
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
368
482
  "name": "body",
369
483
  "hasDynamicHelp": false,
370
484
  "multiple": false,
371
485
  "type": "option"
372
486
  },
373
487
  "body-file": {
374
- "description": "Path to a JSON file used as the request body",
488
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
375
489
  "name": "body-file",
376
490
  "hasDynamicHelp": false,
377
491
  "multiple": false,
378
492
  "type": "option"
493
+ },
494
+ "is-active": {
495
+ "description": "Whether the domain accepts incoming emails",
496
+ "name": "is-active",
497
+ "allowNo": false,
498
+ "type": "boolean"
499
+ },
500
+ "spam-threshold": {
501
+ "description": "Per-domain spam threshold override (Pro plan required)",
502
+ "name": "spam-threshold",
503
+ "hasDynamicHelp": false,
504
+ "multiple": false,
505
+ "type": "option"
379
506
  }
380
507
  },
381
508
  "hasDynamicHelp": false,
@@ -755,7 +882,7 @@
755
882
  "endpoints:create-endpoint": {
756
883
  "aliases": [],
757
884
  "args": {},
758
- "description": "Creates a new webhook endpoint. If a deactivated endpoint with the\nsame URL and domain exists, it is reactivated instead.\nSubject to plan limits on the number of active endpoints.\n\n\nBody fields (JSON --body):\n * url string The webhook URL to deliver events to\n domain_id string? Restrict to emails from a specific domain\n enabled boolean Whether the endpoint is active\n rules object Endpoint-specific filtering rules\n(* = required)",
885
+ "description": "Creates a new webhook endpoint. If a deactivated endpoint with the\nsame URL and domain exists, it is reactivated instead.\nSubject to plan limits on the number of active endpoints.\n\n\nBody fields requiring --body JSON (these are not exposed as flags):\n rules object Endpoint-specific filtering rules\n(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)",
759
886
  "flags": {
760
887
  "api-key": {
761
888
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -774,18 +901,38 @@
774
901
  "type": "option"
775
902
  },
776
903
  "body": {
777
- "description": "JSON request body",
904
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
778
905
  "name": "body",
779
906
  "hasDynamicHelp": false,
780
907
  "multiple": false,
781
908
  "type": "option"
782
909
  },
783
910
  "body-file": {
784
- "description": "Path to a JSON file used as the request body",
911
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
785
912
  "name": "body-file",
786
913
  "hasDynamicHelp": false,
787
914
  "multiple": false,
788
915
  "type": "option"
916
+ },
917
+ "url": {
918
+ "description": "The webhook URL to deliver events to",
919
+ "name": "url",
920
+ "hasDynamicHelp": false,
921
+ "multiple": false,
922
+ "type": "option"
923
+ },
924
+ "domain-id": {
925
+ "description": "Restrict to emails from a specific domain",
926
+ "name": "domain-id",
927
+ "hasDynamicHelp": false,
928
+ "multiple": false,
929
+ "type": "option"
930
+ },
931
+ "enabled": {
932
+ "description": "Whether the endpoint is active",
933
+ "name": "enabled",
934
+ "allowNo": false,
935
+ "type": "boolean"
789
936
  }
790
937
  },
791
938
  "hasDynamicHelp": false,
@@ -913,7 +1060,7 @@
913
1060
  "endpoints:update-endpoint": {
914
1061
  "aliases": [],
915
1062
  "args": {},
916
- "description": "Updates an active webhook endpoint. If the URL is changed, the old\nendpoint is deactivated and a new one is created (or an existing\ndeactivated endpoint with the new URL is reactivated).\n\n\nBody fields (JSON --body):\n domain_id string?\n enabled boolean\n rules object\n url string New webhook URL (triggers endpoint rotation)\n(* = required)",
1063
+ "description": "Updates an active webhook endpoint. If the URL is changed, the old\nendpoint is deactivated and a new one is created (or an existing\ndeactivated endpoint with the new URL is reactivated).\n\n\nBody fields requiring --body JSON (these are not exposed as flags):\n rules object\n(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)",
917
1064
  "flags": {
918
1065
  "api-key": {
919
1066
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -940,18 +1087,38 @@
940
1087
  "type": "option"
941
1088
  },
942
1089
  "body": {
943
- "description": "JSON request body",
1090
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
944
1091
  "name": "body",
945
1092
  "hasDynamicHelp": false,
946
1093
  "multiple": false,
947
1094
  "type": "option"
948
1095
  },
949
1096
  "body-file": {
950
- "description": "Path to a JSON file used as the request body",
1097
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
951
1098
  "name": "body-file",
952
1099
  "hasDynamicHelp": false,
953
1100
  "multiple": false,
954
1101
  "type": "option"
1102
+ },
1103
+ "domain-id": {
1104
+ "description": "domain_id",
1105
+ "name": "domain-id",
1106
+ "hasDynamicHelp": false,
1107
+ "multiple": false,
1108
+ "type": "option"
1109
+ },
1110
+ "enabled": {
1111
+ "description": "enabled",
1112
+ "name": "enabled",
1113
+ "allowNo": false,
1114
+ "type": "boolean"
1115
+ },
1116
+ "url": {
1117
+ "description": "New webhook URL (triggers endpoint rotation)",
1118
+ "name": "url",
1119
+ "hasDynamicHelp": false,
1120
+ "multiple": false,
1121
+ "type": "option"
955
1122
  }
956
1123
  },
957
1124
  "hasDynamicHelp": false,
@@ -967,7 +1134,7 @@
967
1134
  "filters:create-filter": {
968
1135
  "aliases": [],
969
1136
  "args": {},
970
- "description": "Creates a new whitelist or blocklist filter. Per-domain filters\nrequire a Pro plan. Patterns are stored as lowercase.\n\n\nBody fields (JSON --body):\n * pattern string Email address or pattern to filter\n * type string\n domain_id string? Restrict filter to a specific domain (Pro plan required)\n(* = required)",
1137
+ "description": "Creates a new whitelist or blocklist filter. Per-domain filters\nrequire a Pro plan. Patterns are stored as lowercase.\n",
971
1138
  "flags": {
972
1139
  "api-key": {
973
1140
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -986,18 +1153,43 @@
986
1153
  "type": "option"
987
1154
  },
988
1155
  "body": {
989
- "description": "JSON request body",
1156
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
990
1157
  "name": "body",
991
1158
  "hasDynamicHelp": false,
992
1159
  "multiple": false,
993
1160
  "type": "option"
994
1161
  },
995
1162
  "body-file": {
996
- "description": "Path to a JSON file used as the request body",
1163
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
997
1164
  "name": "body-file",
998
1165
  "hasDynamicHelp": false,
999
1166
  "multiple": false,
1000
1167
  "type": "option"
1168
+ },
1169
+ "pattern": {
1170
+ "description": "Email address or pattern to filter",
1171
+ "name": "pattern",
1172
+ "hasDynamicHelp": false,
1173
+ "multiple": false,
1174
+ "type": "option"
1175
+ },
1176
+ "type": {
1177
+ "description": "type",
1178
+ "name": "type",
1179
+ "hasDynamicHelp": false,
1180
+ "multiple": false,
1181
+ "options": [
1182
+ "whitelist",
1183
+ "blocklist"
1184
+ ],
1185
+ "type": "option"
1186
+ },
1187
+ "domain-id": {
1188
+ "description": "Restrict filter to a specific domain (Pro plan required)",
1189
+ "name": "domain-id",
1190
+ "hasDynamicHelp": false,
1191
+ "multiple": false,
1192
+ "type": "option"
1001
1193
  }
1002
1194
  },
1003
1195
  "hasDynamicHelp": false,
@@ -1085,7 +1277,7 @@
1085
1277
  "filters:update-filter": {
1086
1278
  "aliases": [],
1087
1279
  "args": {},
1088
- "description": "Toggle a filter's enabled state.\n\nBody fields (JSON --body):\n * enabled boolean\n(* = required)",
1280
+ "description": "Toggle a filter's enabled state.",
1089
1281
  "flags": {
1090
1282
  "api-key": {
1091
1283
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1112,18 +1304,24 @@
1112
1304
  "type": "option"
1113
1305
  },
1114
1306
  "body": {
1115
- "description": "JSON request body",
1307
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
1116
1308
  "name": "body",
1117
1309
  "hasDynamicHelp": false,
1118
1310
  "multiple": false,
1119
1311
  "type": "option"
1120
1312
  },
1121
1313
  "body-file": {
1122
- "description": "Path to a JSON file used as the request body",
1314
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
1123
1315
  "name": "body-file",
1124
1316
  "hasDynamicHelp": false,
1125
1317
  "multiple": false,
1126
1318
  "type": "option"
1319
+ },
1320
+ "enabled": {
1321
+ "description": "enabled",
1322
+ "name": "enabled",
1323
+ "allowNo": false,
1324
+ "type": "boolean"
1127
1325
  }
1128
1326
  },
1129
1327
  "hasDynamicHelp": false,
@@ -1139,7 +1337,7 @@
1139
1337
  "sending:reply-to-email": {
1140
1338
  "aliases": [],
1141
1339
  "args": {},
1142
- "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional `wait` flag; passing any header or recipient\noverride is rejected by the schema (`additionalProperties:\nfalse`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n\n\nBody fields (JSON --body):\n body_html string HTML reply body. At least one of body_text or body_html is required.\n body_text string Plain-text reply body. At least one of body_text or body_html is required. ...\n from string Optional override for the reply's From header. Defaults to\n wait boolean When true, wait for the first downstream SMTP delivery outcome before retur...\n(* = required)",
1340
+ "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional `wait` flag; passing any header or recipient\noverride is rejected by the schema (`additionalProperties:\nfalse`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
1143
1341
  "flags": {
1144
1342
  "api-key": {
1145
1343
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1166,18 +1364,45 @@
1166
1364
  "type": "option"
1167
1365
  },
1168
1366
  "body": {
1169
- "description": "JSON request body",
1367
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
1170
1368
  "name": "body",
1171
1369
  "hasDynamicHelp": false,
1172
1370
  "multiple": false,
1173
1371
  "type": "option"
1174
1372
  },
1175
1373
  "body-file": {
1176
- "description": "Path to a JSON file used as the request body",
1374
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
1177
1375
  "name": "body-file",
1178
1376
  "hasDynamicHelp": false,
1179
1377
  "multiple": false,
1180
1378
  "type": "option"
1379
+ },
1380
+ "body-html": {
1381
+ "description": "HTML reply body. At least one of body_text or body_html is required.",
1382
+ "name": "body-html",
1383
+ "hasDynamicHelp": false,
1384
+ "multiple": false,
1385
+ "type": "option"
1386
+ },
1387
+ "body-text": {
1388
+ "description": "Plain-text reply body. At least one of body_text or body_html is required. Th...",
1389
+ "name": "body-text",
1390
+ "hasDynamicHelp": false,
1391
+ "multiple": false,
1392
+ "type": "option"
1393
+ },
1394
+ "from": {
1395
+ "description": "Optional override for the reply's From header. Defaults to",
1396
+ "name": "from",
1397
+ "hasDynamicHelp": false,
1398
+ "multiple": false,
1399
+ "type": "option"
1400
+ },
1401
+ "wait": {
1402
+ "description": "When true, wait for the first downstream SMTP delivery outcome before returni...",
1403
+ "name": "wait",
1404
+ "allowNo": false,
1405
+ "type": "boolean"
1181
1406
  }
1182
1407
  },
1183
1408
  "hasDynamicHelp": false,
@@ -1193,7 +1418,7 @@
1193
1418
  "sending:send-email": {
1194
1419
  "aliases": [],
1195
1420
  "args": {},
1196
- "description": "Sends an outbound email through Primitive's outbound relay. By default\nthe request returns once the relay accepts the message for delivery.\nSet `wait: true` to wait for the first downstream SMTP delivery outcome.\n\n\nBody fields (JSON --body):\n * from string RFC 5322 From header. The sender domain must be a verified outbound domain ...\n * subject string Subject line for the outbound message\n * to string Recipient address. Recipient eligibility depends on your account's outbound...\n body_html string HTML message body. At least one of body_text or body_html is required. The ...\n body_text string Plain-text message body. At least one of body_text or body_html is required...\n in_reply_to string Message-ID of the direct parent email when sending a threaded reply.\n references array<string> Full ordered message-id chain for the thread.\n wait boolean When true, wait for the first downstream SMTP delivery outcome before retur...\n wait_timeout_ms integer Maximum time to wait for a delivery outcome when wait is true. Defaults to ...\n(* = required)",
1421
+ "description": "Sends an outbound email through Primitive's outbound relay. By default\nthe request returns once the relay accepts the message for delivery.\nSet `wait: true` to wait for the first downstream SMTP delivery outcome.\n\n\nBody fields requiring --body JSON (these are not exposed as flags):\n references array<string> Full ordered message-id chain for the thread.\n(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)",
1197
1422
  "flags": {
1198
1423
  "api-key": {
1199
1424
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1212,18 +1437,73 @@
1212
1437
  "type": "option"
1213
1438
  },
1214
1439
  "body": {
1215
- "description": "JSON request body",
1440
+ "description": "Full request body as JSON. Prefer per-field flags (e.g. --to, --from, --body-text) when available; --body is the escape hatch for nested or complex fields.",
1216
1441
  "name": "body",
1217
1442
  "hasDynamicHelp": false,
1218
1443
  "multiple": false,
1219
1444
  "type": "option"
1220
1445
  },
1221
1446
  "body-file": {
1222
- "description": "Path to a JSON file used as the request body",
1447
+ "description": "Path to a JSON file used as the request body. Same role as --body for callers passing a saved payload.",
1223
1448
  "name": "body-file",
1224
1449
  "hasDynamicHelp": false,
1225
1450
  "multiple": false,
1226
1451
  "type": "option"
1452
+ },
1453
+ "from": {
1454
+ "description": "RFC 5322 From header. The sender domain must be a verified outbound domain fo...",
1455
+ "name": "from",
1456
+ "hasDynamicHelp": false,
1457
+ "multiple": false,
1458
+ "type": "option"
1459
+ },
1460
+ "subject": {
1461
+ "description": "Subject line for the outbound message",
1462
+ "name": "subject",
1463
+ "hasDynamicHelp": false,
1464
+ "multiple": false,
1465
+ "type": "option"
1466
+ },
1467
+ "to": {
1468
+ "description": "Recipient address. Recipient eligibility depends on your account's outbound e...",
1469
+ "name": "to",
1470
+ "hasDynamicHelp": false,
1471
+ "multiple": false,
1472
+ "type": "option"
1473
+ },
1474
+ "body-html": {
1475
+ "description": "HTML message body. At least one of body_text or body_html is required. The co...",
1476
+ "name": "body-html",
1477
+ "hasDynamicHelp": false,
1478
+ "multiple": false,
1479
+ "type": "option"
1480
+ },
1481
+ "body-text": {
1482
+ "description": "Plain-text message body. At least one of body_text or body_html is required. ...",
1483
+ "name": "body-text",
1484
+ "hasDynamicHelp": false,
1485
+ "multiple": false,
1486
+ "type": "option"
1487
+ },
1488
+ "in-reply-to": {
1489
+ "description": "Message-ID of the direct parent email when sending a threaded reply.",
1490
+ "name": "in-reply-to",
1491
+ "hasDynamicHelp": false,
1492
+ "multiple": false,
1493
+ "type": "option"
1494
+ },
1495
+ "wait": {
1496
+ "description": "When true, wait for the first downstream SMTP delivery outcome before returning.",
1497
+ "name": "wait",
1498
+ "allowNo": false,
1499
+ "type": "boolean"
1500
+ },
1501
+ "wait-timeout-ms": {
1502
+ "description": "Maximum time to wait for a delivery outcome when wait is true. Defaults to 30...",
1503
+ "name": "wait-timeout-ms",
1504
+ "hasDynamicHelp": false,
1505
+ "multiple": false,
1506
+ "type": "option"
1227
1507
  }
1228
1508
  },
1229
1509
  "hasDynamicHelp": false,
@@ -1363,5 +1643,5 @@
1363
1643
  "enableJsonFlag": false
1364
1644
  }
1365
1645
  },
1366
- "version": "0.10.0"
1646
+ "version": "0.11.0"
1367
1647
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Official Primitive Node.js SDK — webhook, api, openapi, contract, and parser modules",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",