@primitivedotdev/sdk 0.12.0 → 0.14.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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { A as UnknownEvent, C as ParsedDataFailed, D as RawContentDownloadOnly, E as RawContent, M as WebhookAttachment, N as WebhookEvent, O as RawContentInline, S as ParsedDataComplete, T as ParsedStatus, _ as ForwardResultInline, a as DmarcPolicy, b as KnownWebhookEvent, c as EmailAnalysis, d as EventType, f as ForwardAnalysis, g as ForwardResultAttachmentSkipped, h as ForwardResultAttachmentAnalyzed, i as DkimSignature, j as ValidateEmailAuthResult, k as SpfResult, l as EmailAuth, m as ForwardResult, n as AuthVerdict, o as DmarcResult, p as ForwardOriginalSender, r as DkimResult, s as EmailAddress, t as AuthConfidence, u as EmailReceivedEvent, v as ForwardVerdict, w as ParsedError, x as ParsedData, y as ForwardVerification } from "./types-9vXGZjPd.js";
2
2
  import { a as buildReplySubject, c as parseHeaderAddress, i as buildForwardSubject, n as ReceivedEmailAddress, o as formatAddress, r as ReceivedEmailThread, s as normalizeReceivedEmail, t as ReceivedEmail } from "./received-email-DNjpq_Wt.js";
3
- import { a as PrimitiveApiError, c as PrimitiveClientOptions, d as SendResult, f as SendThreadInput, h as createPrimitiveClient, l as ReplyInput, n as ForwardInput, p as client, s as PrimitiveClient, u as SendInput } from "./index-K4KbjppU.js";
3
+ import { a as PrimitiveApiError, c as PrimitiveClientOptions, d as SendResult, f as SendThreadInput, h as createPrimitiveClient, l as ReplyInput, n as ForwardInput, p as client, s as PrimitiveClient, u as SendInput } from "./index-Cts9r1sL.js";
4
4
  import { A as VerifyOptions, B as PAYLOAD_ERRORS, C as signStandardWebhooksPayload, D as PRIMITIVE_CONFIRMED_HEADER, E as LEGACY_SIGNATURE_HEADER, F as VerifyDownloadTokenResult, G as VERIFICATION_ERRORS, H as RAW_EMAIL_ERRORS, I as generateDownloadToken, J as WebhookPayloadErrorCode, K as WebhookErrorCode, L as verifyDownloadToken, M as verifyWebhookSignature, N as GenerateDownloadTokenOptions, O as PRIMITIVE_SIGNATURE_HEADER, P as VerifyDownloadTokenOptions, Q as WebhookVerificationErrorCode, R as safeValidateEmailReceivedEvent, S as StandardWebhooksVerifyOptions, T as LEGACY_CONFIRMED_HEADER, U as RawEmailDecodeError, V as PrimitiveWebhookError, W as RawEmailDecodeErrorCode, X as WebhookValidationErrorCode, Y as WebhookValidationError, Z as WebhookVerificationError, _ as emailReceivedEventJsonSchema, a as confirmedHeaders, b as STANDARD_WEBHOOK_TIMESTAMP_HEADER, c as handleWebhook, d as isRawIncluded, f as parseWebhookEvent, g as validateEmailAuth, h as WEBHOOK_VERSION, i as WebhookHeaders, j as signWebhookPayload, k as SignResult, l as isDownloadExpired, m as verifyRawEmailDownload, n as HandleWebhookOptions, o as decodeRawEmail, p as receive, q as WebhookPayloadError, r as ReceiveRequestOptions, s as getDownloadTimeRemaining, t as DecodeRawEmailOptions, u as isEmailReceivedEvent, v as STANDARD_WEBHOOK_ID_HEADER, w as verifyStandardWebhooksSignature, x as StandardWebhooksSignResult, y as STANDARD_WEBHOOK_SIGNATURE_HEADER, z as validateEmailReceivedEvent } from "./index-CbEivn3S.js";
5
5
 
6
6
  //#region src/index.d.ts
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as parseHeaderAddress, i as normalizeReceivedEmail, n as buildReplySubject, r as formatAddress, t as buildForwardSubject } from "./received-email-D6tKtWwW.js";
2
- import { a as client, i as PrimitiveClient, r as PrimitiveApiError, s as createPrimitiveClient } from "./api-CLLpjjWy.js";
2
+ import { a as client, i as PrimitiveClient, r as PrimitiveApiError, s as createPrimitiveClient } from "./api-DH-YKt7a.js";
3
3
  import { A as PRIMITIVE_CONFIRMED_HEADER, B as RAW_EMAIL_ERRORS, C as STANDARD_WEBHOOK_ID_HEADER, D as verifyStandardWebhooksSignature, E as signStandardWebhooksPayload, F as verifyDownloadToken, G as WebhookVerificationError, H as VERIFICATION_ERRORS, I as safeValidateEmailReceivedEvent, L as validateEmailReceivedEvent, M as signWebhookPayload, N as verifyWebhookSignature, O as LEGACY_CONFIRMED_HEADER, P as generateDownloadToken, R as PAYLOAD_ERRORS, S as emailReceivedEventJsonSchema, T as STANDARD_WEBHOOK_TIMESTAMP_HEADER, U as WebhookPayloadError, V as RawEmailDecodeError, W as WebhookValidationError, _ as DmarcResult, a as isDownloadExpired, b as ParsedStatus, c as parseWebhookEvent, d as WEBHOOK_VERSION, f as validateEmailAuth, g as DmarcPolicy, h as DkimResult, i as handleWebhook, j as PRIMITIVE_SIGNATURE_HEADER, k as LEGACY_SIGNATURE_HEADER, l as receive, m as AuthVerdict, n as decodeRawEmail, o as isEmailReceivedEvent, p as AuthConfidence, r as getDownloadTimeRemaining, s as isRawIncluded, t as confirmedHeaders, u as verifyRawEmailDownload, v as EventType, w as STANDARD_WEBHOOK_SIGNATURE_HEADER, x as SpfResult, y as ForwardVerdict, z as PrimitiveWebhookError } from "./webhook-zkN4wUTs.js";
4
4
  //#region src/index.ts
5
5
  const primitive = {
@@ -67,8 +67,22 @@ function extractBodyFields(schema) {
67
67
  kind = "complex";
68
68
  }
69
69
  }
70
+ // Pull the first paragraph of the schema description for use
71
+ // as the CLI flag's --help string. We split on a blank line
72
+ // (paragraph break) and then collapse any soft line wraps
73
+ // inside that paragraph to spaces. This avoids the previous
74
+ // bug where `split("\n")[0]` truncated wrapped prose like
75
+ // "Optional override for ... Defaults to\nthe inbound's..."
76
+ // to "Optional override for ... Defaults to" - a sentence
77
+ // ending with "to" with nothing after it, which read as
78
+ // ellipsis truncation in --help. The remaining paragraphs
79
+ // are intentionally dropped so multi-paragraph schemas don't
80
+ // blow out the per-flag help block.
70
81
  const description = typeof propSchema.description === "string"
71
- ? propSchema.description.split("\n")[0].trim()
82
+ ? propSchema.description
83
+ .split(/\n\s*\n/)[0]
84
+ .replace(/\s*\n\s*/g, " ")
85
+ .trim()
72
86
  : "";
73
87
  const enumRaw = propSchema.enum;
74
88
  const enumValues = kind === "string" && Array.isArray(enumRaw)
@@ -242,6 +256,52 @@ export function formatErrorPayload(payload) {
242
256
  }
243
257
  return JSON.stringify(payload, null, 2);
244
258
  }
259
+ // Pull the top-level error code out of either a server response
260
+ // payload (`{ error: { code: '...' } }` or `{ code: '...' }`) or a
261
+ // thrown Error whose `cause.code` carries the value. Used to drive
262
+ // `--api-key` and similar hints in writeErrorWithHints below.
263
+ // Also exported so individual commands (send, whoami) can branch
264
+ // on auth failures and avoid surfacing misleading "fix this flag"
265
+ // guidance when the real problem is the API key.
266
+ export function extractErrorCode(payload) {
267
+ if (payload instanceof Error) {
268
+ const { code } = extractCauseDetails(payload.cause);
269
+ return code;
270
+ }
271
+ if (payload && typeof payload === "object") {
272
+ const inner = payload.error;
273
+ if (inner && typeof inner === "object" && typeof inner.code === "string") {
274
+ return inner.code;
275
+ }
276
+ const direct = payload.code;
277
+ if (typeof direct === "string")
278
+ return direct;
279
+ }
280
+ return undefined;
281
+ }
282
+ // Common-case actionable hints keyed by error code. The full
283
+ // JSON envelope still goes to stderr unchanged for any caller
284
+ // that wants to parse it; the hint is an extra trailing line so
285
+ // a human reading the output sees "what to actually do next."
286
+ // The AGX walkthrough flagged that an `unauthorized` envelope
287
+ // alone left the agent without context for the env var or the
288
+ // `--api-key` flag; this closes that gap without having to
289
+ // special-case every command.
290
+ const ERROR_CODE_HINTS = {
291
+ unauthorized: "Hint: pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify a key is live.",
292
+ };
293
+ // Write a server / SDK error to stderr in the canonical envelope
294
+ // shape, plus an actionable hint when the code is one we know how
295
+ // to advise on. Replaces the bare
296
+ // `process.stderr.write(${formatErrorPayload(p)}\n)` dance every
297
+ // command was doing.
298
+ export function writeErrorWithHints(payload) {
299
+ process.stderr.write(`${formatErrorPayload(payload)}\n`);
300
+ const code = extractErrorCode(payload);
301
+ if (code && ERROR_CODE_HINTS[code]) {
302
+ process.stderr.write(`${ERROR_CODE_HINTS[code]}\n`);
303
+ }
304
+ }
245
305
  // Reserved flag names the body-field expander must never overwrite.
246
306
  // `--raw-body` and `--body-file` are the JSON escape hatches.
247
307
  // `--api-key`, `--base-url`, `--output` are infra. Path and query
@@ -266,22 +326,23 @@ const RESERVED_FLAG_NAMES = new Set([
266
326
  "output",
267
327
  ]);
268
328
  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;
329
+ // Pass the full first-line description through. oclif's --help
330
+ // renderer wraps long values across multiple lines on its own,
331
+ // so a fixed character cap here just produces ellipsis-truncated
332
+ // sentences ("body_html is required. Th...") that mislead the
333
+ // reader. extractBodyFields already normalizes by taking only
334
+ // the first paragraph of the schema description, so multi-
335
+ // paragraph fields don't blow out the help.
336
+ //
276
337
  // Field-flag UX choice: do NOT mark scalar body fields as
277
338
  // required at the oclif level even when the JSON Schema marks
278
339
  // them required. Reason: a caller can satisfy the requirement
279
- // either via the individual flag OR via --body / --body-file.
340
+ // either via the individual flag OR via --raw-body / --body-file.
280
341
  // Marking the flag required would force the individual-flag
281
342
  // form. The runtime body merger validates the final assembled
282
343
  // body against the same server-side schema either way.
283
344
  const common = {
284
- description: trimmedDesc || field.name,
345
+ description: field.description || field.name,
285
346
  };
286
347
  if (field.kind === "boolean")
287
348
  return Flags.boolean(common);
@@ -460,8 +521,7 @@ export function createOperationCommand(operation) {
460
521
  responseStyle: "fields",
461
522
  });
462
523
  if (result.error) {
463
- const errorPayload = extractErrorPayload(result.error);
464
- process.stderr.write(`${formatErrorPayload(errorPayload)}\n`);
524
+ writeErrorWithHints(extractErrorPayload(result.error));
465
525
  process.exitCode = 1;
466
526
  return;
467
527
  }
@@ -0,0 +1,118 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { listEmails } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, writeErrorWithHints } from "../api-command.js";
5
+ // `primitive emails:latest` is the agent-grade shortcut for "show me
6
+ // the most recent inbound emails as something I can read at a glance."
7
+ // `emails:list-emails` returns the full JSON envelope which is great
8
+ // for piping but blows out the screen for a quick triage. The AGX
9
+ // walkthrough flagged that an agent doing inbox triage had no compact
10
+ // view to reach for; `latest` is that view.
11
+ //
12
+ // Output is a fixed-width text table: short-id, received timestamp,
13
+ // from address, to address, and subject. Subject is truncated for
14
+ // display only; the underlying JSON is unchanged. For machine-readable
15
+ // output, callers should use `emails:list-emails` and pipe to jq.
16
+ const DEFAULT_LIMIT = 10;
17
+ const MAX_LIMIT = 100;
18
+ // Truncation widths chosen so a row fits in ~140 columns total. Long
19
+ // values wrap to "..." rather than blowing out terminal layout.
20
+ const SUBJECT_DISPLAY_WIDTH = 50;
21
+ const ADDRESS_DISPLAY_WIDTH = 32;
22
+ const ID_DISPLAY_WIDTH = 8;
23
+ const RECEIVED_DISPLAY_WIDTH = 19;
24
+ // Truncate to width with right-padding; values longer than width are
25
+ // cut to width-3 with a "..." suffix so the output is exactly `width`
26
+ // chars (3 of which are the ellipsis). Display-only; never mutates
27
+ // the underlying value the caller passed in.
28
+ //
29
+ // Width-exact output matters here: formatRow relies on each column
30
+ // being exactly its declared width so columns line up across rows.
31
+ // An overflowing truncate would shift every later column to the
32
+ // right whenever truncation fired (e.g. a row with both addresses
33
+ // truncated would push SUBJECT 4 chars off).
34
+ export function truncate(value, width) {
35
+ if (value.length <= width)
36
+ return value.padEnd(width);
37
+ return `${value.slice(0, width - 3)}...`;
38
+ }
39
+ // Compact ISO timestamp for display: `YYYY-MM-DD HH:MM:SS` in UTC.
40
+ // The full ISO string with milliseconds and `T`/`Z` markers is too
41
+ // dense to scan at a glance; this is the same shape git log uses.
42
+ export function formatReceivedAt(value) {
43
+ if (!value)
44
+ return "-".padEnd(RECEIVED_DISPLAY_WIDTH);
45
+ const d = new Date(value);
46
+ if (Number.isNaN(d.getTime()))
47
+ return value.padEnd(RECEIVED_DISPLAY_WIDTH);
48
+ const pad = (n) => String(n).padStart(2, "0");
49
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
50
+ }
51
+ export function formatRow(email) {
52
+ const id = truncate(email.id.slice(0, ID_DISPLAY_WIDTH), ID_DISPLAY_WIDTH);
53
+ const received = formatReceivedAt(email.received_at);
54
+ const from = truncate(email.sender ?? "", ADDRESS_DISPLAY_WIDTH);
55
+ const to = truncate(email.recipient ?? "", ADDRESS_DISPLAY_WIDTH);
56
+ const subject = (email.subject ?? "").replace(/\s+/g, " ");
57
+ const subjectCol = truncate(subject, SUBJECT_DISPLAY_WIDTH);
58
+ return `${id} ${received} ${from} ${to} ${subjectCol}`;
59
+ }
60
+ class EmailsLatestCommand extends Command {
61
+ static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails:list-emails\` (full JSON envelope, cursor pagination, filters).
62
+
63
+ The displayed id is the first ${ID_DISPLAY_WIDTH} characters of the email's UUID; pass the full UUID (from \`emails:list-emails\` or \`emails:get-email\`) to operations that need it.`;
64
+ static summary = "Show the most recent inbound emails as a compact table";
65
+ static examples = [
66
+ "<%= config.bin %> emails:latest",
67
+ "<%= config.bin %> emails:latest --limit 25",
68
+ ];
69
+ static flags = {
70
+ "api-key": Flags.string({
71
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
72
+ env: "PRIMITIVE_API_KEY",
73
+ }),
74
+ "base-url": Flags.string({
75
+ description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
76
+ env: "PRIMITIVE_API_URL",
77
+ }),
78
+ limit: Flags.integer({
79
+ description: `Number of rows to print (1-${MAX_LIMIT}, default ${DEFAULT_LIMIT}).`,
80
+ default: DEFAULT_LIMIT,
81
+ // oclif validates min/max at parse time and emits a consistent
82
+ // out-of-range error before run() is reached, so no manual
83
+ // bounds check is needed here.
84
+ min: 1,
85
+ max: MAX_LIMIT,
86
+ }),
87
+ };
88
+ async run() {
89
+ const { flags } = await this.parse(EmailsLatestCommand);
90
+ const apiClient = new PrimitiveApiClient({
91
+ apiKey: flags["api-key"],
92
+ baseUrl: flags["base-url"],
93
+ });
94
+ const result = await listEmails({
95
+ client: apiClient.client,
96
+ query: { limit: flags.limit },
97
+ responseStyle: "fields",
98
+ });
99
+ if (result.error) {
100
+ writeErrorWithHints(extractErrorPayload(result.error));
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ const envelope = result.data;
105
+ const rows = envelope?.data ?? [];
106
+ if (rows.length === 0) {
107
+ process.stderr.write("No inbound emails yet. Send an email to one of your verified domains to populate this list.\n");
108
+ return;
109
+ }
110
+ // Header on stderr so the table itself stays grep-friendly.
111
+ const header = `${"ID".padEnd(ID_DISPLAY_WIDTH)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
112
+ process.stderr.write(`${header}\n`);
113
+ for (const row of rows) {
114
+ this.log(formatRow(row));
115
+ }
116
+ }
117
+ }
118
+ export default EmailsLatestCommand;
@@ -1,7 +1,7 @@
1
1
  import { Command, Errors, Flags } from "@oclif/core";
2
2
  import { listDomains, sendEmail } from "../../api/generated/sdk.gen.js";
3
3
  import { PrimitiveApiClient } from "../../api/index.js";
4
- import { extractErrorPayload, formatErrorPayload } from "../api-command.js";
4
+ import { extractErrorCode, extractErrorPayload, formatErrorPayload, writeErrorWithHints, } from "../api-command.js";
5
5
  // `primitive send` is the agent-grade shortcut for the most common
6
6
  // case: send a fresh outbound email. It wraps `sending:send-email`
7
7
  // with two ergonomic defaults that the underlying operation can't
@@ -30,7 +30,15 @@ import { extractErrorPayload, formatErrorPayload } from "../api-command.js";
30
30
  // pattern-matching from there lands in the happy path. We just
31
31
  // don't need swaks's `--server` / `--auth-*` flags because the
32
32
  // HTTPS API key is the auth and the server is implicit.
33
- const SUBJECT_MAX_LENGTH = 70;
33
+ // 200 chars is a generous cap that almost never trips on natural
34
+ // first-line subjects (a sentence is typically <120 chars). The
35
+ // previous 70-char limit was tight enough that legitimate one-line
36
+ // bodies routinely produced ellipsis-truncated subjects in inbox
37
+ // listings, e.g. `"this is the simplest possible send: agent typed
38
+ // two flags and hit\\n e..."` from the AGX walkthrough. Real spam
39
+ // scoring engines don't penalize subjects under ~200 chars, so 200
40
+ // is both more useful and still well under the practical wire limit.
41
+ const SUBJECT_MAX_LENGTH = 200;
34
42
  function deriveSubject(body) {
35
43
  for (const line of body.split("\n")) {
36
44
  const trimmed = line.trim();
@@ -52,6 +60,20 @@ async function pickDefaultFromAddress(apiClient) {
52
60
  });
53
61
  if (result.error) {
54
62
  const errorPayload = extractErrorPayload(result.error);
63
+ // If the underlying failure is an auth problem, don't pretend
64
+ // --from will fix it: the actual sendEmail call would 401 too.
65
+ // Surface the auth hint via writeErrorWithHints and bail with
66
+ // a focused message instead of the verbose "underlying error"
67
+ // wrapping.
68
+ if (extractErrorCode(errorPayload) === "unauthorized") {
69
+ writeErrorWithHints(errorPayload);
70
+ // exit: 1 to match the run() unauthorized path (which uses
71
+ // `process.exitCode = 1`). oclif's CLIError defaults to 2,
72
+ // so without this override the same "unauthorized" condition
73
+ // exits 2 when surfaced from listDomains and 1 when surfaced
74
+ // from sendEmail, breaking callers that branch on exit code.
75
+ throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
76
+ }
55
77
  throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
56
78
  }
57
79
  const envelope = result.data;
@@ -147,8 +169,7 @@ class SendCommand extends Command {
147
169
  responseStyle: "fields",
148
170
  });
149
171
  if (result.error) {
150
- const errorPayload = extractErrorPayload(result.error);
151
- process.stderr.write(`${formatErrorPayload(errorPayload)}\n`);
172
+ writeErrorWithHints(extractErrorPayload(result.error));
152
173
  process.exitCode = 1;
153
174
  return;
154
175
  }
@@ -1,7 +1,7 @@
1
1
  import { Command, Errors, Flags } from "@oclif/core";
2
2
  import { getAccount } from "../../api/generated/sdk.gen.js";
3
3
  import { PrimitiveApiClient } from "../../api/index.js";
4
- import { extractErrorPayload, formatErrorPayload } from "../api-command.js";
4
+ import { extractErrorPayload, writeErrorWithHints } from "../api-command.js";
5
5
  // `primitive whoami` is the credentials smoke-test the AGX
6
6
  // walkthrough kept asking for. Before this command, a user with a
7
7
  // suspect API key had no fast way to verify "is my key live and
@@ -40,8 +40,7 @@ class WhoamiCommand extends Command {
40
40
  responseStyle: "fields",
41
41
  });
42
42
  if (result.error) {
43
- const errorPayload = extractErrorPayload(result.error);
44
- process.stderr.write(`${formatErrorPayload(errorPayload)}\n`);
43
+ writeErrorWithHints(extractErrorPayload(result.error));
45
44
  process.exitCode = 1;
46
45
  return;
47
46
  }
@@ -1,16 +1,73 @@
1
- import { Args, Command } from "@oclif/core";
1
+ import { Args, Command, Errors } from "@oclif/core";
2
2
  import { operationManifest, } from "../openapi/index.js";
3
3
  import { createOperationCommand } from "./api-command.js";
4
+ import EmailsLatestCommand from "./commands/emails-latest.js";
4
5
  import SendCommand from "./commands/send.js";
5
6
  import WhoamiCommand from "./commands/whoami.js";
6
7
  import { renderFishCompletion } from "./fish-completion.js";
7
8
  class ListOperationsCommand extends Command {
8
- static description = "List all generated API operations";
9
- static summary = "List all generated API operations";
9
+ static description = "List all generated API operations as JSON. Useful for piping to `jq` to discover available commands, their request/response schemas, and per-field descriptions. For inspecting a single operation in detail, prefer `primitive describe <command>`.";
10
+ static summary = "List all generated API operations (JSON)";
10
11
  async run() {
11
12
  this.log(JSON.stringify(operationManifest, null, 2));
12
13
  }
13
14
  }
15
+ // Looks up an operation manifest entry by its `<topic>:<command>` id
16
+ // (e.g. `emails:get-email`). On miss, returns up to 5 closest
17
+ // candidates by substring match so the caller can render a
18
+ // "did you mean" hint. Pure function: no oclif config dependency,
19
+ // so it's also unit-testable in isolation.
20
+ export function lookupOperation(id) {
21
+ const trimmed = id.trim();
22
+ const sep = trimmed.indexOf(":");
23
+ const tag = sep === -1 ? "" : trimmed.slice(0, sep);
24
+ const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
25
+ const match = operationManifest.find((op) => op.command === cmd && op.tagCommand === tag) ?? null;
26
+ if (match)
27
+ return { match, candidates: [] };
28
+ const candidates = operationManifest
29
+ .filter((op) => op.command.includes(cmd) || op.tagCommand.includes(tag))
30
+ .slice(0, 5)
31
+ .map((op) => op.tagCommand ? `${op.tagCommand}:${op.command}` : op.command);
32
+ return { match: null, candidates };
33
+ }
34
+ // `primitive describe <command>` is the operation-detail inspector
35
+ // the AGX walkthrough kept wanting. The information is already in
36
+ // the operation manifest emitted by `list-operations`, but agents
37
+ // don't intuitively reach for `list-operations | jq '.[] | select(...)'`
38
+ // when they want to know "what does the from_email field on this
39
+ // response actually mean." A direct command is more discoverable.
40
+ //
41
+ // Lookup is by the colon-joined command id (e.g. `emails:get-email`,
42
+ // `sending:send-email`, `account:get-account`). For top-level
43
+ // generated commands (without a topic), pass the bare command id.
44
+ class DescribeCommand extends Command {
45
+ static args = {
46
+ command: Args.string({
47
+ description: "Command id to describe, in `<topic>:<command>` form (e.g. `emails:get-email`). Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate.",
48
+ required: true,
49
+ }),
50
+ };
51
+ static description = `Print the full operation manifest entry for a single API command, including the path, request schema, response schema, and per-field descriptions sourced from the OpenAPI spec.
52
+
53
+ Useful for clarifying response field meanings (e.g. on inbound EmailDetail, which of \`sender\`, \`from_email\`, \`from_header\`, and \`smtp_mail_from\` to read), confirming required body fields, or checking a path's parameter shape before composing a request.`;
54
+ static summary = "Describe a single API operation in detail";
55
+ static examples = [
56
+ "<%= config.bin %> describe emails:get-email",
57
+ "<%= config.bin %> describe sending:send-email",
58
+ ];
59
+ async run() {
60
+ const { args } = await this.parse(DescribeCommand);
61
+ const { match, candidates } = lookupOperation(args.command);
62
+ if (!match) {
63
+ const hint = candidates.length > 0
64
+ ? `Did you mean: ${candidates.join(", ")}?`
65
+ : "Run `primitive list-operations` to enumerate.";
66
+ throw new Errors.CLIError(`Unknown operation \`${args.command.trim()}\`. ${hint}`, { exit: 1 });
67
+ }
68
+ this.log(JSON.stringify(match, null, 2));
69
+ }
70
+ }
14
71
  class CompletionCommand extends Command {
15
72
  static args = {
16
73
  shell: Args.string({
@@ -40,6 +97,11 @@ const generatedCommands = Object.fromEntries(operationManifest.map((operation) =
40
97
  export const COMMANDS = {
41
98
  completion: CompletionCommand,
42
99
  "list-operations": ListOperationsCommand,
100
+ // `describe` prints a single operation's full manifest entry
101
+ // (path, request schema, response schema, per-field descriptions).
102
+ // The same data is in `list-operations` but agents don't reach for
103
+ // `list-operations | jq` when they want to clarify a field meaning.
104
+ describe: DescribeCommand,
43
105
  // `send` is the agent-grade shortcut for sending:send-email with
44
106
  // sensible defaults (auto from-address, auto subject). The full
45
107
  // operation stays available under sending:send-email for callers
@@ -50,5 +112,9 @@ export const COMMANDS = {
50
112
  // wanting this before risking a real call against a possibly-
51
113
  // bad key.
52
114
  whoami: WhoamiCommand,
115
+ // `emails:latest` is the inbox-triage shortcut: the most recent N
116
+ // inbound emails as a compact text table. emails:list-emails stays
117
+ // available for the full JSON envelope + cursor pagination.
118
+ "emails:latest": EmailsLatestCommand,
53
119
  ...generatedCommands,
54
120
  };