@primitivedotdev/sdk 0.10.0 → 0.12.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 `--raw-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; --raw-body for
106
+ * the leftovers below."
107
+ */
108
+ function renderRequestSchemaSummary(schema) {
109
+ const fields = extractBodyFields(schema);
110
+ if (fields.length === 0)
111
+ return null;
112
+ const complex = fields.filter((f) => f.kind === "complex");
113
+ if (complex.length === 0)
70
114
  return null;
71
- const nameWidth = Math.min(24, Math.max(...entries.map((e) => e.name.length)));
72
- const lines = ["Body fields (JSON --body):"];
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 --raw-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) {
@@ -134,9 +180,9 @@ function parseJson(source, flagLabel) {
134
180
  }
135
181
  export function readJsonBody(flags) {
136
182
  const bodyFile = flags["body-file"];
137
- const body = flags.body;
138
- if (bodyFile && body) {
139
- throw cliError("Use either --body or --body-file, not both");
183
+ const rawBody = flags["raw-body"];
184
+ if (bodyFile && rawBody) {
185
+ throw cliError("Use either --raw-body or --body-file, not both");
140
186
  }
141
187
  if (typeof bodyFile === "string") {
142
188
  let contents;
@@ -149,8 +195,8 @@ export function readJsonBody(flags) {
149
195
  }
150
196
  return parseJson(contents, `--body-file ${bodyFile}`);
151
197
  }
152
- if (typeof body === "string") {
153
- return parseJson(body, "--body");
198
+ if (typeof rawBody === "string") {
199
+ return parseJson(rawBody, "--raw-body");
154
200
  }
155
201
  return undefined;
156
202
  }
@@ -196,6 +242,56 @@ 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
+ // `--raw-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
+ //
250
+ // Note: `--body` is intentionally NOT reserved here. The naive
251
+ // agent expectation (per AGX walkthrough) is that --body means
252
+ // "the message body content," which collides with the JSON
253
+ // escape-hatch meaning we used pre-0.12. The escape hatch is now
254
+ // `--raw-body`; --body is free to be claimed by per-field flag
255
+ // expansion as the kebab-cased version of a `body` schema field
256
+ // (e.g. on a future `body: { ... }` schema). For send-mail today,
257
+ // the body-text field is `body_text` -> `--body-text`, and there
258
+ // is no top-level `body` field, so --body remains unclaimed at
259
+ // the generated-command level. The agent shortcut `primitive
260
+ // send` defines its own --body for the message text.
261
+ const RESERVED_FLAG_NAMES = new Set([
262
+ "api-key",
263
+ "base-url",
264
+ "raw-body",
265
+ "body-file",
266
+ "output",
267
+ ]);
268
+ function bodyFieldFlag(field) {
269
+ // Flag descriptions cap at 80 chars so oclif's --help output
270
+ // stays readable; the schema's full description is also visible
271
+ // via `primitive list-operations | jq`.
272
+ const descMax = 80;
273
+ const trimmedDesc = field.description.length > descMax
274
+ ? `${field.description.slice(0, descMax - 3)}...`
275
+ : field.description;
276
+ // Field-flag UX choice: do NOT mark scalar body fields as
277
+ // required at the oclif level even when the JSON Schema marks
278
+ // them required. Reason: a caller can satisfy the requirement
279
+ // either via the individual flag OR via --body / --body-file.
280
+ // Marking the flag required would force the individual-flag
281
+ // form. The runtime body merger validates the final assembled
282
+ // body against the same server-side schema either way.
283
+ const common = {
284
+ description: trimmedDesc || field.name,
285
+ };
286
+ if (field.kind === "boolean")
287
+ return Flags.boolean(common);
288
+ if (field.kind === "integer")
289
+ return Flags.integer(common);
290
+ if (field.enumValues) {
291
+ return Flags.string({ ...common, options: field.enumValues });
292
+ }
293
+ return Flags.string(common);
294
+ }
199
295
  function buildFlags(operation) {
200
296
  const flags = {
201
297
  "api-key": Flags.string({
@@ -210,18 +306,67 @@ function buildFlags(operation) {
210
306
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) {
211
307
  flags[flagName(parameter.name)] = flagForParameter(parameter);
212
308
  }
309
+ const bodyFieldFlagToProperty = new Map();
213
310
  if (operation.hasJsonBody) {
214
- flags.body = Flags.string({ description: "JSON request body" });
311
+ flags["raw-body"] = Flags.string({
312
+ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
313
+ });
215
314
  flags["body-file"] = Flags.string({
216
- description: "Path to a JSON file used as the request body",
315
+ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
217
316
  });
317
+ // Expand top-level scalar body fields into individual flags so
318
+ // `primitive sending:send-email --to alice@x --from support@x
319
+ // --body-text "hi"` works without constructing JSON. Driven by
320
+ // the requestSchema embedded on the manifest. Skip flags that
321
+ // collide with reserved names or with path/query params already
322
+ // added above; those collisions fall back to --body.
323
+ //
324
+ // Collisions are tracked in the returned map so the run()
325
+ // handler doesn't misread a path/query param's value as a
326
+ // body-field override. (A naive "look up parsedFlags[name]"
327
+ // pass would happily pick up the path param's value and
328
+ // silently write it into the body.)
329
+ const bodyFields = extractBodyFields(operation.requestSchema);
330
+ for (const field of bodyFields) {
331
+ if (field.kind === "complex")
332
+ continue;
333
+ const name = flagName(field.name);
334
+ if (RESERVED_FLAG_NAMES.has(name))
335
+ continue;
336
+ if (flags[name] !== undefined)
337
+ continue;
338
+ flags[name] = bodyFieldFlag(field);
339
+ bodyFieldFlagToProperty.set(name, field.name);
340
+ }
218
341
  }
219
342
  if (operation.binaryResponse) {
220
343
  flags.output = Flags.string({
221
344
  description: "Write binary response bytes to a file",
222
345
  });
223
346
  }
224
- return flags;
347
+ return { flags, bodyFieldFlagToProperty };
348
+ }
349
+ // Pull body field values out of the parsed CLI flags. Returns
350
+ // only fields the user actually supplied (omits undefined). Used
351
+ // to override / extend the JSON --body when both forms are
352
+ // present (per-field flags take precedence on key conflicts).
353
+ //
354
+ // The `bodyFieldFlagToProperty` allowlist comes from buildFlags and
355
+ // records ONLY the flags actually registered as body-field flags.
356
+ // Without it, this function would naively read parsedFlags by
357
+ // kebab-cased field name and pick up values from a colliding path
358
+ // or query param flag, silently writing them into the body under
359
+ // the body-field key. The allowlist keeps the merge honest: only
360
+ // flags this CLI generator owns end up in the body.
361
+ function collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty) {
362
+ const result = {};
363
+ for (const [flag, property] of bodyFieldFlagToProperty) {
364
+ const value = parsedFlags[flag];
365
+ if (value === undefined)
366
+ continue;
367
+ result[property] = value;
368
+ }
369
+ return result;
225
370
  }
226
371
  function collectValues(parameters, flags) {
227
372
  const values = {};
@@ -234,7 +379,7 @@ function collectValues(parameters, flags) {
234
379
  return values;
235
380
  }
236
381
  export function createOperationCommand(operation) {
237
- const flags = buildFlags(operation);
382
+ const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
238
383
  // Append a "Body fields" summary to the description so agents
239
384
  // running `<command> --help` learn the JSON shape immediately.
240
385
  // Without this, `--help` only said "JSON request body" and agents
@@ -262,11 +407,48 @@ export function createOperationCommand(operation) {
262
407
  ? parsedFlags["base-url"]
263
408
  : undefined,
264
409
  });
265
- const body = operation.hasJsonBody
266
- ? readJsonBody(parsedFlags)
267
- : undefined;
410
+ // Two body sources, merged: explicit JSON via --body /
411
+ // --body-file (the base) plus per-field flags (the
412
+ // overrides). Per-field flag values take precedence on key
413
+ // conflicts so a caller can pass a base payload via --body
414
+ // and override one field on the command line.
415
+ let body;
416
+ if (operation.hasJsonBody) {
417
+ const explicit = readJsonBody(parsedFlags);
418
+ const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
419
+ if (Object.keys(overrides).length > 0) {
420
+ if (explicit === undefined) {
421
+ body = overrides;
422
+ }
423
+ else if (explicit !== null &&
424
+ typeof explicit === "object" &&
425
+ !Array.isArray(explicit)) {
426
+ body = { ...explicit, ...overrides };
427
+ }
428
+ else {
429
+ // Caller passed --raw-body as null, an array, or a
430
+ // primitive AND also passed per-field flags. We can't
431
+ // merge per-field overrides into a non-object body
432
+ // shape, and silently dropping either source would
433
+ // leave the caller's actual intent unclear. Refuse
434
+ // loudly so the next attempt is unambiguous.
435
+ const explicitKind = explicit === null
436
+ ? "null"
437
+ : Array.isArray(explicit)
438
+ ? "array"
439
+ : typeof explicit;
440
+ const overrideFlags = Object.keys(overrides)
441
+ .map((p) => `--${flagName(p)}`)
442
+ .join(", ");
443
+ throw new Errors.CLIError(`--raw-body must be a JSON object when also passing per-field flags (got ${explicitKind}); supplied per-field flags: ${overrideFlags}. Either drop --raw-body and rely on the per-field flags, or move every field into the JSON --raw-body and drop the flags.`);
444
+ }
445
+ }
446
+ else {
447
+ body = explicit;
448
+ }
449
+ }
268
450
  if (operation.bodyRequired && body === undefined) {
269
- throw new Errors.CLIError(`Operation ${operation.operationId} requires --body or --body-file`);
451
+ throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --raw-body / --body-file.`);
270
452
  }
271
453
  const operationFn = operations[operation.sdkName];
272
454
  const result = await operationFn({
@@ -299,8 +481,34 @@ export function createOperationCommand(operation) {
299
481
  if (cursor) {
300
482
  process.stderr.write(`next cursor: ${cursor}\n`);
301
483
  }
484
+ // Empty-result hint. When a list-style operation returns
485
+ // an empty array, emit an operation-specific note to
486
+ // stderr so a naive caller can distinguish "nothing here"
487
+ // from "something isn't set up." Stdout still gets the
488
+ // raw `[]` so machine-readable output is unchanged. The
489
+ // AGX walkthrough flagged this: `list-deliveries` returning
490
+ // `[]` left the agent unsure whether they had an empty
491
+ // delivery log or no endpoints configured at all.
492
+ if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
493
+ const hint = EMPTY_RESULT_HINTS[operation.sdkName];
494
+ if (hint)
495
+ process.stderr.write(`${hint}\n`);
496
+ }
302
497
  this.log(JSON.stringify(envelope?.data ?? null, null, 2));
303
498
  }
304
499
  }
305
500
  return OperationCommand;
306
501
  }
502
+ // Empty-state hints for list-style operations whose empty result
503
+ // would otherwise leave the caller wondering "is this empty
504
+ // because there's nothing to list, or because something earlier
505
+ // in the setup chain isn't done?" Keys are the manifest's
506
+ // `sdkName` for the operation. Operations without an entry fall
507
+ // back to no hint (silent empty array, same as before).
508
+ const EMPTY_RESULT_HINTS = {
509
+ listDeliveries: "(no results) Often means no webhook endpoints are configured to receive deliveries. Run `primitive endpoints:list-endpoints` to check.",
510
+ listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints:create-endpoint --url <your-url>`.",
511
+ listEmails: "(no results) No inbound emails received yet on this account.",
512
+ listDomains: "(no results) No domains on this account. Add one with `primitive domains:add-domain --domain <yourdomain.example>`.",
513
+ listFilters: "(no results) No filter rules configured.",
514
+ };
@@ -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;
@@ -0,0 +1,68 @@
1
+ import { Command, Errors, Flags } from "@oclif/core";
2
+ import { getAccount } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, formatErrorPayload } from "../api-command.js";
5
+ // `primitive whoami` is the credentials smoke-test the AGX
6
+ // walkthrough kept asking for. Before this command, a user with a
7
+ // suspect API key had no fast way to verify "is my key live and
8
+ // pointed at the org I expect" short of trying any other call and
9
+ // reading a 401. That ambiguity bit two consecutive walkthroughs.
10
+ //
11
+ // Implementation: thin wrapper over /api/v1/account that prints
12
+ // the account email, plan, id, and onboarding status. Any auth
13
+ // problem surfaces as the standard error envelope, same as the
14
+ // generated commands.
15
+ class WhoamiCommand extends Command {
16
+ static description = `Print the account currently authenticated by the API key. Useful as a credentials smoke test: confirms the key is live and shows which account it belongs to.`;
17
+ static summary = "Print the authenticated account (credentials smoke test)";
18
+ static examples = [
19
+ "<%= config.bin %> whoami",
20
+ "<%= config.bin %> whoami --api-key prim_...",
21
+ ];
22
+ static flags = {
23
+ "api-key": Flags.string({
24
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
25
+ env: "PRIMITIVE_API_KEY",
26
+ }),
27
+ "base-url": Flags.string({
28
+ description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
29
+ env: "PRIMITIVE_API_URL",
30
+ }),
31
+ };
32
+ async run() {
33
+ const { flags } = await this.parse(WhoamiCommand);
34
+ const apiClient = new PrimitiveApiClient({
35
+ apiKey: flags["api-key"],
36
+ baseUrl: flags["base-url"],
37
+ });
38
+ const result = await getAccount({
39
+ client: apiClient.client,
40
+ responseStyle: "fields",
41
+ });
42
+ if (result.error) {
43
+ const errorPayload = extractErrorPayload(result.error);
44
+ process.stderr.write(`${formatErrorPayload(errorPayload)}\n`);
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ const envelope = result.data;
49
+ const account = envelope?.data;
50
+ if (!account) {
51
+ process.stderr.write("Server returned an empty account body; this should not happen for a valid key.\n");
52
+ throw new Errors.CLIError("unexpected empty response");
53
+ }
54
+ // Concise human-readable summary on stderr; the full account
55
+ // JSON goes to stdout so a script can pipe it.
56
+ const onboarding = account.onboarding_completed === true
57
+ ? "complete"
58
+ : account.onboarding_step
59
+ ? `in progress (step: ${account.onboarding_step})`
60
+ : "incomplete";
61
+ process.stderr.write(`Authenticated as ${account.email}\n`);
62
+ process.stderr.write(` Account id: ${account.id}\n`);
63
+ process.stderr.write(` Plan: ${account.plan}\n`);
64
+ process.stderr.write(` Onboarding: ${onboarding}\n`);
65
+ this.log(JSON.stringify(account, null, 2));
66
+ }
67
+ }
68
+ export default WhoamiCommand;
@@ -1,6 +1,8 @@
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";
5
+ import WhoamiCommand from "./commands/whoami.js";
4
6
  import { renderFishCompletion } from "./fish-completion.js";
5
7
  class ListOperationsCommand extends Command {
6
8
  static description = "List all generated API operations";
@@ -38,5 +40,15 @@ const generatedCommands = Object.fromEntries(operationManifest.map((operation) =
38
40
  export const COMMANDS = {
39
41
  completion: CompletionCommand,
40
42
  "list-operations": ListOperationsCommand,
43
+ // `send` is the agent-grade shortcut for sending:send-email with
44
+ // sensible defaults (auto from-address, auto subject). The full
45
+ // operation stays available under sending:send-email for callers
46
+ // who want every flag.
47
+ send: SendCommand,
48
+ // `whoami` is the credentials smoke test. Prints the account the
49
+ // current API key authenticates as. AGX walkthroughs kept
50
+ // wanting this before risking a real call against a possibly-
51
+ // bad key.
52
+ whoami: WhoamiCommand,
41
53
  ...generatedCommands,
42
54
  };
@@ -42,6 +42,136 @@
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
+ },
139
+ "whoami": {
140
+ "aliases": [],
141
+ "args": {},
142
+ "description": "Print the account currently authenticated by the API key. Useful as a credentials smoke test: confirms the key is live and shows which account it belongs to.",
143
+ "examples": [
144
+ "<%= config.bin %> whoami",
145
+ "<%= config.bin %> whoami --api-key prim_..."
146
+ ],
147
+ "flags": {
148
+ "api-key": {
149
+ "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
150
+ "env": "PRIMITIVE_API_KEY",
151
+ "name": "api-key",
152
+ "hasDynamicHelp": false,
153
+ "multiple": false,
154
+ "type": "option"
155
+ },
156
+ "base-url": {
157
+ "description": "API base URL (defaults to PRIMITIVE_API_URL or production)",
158
+ "env": "PRIMITIVE_API_URL",
159
+ "name": "base-url",
160
+ "hasDynamicHelp": false,
161
+ "multiple": false,
162
+ "type": "option"
163
+ }
164
+ },
165
+ "hasDynamicHelp": false,
166
+ "hiddenAliases": [],
167
+ "id": "whoami",
168
+ "pluginAlias": "@primitivedotdev/sdk",
169
+ "pluginName": "@primitivedotdev/sdk",
170
+ "pluginType": "core",
171
+ "strict": true,
172
+ "summary": "Print the authenticated account (credentials smoke test)",
173
+ "enableJsonFlag": false
174
+ },
45
175
  "account:get-account": {
46
176
  "aliases": [],
47
177
  "args": {},
@@ -173,7 +303,7 @@
173
303
  "account:update-account": {
174
304
  "aliases": [],
175
305
  "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)",
306
+ "description": "PATCH /account",
177
307
  "flags": {
178
308
  "api-key": {
179
309
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -191,19 +321,32 @@
191
321
  "multiple": false,
192
322
  "type": "option"
193
323
  },
194
- "body": {
195
- "description": "JSON request body",
196
- "name": "body",
324
+ "raw-body": {
325
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
326
+ "name": "raw-body",
197
327
  "hasDynamicHelp": false,
198
328
  "multiple": false,
199
329
  "type": "option"
200
330
  },
201
331
  "body-file": {
202
- "description": "Path to a JSON file used as the request body",
332
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
203
333
  "name": "body-file",
204
334
  "hasDynamicHelp": false,
205
335
  "multiple": false,
206
336
  "type": "option"
337
+ },
338
+ "discard-content-on-webhook-confirmed": {
339
+ "description": "Whether to discard email content after the webhook endpoint confirms receipt.",
340
+ "name": "discard-content-on-webhook-confirmed",
341
+ "allowNo": false,
342
+ "type": "boolean"
343
+ },
344
+ "spam-threshold": {
345
+ "description": "Global spam score threshold (0-15). Emails scoring above this are rejected. S...",
346
+ "name": "spam-threshold",
347
+ "hasDynamicHelp": false,
348
+ "multiple": false,
349
+ "type": "option"
207
350
  }
208
351
  },
209
352
  "hasDynamicHelp": false,
@@ -219,7 +362,7 @@
219
362
  "domains:add-domain": {
220
363
  "aliases": [],
221
364
  "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)",
365
+ "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
366
  "flags": {
224
367
  "api-key": {
225
368
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -237,19 +380,26 @@
237
380
  "multiple": false,
238
381
  "type": "option"
239
382
  },
240
- "body": {
241
- "description": "JSON request body",
242
- "name": "body",
383
+ "raw-body": {
384
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
385
+ "name": "raw-body",
243
386
  "hasDynamicHelp": false,
244
387
  "multiple": false,
245
388
  "type": "option"
246
389
  },
247
390
  "body-file": {
248
- "description": "Path to a JSON file used as the request body",
391
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
249
392
  "name": "body-file",
250
393
  "hasDynamicHelp": false,
251
394
  "multiple": false,
252
395
  "type": "option"
396
+ },
397
+ "domain": {
398
+ "description": "The domain name to claim (e.g. \"example.com\")",
399
+ "name": "domain",
400
+ "hasDynamicHelp": false,
401
+ "multiple": false,
402
+ "type": "option"
253
403
  }
254
404
  },
255
405
  "hasDynamicHelp": false,
@@ -337,7 +487,7 @@
337
487
  "domains:update-domain": {
338
488
  "aliases": [],
339
489
  "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)",
490
+ "description": "Update a verified domain's settings. Only verified domains can be\nupdated. Per-domain spam thresholds require a Pro plan.\n",
341
491
  "flags": {
342
492
  "api-key": {
343
493
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -363,19 +513,32 @@
363
513
  "multiple": false,
364
514
  "type": "option"
365
515
  },
366
- "body": {
367
- "description": "JSON request body",
368
- "name": "body",
516
+ "raw-body": {
517
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
518
+ "name": "raw-body",
369
519
  "hasDynamicHelp": false,
370
520
  "multiple": false,
371
521
  "type": "option"
372
522
  },
373
523
  "body-file": {
374
- "description": "Path to a JSON file used as the request body",
524
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
375
525
  "name": "body-file",
376
526
  "hasDynamicHelp": false,
377
527
  "multiple": false,
378
528
  "type": "option"
529
+ },
530
+ "is-active": {
531
+ "description": "Whether the domain accepts incoming emails",
532
+ "name": "is-active",
533
+ "allowNo": false,
534
+ "type": "boolean"
535
+ },
536
+ "spam-threshold": {
537
+ "description": "Per-domain spam threshold override (Pro plan required)",
538
+ "name": "spam-threshold",
539
+ "hasDynamicHelp": false,
540
+ "multiple": false,
541
+ "type": "option"
379
542
  }
380
543
  },
381
544
  "hasDynamicHelp": false,
@@ -755,7 +918,7 @@
755
918
  "endpoints:create-endpoint": {
756
919
  "aliases": [],
757
920
  "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)",
921
+ "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 --raw-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
922
  "flags": {
760
923
  "api-key": {
761
924
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -773,19 +936,39 @@
773
936
  "multiple": false,
774
937
  "type": "option"
775
938
  },
776
- "body": {
777
- "description": "JSON request body",
778
- "name": "body",
939
+ "raw-body": {
940
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
941
+ "name": "raw-body",
779
942
  "hasDynamicHelp": false,
780
943
  "multiple": false,
781
944
  "type": "option"
782
945
  },
783
946
  "body-file": {
784
- "description": "Path to a JSON file used as the request body",
947
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
785
948
  "name": "body-file",
786
949
  "hasDynamicHelp": false,
787
950
  "multiple": false,
788
951
  "type": "option"
952
+ },
953
+ "url": {
954
+ "description": "The webhook URL to deliver events to",
955
+ "name": "url",
956
+ "hasDynamicHelp": false,
957
+ "multiple": false,
958
+ "type": "option"
959
+ },
960
+ "domain-id": {
961
+ "description": "Restrict to emails from a specific domain",
962
+ "name": "domain-id",
963
+ "hasDynamicHelp": false,
964
+ "multiple": false,
965
+ "type": "option"
966
+ },
967
+ "enabled": {
968
+ "description": "Whether the endpoint is active",
969
+ "name": "enabled",
970
+ "allowNo": false,
971
+ "type": "boolean"
789
972
  }
790
973
  },
791
974
  "hasDynamicHelp": false,
@@ -913,7 +1096,7 @@
913
1096
  "endpoints:update-endpoint": {
914
1097
  "aliases": [],
915
1098
  "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)",
1099
+ "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 --raw-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
1100
  "flags": {
918
1101
  "api-key": {
919
1102
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -939,19 +1122,39 @@
939
1122
  "multiple": false,
940
1123
  "type": "option"
941
1124
  },
942
- "body": {
943
- "description": "JSON request body",
944
- "name": "body",
1125
+ "raw-body": {
1126
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
1127
+ "name": "raw-body",
945
1128
  "hasDynamicHelp": false,
946
1129
  "multiple": false,
947
1130
  "type": "option"
948
1131
  },
949
1132
  "body-file": {
950
- "description": "Path to a JSON file used as the request body",
1133
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
951
1134
  "name": "body-file",
952
1135
  "hasDynamicHelp": false,
953
1136
  "multiple": false,
954
1137
  "type": "option"
1138
+ },
1139
+ "domain-id": {
1140
+ "description": "domain_id",
1141
+ "name": "domain-id",
1142
+ "hasDynamicHelp": false,
1143
+ "multiple": false,
1144
+ "type": "option"
1145
+ },
1146
+ "enabled": {
1147
+ "description": "enabled",
1148
+ "name": "enabled",
1149
+ "allowNo": false,
1150
+ "type": "boolean"
1151
+ },
1152
+ "url": {
1153
+ "description": "New webhook URL (triggers endpoint rotation)",
1154
+ "name": "url",
1155
+ "hasDynamicHelp": false,
1156
+ "multiple": false,
1157
+ "type": "option"
955
1158
  }
956
1159
  },
957
1160
  "hasDynamicHelp": false,
@@ -967,7 +1170,7 @@
967
1170
  "filters:create-filter": {
968
1171
  "aliases": [],
969
1172
  "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)",
1173
+ "description": "Creates a new whitelist or blocklist filter. Per-domain filters\nrequire a Pro plan. Patterns are stored as lowercase.\n",
971
1174
  "flags": {
972
1175
  "api-key": {
973
1176
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -985,19 +1188,44 @@
985
1188
  "multiple": false,
986
1189
  "type": "option"
987
1190
  },
988
- "body": {
989
- "description": "JSON request body",
990
- "name": "body",
1191
+ "raw-body": {
1192
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
1193
+ "name": "raw-body",
991
1194
  "hasDynamicHelp": false,
992
1195
  "multiple": false,
993
1196
  "type": "option"
994
1197
  },
995
1198
  "body-file": {
996
- "description": "Path to a JSON file used as the request body",
1199
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
997
1200
  "name": "body-file",
998
1201
  "hasDynamicHelp": false,
999
1202
  "multiple": false,
1000
1203
  "type": "option"
1204
+ },
1205
+ "pattern": {
1206
+ "description": "Email address or pattern to filter",
1207
+ "name": "pattern",
1208
+ "hasDynamicHelp": false,
1209
+ "multiple": false,
1210
+ "type": "option"
1211
+ },
1212
+ "type": {
1213
+ "description": "type",
1214
+ "name": "type",
1215
+ "hasDynamicHelp": false,
1216
+ "multiple": false,
1217
+ "options": [
1218
+ "whitelist",
1219
+ "blocklist"
1220
+ ],
1221
+ "type": "option"
1222
+ },
1223
+ "domain-id": {
1224
+ "description": "Restrict filter to a specific domain (Pro plan required)",
1225
+ "name": "domain-id",
1226
+ "hasDynamicHelp": false,
1227
+ "multiple": false,
1228
+ "type": "option"
1001
1229
  }
1002
1230
  },
1003
1231
  "hasDynamicHelp": false,
@@ -1085,7 +1313,7 @@
1085
1313
  "filters:update-filter": {
1086
1314
  "aliases": [],
1087
1315
  "args": {},
1088
- "description": "Toggle a filter's enabled state.\n\nBody fields (JSON --body):\n * enabled boolean\n(* = required)",
1316
+ "description": "Toggle a filter's enabled state.",
1089
1317
  "flags": {
1090
1318
  "api-key": {
1091
1319
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1111,19 +1339,25 @@
1111
1339
  "multiple": false,
1112
1340
  "type": "option"
1113
1341
  },
1114
- "body": {
1115
- "description": "JSON request body",
1116
- "name": "body",
1342
+ "raw-body": {
1343
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
1344
+ "name": "raw-body",
1117
1345
  "hasDynamicHelp": false,
1118
1346
  "multiple": false,
1119
1347
  "type": "option"
1120
1348
  },
1121
1349
  "body-file": {
1122
- "description": "Path to a JSON file used as the request body",
1350
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
1123
1351
  "name": "body-file",
1124
1352
  "hasDynamicHelp": false,
1125
1353
  "multiple": false,
1126
1354
  "type": "option"
1355
+ },
1356
+ "enabled": {
1357
+ "description": "enabled",
1358
+ "name": "enabled",
1359
+ "allowNo": false,
1360
+ "type": "boolean"
1127
1361
  }
1128
1362
  },
1129
1363
  "hasDynamicHelp": false,
@@ -1139,7 +1373,7 @@
1139
1373
  "sending:reply-to-email": {
1140
1374
  "aliases": [],
1141
1375
  "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)",
1376
+ "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
1377
  "flags": {
1144
1378
  "api-key": {
1145
1379
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1165,19 +1399,46 @@
1165
1399
  "multiple": false,
1166
1400
  "type": "option"
1167
1401
  },
1168
- "body": {
1169
- "description": "JSON request body",
1170
- "name": "body",
1402
+ "raw-body": {
1403
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
1404
+ "name": "raw-body",
1171
1405
  "hasDynamicHelp": false,
1172
1406
  "multiple": false,
1173
1407
  "type": "option"
1174
1408
  },
1175
1409
  "body-file": {
1176
- "description": "Path to a JSON file used as the request body",
1410
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
1177
1411
  "name": "body-file",
1178
1412
  "hasDynamicHelp": false,
1179
1413
  "multiple": false,
1180
1414
  "type": "option"
1415
+ },
1416
+ "body-html": {
1417
+ "description": "HTML reply body. At least one of body_text or body_html is required.",
1418
+ "name": "body-html",
1419
+ "hasDynamicHelp": false,
1420
+ "multiple": false,
1421
+ "type": "option"
1422
+ },
1423
+ "body-text": {
1424
+ "description": "Plain-text reply body. At least one of body_text or body_html is required. Th...",
1425
+ "name": "body-text",
1426
+ "hasDynamicHelp": false,
1427
+ "multiple": false,
1428
+ "type": "option"
1429
+ },
1430
+ "from": {
1431
+ "description": "Optional override for the reply's From header. Defaults to",
1432
+ "name": "from",
1433
+ "hasDynamicHelp": false,
1434
+ "multiple": false,
1435
+ "type": "option"
1436
+ },
1437
+ "wait": {
1438
+ "description": "When true, wait for the first downstream SMTP delivery outcome before returni...",
1439
+ "name": "wait",
1440
+ "allowNo": false,
1441
+ "type": "boolean"
1181
1442
  }
1182
1443
  },
1183
1444
  "hasDynamicHelp": false,
@@ -1193,7 +1454,7 @@
1193
1454
  "sending:send-email": {
1194
1455
  "aliases": [],
1195
1456
  "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)",
1457
+ "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 --raw-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
1458
  "flags": {
1198
1459
  "api-key": {
1199
1460
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1211,19 +1472,74 @@
1211
1472
  "multiple": false,
1212
1473
  "type": "option"
1213
1474
  },
1214
- "body": {
1215
- "description": "JSON request body",
1216
- "name": "body",
1475
+ "raw-body": {
1476
+ "description": "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available.",
1477
+ "name": "raw-body",
1217
1478
  "hasDynamicHelp": false,
1218
1479
  "multiple": false,
1219
1480
  "type": "option"
1220
1481
  },
1221
1482
  "body-file": {
1222
- "description": "Path to a JSON file used as the request body",
1483
+ "description": "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload.",
1223
1484
  "name": "body-file",
1224
1485
  "hasDynamicHelp": false,
1225
1486
  "multiple": false,
1226
1487
  "type": "option"
1488
+ },
1489
+ "from": {
1490
+ "description": "RFC 5322 From header. The sender domain must be a verified outbound domain fo...",
1491
+ "name": "from",
1492
+ "hasDynamicHelp": false,
1493
+ "multiple": false,
1494
+ "type": "option"
1495
+ },
1496
+ "subject": {
1497
+ "description": "Subject line for the outbound message",
1498
+ "name": "subject",
1499
+ "hasDynamicHelp": false,
1500
+ "multiple": false,
1501
+ "type": "option"
1502
+ },
1503
+ "to": {
1504
+ "description": "Recipient address. Recipient eligibility depends on your account's outbound e...",
1505
+ "name": "to",
1506
+ "hasDynamicHelp": false,
1507
+ "multiple": false,
1508
+ "type": "option"
1509
+ },
1510
+ "body-html": {
1511
+ "description": "HTML message body. At least one of body_text or body_html is required. The co...",
1512
+ "name": "body-html",
1513
+ "hasDynamicHelp": false,
1514
+ "multiple": false,
1515
+ "type": "option"
1516
+ },
1517
+ "body-text": {
1518
+ "description": "Plain-text message body. At least one of body_text or body_html is required. ...",
1519
+ "name": "body-text",
1520
+ "hasDynamicHelp": false,
1521
+ "multiple": false,
1522
+ "type": "option"
1523
+ },
1524
+ "in-reply-to": {
1525
+ "description": "Message-ID of the direct parent email when sending a threaded reply.",
1526
+ "name": "in-reply-to",
1527
+ "hasDynamicHelp": false,
1528
+ "multiple": false,
1529
+ "type": "option"
1530
+ },
1531
+ "wait": {
1532
+ "description": "When true, wait for the first downstream SMTP delivery outcome before returning.",
1533
+ "name": "wait",
1534
+ "allowNo": false,
1535
+ "type": "boolean"
1536
+ },
1537
+ "wait-timeout-ms": {
1538
+ "description": "Maximum time to wait for a delivery outcome when wait is true. Defaults to 30...",
1539
+ "name": "wait-timeout-ms",
1540
+ "hasDynamicHelp": false,
1541
+ "multiple": false,
1542
+ "type": "option"
1227
1543
  }
1228
1544
  },
1229
1545
  "hasDynamicHelp": false,
@@ -1363,5 +1679,5 @@
1363
1679
  "enableJsonFlag": false
1364
1680
  }
1365
1681
  },
1366
- "version": "0.10.0"
1682
+ "version": "0.12.0"
1367
1683
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",