@primitivedotdev/sdk 0.9.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.
- package/README.md +22 -0
- package/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +26 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +72 -33
- package/dist/{api-COSr-Fqm.js → api-CLLpjjWy.js} +87 -31
- package/dist/{index-DVow4Fjd.d.ts → index-K4KbjppU.d.ts} +220 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oclif/api-command.js +217 -47
- package/dist/oclif/commands/send.js +159 -0
- package/dist/oclif/index.js +6 -0
- package/dist/openapi/openapi.generated.js +190 -3
- package/dist/openapi/operations.generated.js +49 -0
- package/oclif.manifest.json +359 -25
- package/package.json +1 -1
|
@@ -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
|
|
12
|
+
return [];
|
|
25
13
|
const properties = schema.properties;
|
|
26
14
|
if (!properties || typeof properties !== "object")
|
|
27
|
-
return
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(`${
|
|
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({
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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;
|
package/dist/oclif/index.js
CHANGED
|
@@ -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
|
};
|