@primitivedotdev/sdk 0.13.0 → 0.15.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/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +67 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/{api-DvJpdOJ8.js → api-DpATn7LQ.js} +76 -2
- package/dist/{index-ChLFXxTa.d.ts → index-DEY4h3MZ.d.ts} +464 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oclif/commands/emails-latest.js +131 -0
- package/dist/oclif/commands/send.js +10 -1
- package/dist/oclif/index.js +78 -3
- package/dist/openapi/index.d.ts +6 -0
- package/dist/openapi/openapi.generated.js +395 -47
- package/dist/openapi/operations.generated.js +2395 -98
- package/oclif.manifest.json +212 -13
- package/package.json +3 -3
|
@@ -0,0 +1,131 @@
|
|
|
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) or pass \`--json\` here for the same raw shape without pagination/filters.
|
|
62
|
+
|
|
63
|
+
The default text table truncates each row's id to the first ${ID_DISPLAY_WIDTH} characters for readability. Operations that take an id (\`emails:get-email\`, \`emails:delete-email\`, etc.) require the full UUID, so pass \`--json\` or use \`emails:list-emails\` when you need to feed an id back into another command.
|
|
64
|
+
|
|
65
|
+
Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT.`;
|
|
66
|
+
static summary = "Show the most recent inbound emails as a compact table";
|
|
67
|
+
static examples = [
|
|
68
|
+
"<%= config.bin %> emails:latest",
|
|
69
|
+
"<%= config.bin %> emails:latest --limit 25",
|
|
70
|
+
"<%= config.bin %> emails:latest --json | jq '.data[0].id'",
|
|
71
|
+
];
|
|
72
|
+
static flags = {
|
|
73
|
+
"api-key": Flags.string({
|
|
74
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
|
|
75
|
+
env: "PRIMITIVE_API_KEY",
|
|
76
|
+
}),
|
|
77
|
+
"base-url": Flags.string({
|
|
78
|
+
description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
|
|
79
|
+
env: "PRIMITIVE_API_URL",
|
|
80
|
+
}),
|
|
81
|
+
limit: Flags.integer({
|
|
82
|
+
description: `Number of rows to print (1-${MAX_LIMIT}, default ${DEFAULT_LIMIT}).`,
|
|
83
|
+
default: DEFAULT_LIMIT,
|
|
84
|
+
// oclif validates min/max at parse time and emits a consistent
|
|
85
|
+
// out-of-range error before run() is reached, so no manual
|
|
86
|
+
// bounds check is needed here.
|
|
87
|
+
min: 1,
|
|
88
|
+
max: MAX_LIMIT,
|
|
89
|
+
}),
|
|
90
|
+
json: Flags.boolean({
|
|
91
|
+
description: "Print the raw response envelope (with full UUIDs and meta) as JSON on STDOUT instead of the text table. Useful for piping into `jq`, capturing ids for follow-up commands, or scripting.",
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
async run() {
|
|
95
|
+
const { flags } = await this.parse(EmailsLatestCommand);
|
|
96
|
+
const apiClient = new PrimitiveApiClient({
|
|
97
|
+
apiKey: flags["api-key"],
|
|
98
|
+
baseUrl: flags["base-url"],
|
|
99
|
+
});
|
|
100
|
+
const result = await listEmails({
|
|
101
|
+
client: apiClient.client,
|
|
102
|
+
query: { limit: flags.limit },
|
|
103
|
+
responseStyle: "fields",
|
|
104
|
+
});
|
|
105
|
+
if (result.error) {
|
|
106
|
+
writeErrorWithHints(extractErrorPayload(result.error));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const envelope = result.data;
|
|
111
|
+
if (flags.json) {
|
|
112
|
+
// Raw envelope on stdout. Mirrors the shape `emails:list-emails`
|
|
113
|
+
// emits so callers can swap one for the other when they want
|
|
114
|
+
// table vs json without remembering different command names.
|
|
115
|
+
this.log(JSON.stringify(envelope ?? null, null, 2));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const rows = envelope?.data ?? [];
|
|
119
|
+
if (rows.length === 0) {
|
|
120
|
+
process.stderr.write("No inbound emails yet. Send an email to one of your verified domains to populate this list.\n");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Header on stderr so the table itself stays grep-friendly.
|
|
124
|
+
const header = `${"ID".padEnd(ID_DISPLAY_WIDTH)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
|
|
125
|
+
process.stderr.write(`${header}\n`);
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
this.log(formatRow(row));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export default EmailsLatestCommand;
|
|
@@ -30,7 +30,15 @@ import { extractErrorCode, extractErrorPayload, formatErrorPayload, writeErrorWi
|
|
|
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
|
-
|
|
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();
|
|
@@ -95,6 +103,7 @@ class SendCommand extends Command {
|
|
|
95
103
|
"<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
|
|
96
104
|
"<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
|
|
97
105
|
"<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
|
|
106
|
+
"<%= config.bin %> send --to inbox@your-managed-domain.primitive.email --body 'self-loop smoke test' --wait # any *.primitive.email address routes back to the sending account; useful for proving outbound + inbound work end-to-end",
|
|
98
107
|
];
|
|
99
108
|
static flags = {
|
|
100
109
|
"api-key": Flags.string({
|
package/dist/oclif/index.js
CHANGED
|
@@ -1,16 +1,82 @@
|
|
|
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
|
+
The manifest entry's \`responseSchema\` carries the inlined JSON Schema for the operation's 200/201 \`data\` envelope contents (\`$ref\`s resolved). Use it to look up what specific response fields mean. Examples:
|
|
54
|
+
|
|
55
|
+
# Which of EmailDetail's sender-shaped fields is canonical?
|
|
56
|
+
primitive describe emails:get-email | jq '.responseSchema.properties | keys'
|
|
57
|
+
primitive describe emails:get-email | jq -r '.responseSchema.properties.from_email.description'
|
|
58
|
+
|
|
59
|
+
# What does each value of SentEmailStatus mean?
|
|
60
|
+
primitive describe sending:get-sent-email | jq -r '.responseSchema.properties.status.description'
|
|
61
|
+
|
|
62
|
+
\`requestSchema\` is the same shape for the request body when one exists. For a single field across many operations at once, use \`primitive list-operations | jq\` instead.`;
|
|
63
|
+
static summary = "Describe a single API operation in detail";
|
|
64
|
+
static examples = [
|
|
65
|
+
"<%= config.bin %> describe emails:get-email",
|
|
66
|
+
"<%= config.bin %> describe sending:send-email",
|
|
67
|
+
];
|
|
68
|
+
async run() {
|
|
69
|
+
const { args } = await this.parse(DescribeCommand);
|
|
70
|
+
const { match, candidates } = lookupOperation(args.command);
|
|
71
|
+
if (!match) {
|
|
72
|
+
const hint = candidates.length > 0
|
|
73
|
+
? `Did you mean: ${candidates.join(", ")}?`
|
|
74
|
+
: "Run `primitive list-operations` to enumerate.";
|
|
75
|
+
throw new Errors.CLIError(`Unknown operation \`${args.command.trim()}\`. ${hint}`, { exit: 1 });
|
|
76
|
+
}
|
|
77
|
+
this.log(JSON.stringify(match, null, 2));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
14
80
|
class CompletionCommand extends Command {
|
|
15
81
|
static args = {
|
|
16
82
|
shell: Args.string({
|
|
@@ -40,6 +106,11 @@ const generatedCommands = Object.fromEntries(operationManifest.map((operation) =
|
|
|
40
106
|
export const COMMANDS = {
|
|
41
107
|
completion: CompletionCommand,
|
|
42
108
|
"list-operations": ListOperationsCommand,
|
|
109
|
+
// `describe` prints a single operation's full manifest entry
|
|
110
|
+
// (path, request schema, response schema, per-field descriptions).
|
|
111
|
+
// The same data is in `list-operations` but agents don't reach for
|
|
112
|
+
// `list-operations | jq` when they want to clarify a field meaning.
|
|
113
|
+
describe: DescribeCommand,
|
|
43
114
|
// `send` is the agent-grade shortcut for sending:send-email with
|
|
44
115
|
// sensible defaults (auto from-address, auto subject). The full
|
|
45
116
|
// operation stays available under sending:send-email for callers
|
|
@@ -50,5 +121,9 @@ export const COMMANDS = {
|
|
|
50
121
|
// wanting this before risking a real call against a possibly-
|
|
51
122
|
// bad key.
|
|
52
123
|
whoami: WhoamiCommand,
|
|
124
|
+
// `emails:latest` is the inbox-triage shortcut: the most recent N
|
|
125
|
+
// inbound emails as a compact text table. emails:list-emails stays
|
|
126
|
+
// available for the full JSON envelope + cursor pagination.
|
|
127
|
+
"emails:latest": EmailsLatestCommand,
|
|
53
128
|
...generatedCommands,
|
|
54
129
|
};
|
package/dist/openapi/index.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ type PrimitiveOperationManifest = {
|
|
|
37
37
|
* true. `$ref`s into the OpenAPI components are inlined.
|
|
38
38
|
*/
|
|
39
39
|
requestSchema: Record<string, unknown> | null;
|
|
40
|
+
/**
|
|
41
|
+
* Resolved JSON Schema for the 200/201 response body's `data`
|
|
42
|
+
* envelope contents. Same shape as `requestSchema`: `$ref`s
|
|
43
|
+
* inlined. Null on operations without a 200/201 JSON response.
|
|
44
|
+
*/
|
|
45
|
+
responseSchema: Record<string, unknown> | null;
|
|
40
46
|
sdkName: string;
|
|
41
47
|
summary: string | null;
|
|
42
48
|
tag: string;
|