@primitivedotdev/cli 0.30.2 → 0.31.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/oclif/index.js +468 -184
- package/package.json +5 -1
package/dist/oclif/index.js
CHANGED
|
@@ -2413,6 +2413,15 @@ const openapiDocument = {
|
|
|
2413
2413
|
},
|
|
2414
2414
|
"description": "Filter by domain ID."
|
|
2415
2415
|
},
|
|
2416
|
+
{
|
|
2417
|
+
"name": "reply_to_sent_email_id",
|
|
2418
|
+
"in": "query",
|
|
2419
|
+
"schema": {
|
|
2420
|
+
"type": "string",
|
|
2421
|
+
"format": "uuid"
|
|
2422
|
+
},
|
|
2423
|
+
"description": "Filter to inbound emails that are replies to a specific\noutbound send. The value is a `sent_emails.id` (UUID). At\ninbound ingest, Primitive matches the parsed In-Reply-To\nheader (or References as a fallback) against\n`sent_emails.message_id` in the same org and records the\nresolved id on `emails.reply_to_sent_email_id`. This filter\nis the strict-threading lookup behind `primitive chat` and\nany UI that wants to show the inbound reply to a given\nsend. NULL on inbound that isn't a threaded reply to one\nof your sends, so existing emails received before this\ningestion landed will not match.\n"
|
|
2424
|
+
},
|
|
2416
2425
|
{
|
|
2417
2426
|
"name": "status",
|
|
2418
2427
|
"in": "query",
|
|
@@ -4666,6 +4675,11 @@ const openapiDocument = {
|
|
|
4666
4675
|
"type": "array",
|
|
4667
4676
|
"description": "Sent emails recorded as replies to this inbound, in send\norder (ascending). Populated when a customer's send-mail\nrequest carries an `in_reply_to` Message-ID that matches\nthis inbound's `message_id` in the same org. Includes\nattempts that were gate-denied, so the array reflects every\nrecorded reply attempt regardless of outcome.\n",
|
|
4668
4677
|
"items": { "$ref": "#/components/schemas/EmailDetailReply" }
|
|
4678
|
+
},
|
|
4679
|
+
"reply_to_sent_email_id": {
|
|
4680
|
+
"type": ["string", "null"],
|
|
4681
|
+
"format": "uuid",
|
|
4682
|
+
"description": "The `sent_emails.id` of the outbound this inbound was a\nreply to, when resolvable. Set at inbound ingest by\nmatching the parsed In-Reply-To (or References, as a\nfallback) against `sent_emails.message_id` in the same\norg. The mirror of `sent_emails.in_reply_to_email_id` for\nthe inbound side of a thread. NULL when the inbound is\nnot a threaded reply to one of your sends, when neither\nheader survived the path through intermediate MTAs, or on\ninbound received before this auto-link landed.\n"
|
|
4669
4683
|
}
|
|
4670
4684
|
},
|
|
4671
4685
|
"required": [
|
|
@@ -7336,6 +7350,11 @@ const operationManifest = [
|
|
|
7336
7350
|
"created_at"
|
|
7337
7351
|
]
|
|
7338
7352
|
}
|
|
7353
|
+
},
|
|
7354
|
+
"reply_to_sent_email_id": {
|
|
7355
|
+
"type": ["string", "null"],
|
|
7356
|
+
"format": "uuid",
|
|
7357
|
+
"description": "The `sent_emails.id` of the outbound this inbound was a\nreply to, when resolvable. Set at inbound ingest by\nmatching the parsed In-Reply-To (or References, as a\nfallback) against `sent_emails.message_id` in the same\norg. The mirror of `sent_emails.in_reply_to_email_id` for\nthe inbound side of a thread. NULL when the inbound is\nnot a threaded reply to one of your sends, when neither\nheader survived the path through intermediate MTAs, or on\ninbound received before this auto-link landed.\n"
|
|
7339
7358
|
}
|
|
7340
7359
|
},
|
|
7341
7360
|
"required": [
|
|
@@ -7588,6 +7607,13 @@ const operationManifest = [
|
|
|
7588
7607
|
"required": false,
|
|
7589
7608
|
"type": "string"
|
|
7590
7609
|
},
|
|
7610
|
+
{
|
|
7611
|
+
"description": "Filter to inbound emails that are replies to a specific\noutbound send. The value is a `sent_emails.id` (UUID). At\ninbound ingest, Primitive matches the parsed In-Reply-To\nheader (or References as a fallback) against\n`sent_emails.message_id` in the same org and records the\nresolved id on `emails.reply_to_sent_email_id`. This filter\nis the strict-threading lookup behind `primitive chat` and\nany UI that wants to show the inbound reply to a given\nsend. NULL on inbound that isn't a threaded reply to one\nof your sends, so existing emails received before this\ningestion landed will not match.\n",
|
|
7612
|
+
"enum": null,
|
|
7613
|
+
"name": "reply_to_sent_email_id",
|
|
7614
|
+
"required": false,
|
|
7615
|
+
"type": "string"
|
|
7616
|
+
},
|
|
7591
7617
|
{
|
|
7592
7618
|
"description": "Filter by inbound email lifecycle status.",
|
|
7593
7619
|
"enum": null,
|
|
@@ -11062,7 +11088,7 @@ function resolveConfigEnvironment(config) {
|
|
|
11062
11088
|
} : null;
|
|
11063
11089
|
}
|
|
11064
11090
|
function upsertCliEnvironment(params) {
|
|
11065
|
-
const name = normalizeCliEnvironmentName(params.environmentName ??
|
|
11091
|
+
const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
|
|
11066
11092
|
const existing = params.config.environments[name] ?? {};
|
|
11067
11093
|
const nextHeaders = { ...existing.headers ?? {} };
|
|
11068
11094
|
for (const assignment of params.headers ?? []) {
|
|
@@ -11139,6 +11165,7 @@ function resolveCliApiRequestConfig(params) {
|
|
|
11139
11165
|
const currentEnvironment = resolveConfigEnvironment(loadCliConfig(params.configDir));
|
|
11140
11166
|
const configuredApiBaseUrl1 = currentEnvironment?.config.api_base_url_1;
|
|
11141
11167
|
const configuredApiBaseUrl2 = currentEnvironment?.config.api_base_url_2;
|
|
11168
|
+
if (currentEnvironment !== null && currentEnvironment.name !== "default" && params.apiBaseUrl1 === void 0 && configuredApiBaseUrl1 === void 0) throw new Errors.CLIError(`The active Primitive CLI environment \`${currentEnvironment.name}\` does not specify an api_base_url_1. Set one with \`primitive config set --environment ${currentEnvironment.name} --api-base-url-1 https://...\`, or switch to a different environment with \`primitive config use <name>\`. Refusing to fall back to the production default for a non-default environment.`, { exit: 1 });
|
|
11142
11169
|
const apiBaseUrl1 = params.apiBaseUrl1 !== void 0 ? normalizeApiBaseUrl1(params.apiBaseUrl1) : configuredApiBaseUrl1;
|
|
11143
11170
|
const apiBaseUrl2 = params.apiBaseUrl2 !== void 0 ? normalizeApiBaseUrl2(params.apiBaseUrl2) : configuredApiBaseUrl2;
|
|
11144
11171
|
return {
|
|
@@ -11410,26 +11437,26 @@ function coerceParameterValue(parameter, value) {
|
|
|
11410
11437
|
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
|
|
11411
11438
|
throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
|
|
11412
11439
|
}
|
|
11413
|
-
function cliError$
|
|
11440
|
+
function cliError$6(message) {
|
|
11414
11441
|
return new Errors.CLIError(message, { exit: 1 });
|
|
11415
11442
|
}
|
|
11416
11443
|
function parseJson(source, flagLabel) {
|
|
11417
11444
|
try {
|
|
11418
11445
|
return JSON.parse(source);
|
|
11419
11446
|
} catch (error) {
|
|
11420
|
-
throw cliError$
|
|
11447
|
+
throw cliError$6(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
11421
11448
|
}
|
|
11422
11449
|
}
|
|
11423
11450
|
function readJsonBody(flags) {
|
|
11424
11451
|
const bodyFile = flags["body-file"];
|
|
11425
11452
|
const rawBody = flags["raw-body"];
|
|
11426
|
-
if (bodyFile && rawBody) throw cliError$
|
|
11453
|
+
if (bodyFile && rawBody) throw cliError$6("Use either --raw-body or --body-file, not both");
|
|
11427
11454
|
if (typeof bodyFile === "string") {
|
|
11428
11455
|
let contents;
|
|
11429
11456
|
try {
|
|
11430
11457
|
contents = readFileSync(bodyFile, "utf8");
|
|
11431
11458
|
} catch (error) {
|
|
11432
|
-
throw cliError$
|
|
11459
|
+
throw cliError$6(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
11433
11460
|
}
|
|
11434
11461
|
return parseJson(contents, `--body-file ${bodyFile}`);
|
|
11435
11462
|
}
|
|
@@ -11439,7 +11466,7 @@ function readTextFileFlag(path, flagLabel) {
|
|
|
11439
11466
|
try {
|
|
11440
11467
|
return readFileSync(path, "utf8");
|
|
11441
11468
|
} catch (error) {
|
|
11442
|
-
throw cliError$
|
|
11469
|
+
throw cliError$6(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
11443
11470
|
}
|
|
11444
11471
|
}
|
|
11445
11472
|
function extractErrorPayload(raw) {
|
|
@@ -11504,15 +11531,27 @@ function writeErrorWithHints(payload) {
|
|
|
11504
11531
|
}
|
|
11505
11532
|
if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
|
|
11506
11533
|
}
|
|
11507
|
-
|
|
11508
|
-
|
|
11534
|
+
/**
|
|
11535
|
+
* Surface a user-facing hint when a request comes back unauthorized.
|
|
11536
|
+
*
|
|
11537
|
+
* Deliberately does NOT mutate the saved credentials.json. Auto-
|
|
11538
|
+
* deleting on any 401 made transient rejections look like permanent
|
|
11539
|
+
* credential failures and forced unnecessary re-login cycles; we
|
|
11540
|
+
* surface a hint and let the user decide whether to re-authenticate.
|
|
11541
|
+
*
|
|
11542
|
+
* The one legitimate auto-delete case lives in `primitive login`'s
|
|
11543
|
+
* `checkExistingLogin`: the user has explicitly asked to log in,
|
|
11544
|
+
* existing credentials are probed, and if they fail we clean up
|
|
11545
|
+
* before minting a new key. That path calls `deleteCliCredentials`
|
|
11546
|
+
* directly rather than going through this function.
|
|
11547
|
+
*/
|
|
11548
|
+
function surfaceUnauthorizedHint(params) {
|
|
11549
|
+
if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return;
|
|
11509
11550
|
if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl1 !== params.auth.credentials.api_base_url_1) {
|
|
11510
|
-
process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The
|
|
11511
|
-
return
|
|
11551
|
+
process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The saved credential is preserved; unset PRIMITIVE_API_BASE_URL_1, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
|
|
11552
|
+
return;
|
|
11512
11553
|
}
|
|
11513
|
-
|
|
11514
|
-
process.stderr.write("Removed saved Primitive CLI credentials because the backing API key is no longer valid. Run `primitive login` to create a new one.\n");
|
|
11515
|
-
return true;
|
|
11554
|
+
process.stderr.write("Your saved Primitive CLI credential was rejected. If the command was working a moment ago, please retry; brief retries often clear transient rejections. If it keeps failing, run `primitive logout && primitive login` to mint a fresh credential.\n");
|
|
11516
11555
|
}
|
|
11517
11556
|
function formatElapsed(ms) {
|
|
11518
11557
|
const seconds = ms / 1e3;
|
|
@@ -11676,7 +11715,7 @@ function createOperationCommand(operation) {
|
|
|
11676
11715
|
if (result.error) {
|
|
11677
11716
|
const errorPayload = extractErrorPayload(result.error);
|
|
11678
11717
|
writeErrorWithHints(errorPayload);
|
|
11679
|
-
|
|
11718
|
+
surfaceUnauthorizedHint({
|
|
11680
11719
|
auth,
|
|
11681
11720
|
baseUrlOverridden,
|
|
11682
11721
|
configDir: this.config.configDir,
|
|
@@ -11737,6 +11776,384 @@ function canonicalizeCliReferences(description) {
|
|
|
11737
11776
|
return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
|
|
11738
11777
|
}
|
|
11739
11778
|
//#endregion
|
|
11779
|
+
//#region src/oclif/outbound-defaults.ts
|
|
11780
|
+
const SUBJECT_MAX_LENGTH = 200;
|
|
11781
|
+
function deriveSubject(body) {
|
|
11782
|
+
for (const line of body.split("\n")) {
|
|
11783
|
+
const trimmed = line.trim();
|
|
11784
|
+
if (!trimmed) continue;
|
|
11785
|
+
return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
|
|
11786
|
+
}
|
|
11787
|
+
return "Message";
|
|
11788
|
+
}
|
|
11789
|
+
function isVerifiedDomain(domain) {
|
|
11790
|
+
return domain.is_active === true;
|
|
11791
|
+
}
|
|
11792
|
+
async function pickDefaultFromAddress(apiClient, authFailureContext) {
|
|
11793
|
+
const result = await listDomains({
|
|
11794
|
+
client: apiClient.client,
|
|
11795
|
+
responseStyle: "fields"
|
|
11796
|
+
});
|
|
11797
|
+
if (result.error) {
|
|
11798
|
+
const errorPayload = extractErrorPayload(result.error);
|
|
11799
|
+
if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
|
|
11800
|
+
writeErrorWithHints(errorPayload);
|
|
11801
|
+
surfaceUnauthorizedHint({
|
|
11802
|
+
...authFailureContext,
|
|
11803
|
+
payload: errorPayload
|
|
11804
|
+
});
|
|
11805
|
+
throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
|
|
11806
|
+
}
|
|
11807
|
+
throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
|
|
11808
|
+
}
|
|
11809
|
+
const first = result.data?.data?.find(isVerifiedDomain);
|
|
11810
|
+
if (!first) 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` and verify it.");
|
|
11811
|
+
return `agent@${first.domain}`;
|
|
11812
|
+
}
|
|
11813
|
+
//#endregion
|
|
11814
|
+
//#region src/oclif/commands/emails-poll.ts
|
|
11815
|
+
function quoteDslValue(value) {
|
|
11816
|
+
if (/^[^\s"]+$/.test(value)) return value;
|
|
11817
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
11818
|
+
}
|
|
11819
|
+
function combineQ(q, domain) {
|
|
11820
|
+
const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
|
|
11821
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
11822
|
+
}
|
|
11823
|
+
function normalizeIsoDate(value, label) {
|
|
11824
|
+
const parsed = new Date(value);
|
|
11825
|
+
if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
|
|
11826
|
+
return parsed.toISOString();
|
|
11827
|
+
}
|
|
11828
|
+
function filtersFromFlags(flags) {
|
|
11829
|
+
return {
|
|
11830
|
+
body: flags.body,
|
|
11831
|
+
domain: flags.domain,
|
|
11832
|
+
domainId: flags["domain-id"],
|
|
11833
|
+
from: flags.from,
|
|
11834
|
+
hasAttachment: flags["has-attachment"],
|
|
11835
|
+
q: flags.q,
|
|
11836
|
+
replyToSentEmailId: flags["reply-to-sent-email-id"],
|
|
11837
|
+
spamScoreGte: flags["spam-score-gte"],
|
|
11838
|
+
spamScoreLt: flags["spam-score-lt"],
|
|
11839
|
+
subject: flags.subject,
|
|
11840
|
+
to: flags.to
|
|
11841
|
+
};
|
|
11842
|
+
}
|
|
11843
|
+
function sinceFromFlags(flags) {
|
|
11844
|
+
if (flags.since) return normalizeIsoDate(flags.since, "--since");
|
|
11845
|
+
return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
|
|
11846
|
+
}
|
|
11847
|
+
function buildEmailSearchQuery(params) {
|
|
11848
|
+
const query = {
|
|
11849
|
+
include_facets: "false",
|
|
11850
|
+
limit: params.pageSize,
|
|
11851
|
+
snippet: "false",
|
|
11852
|
+
sort: "received_at_asc"
|
|
11853
|
+
};
|
|
11854
|
+
const q = combineQ(params.filters.q, params.filters.domain);
|
|
11855
|
+
if (q) query.q = q;
|
|
11856
|
+
if (params.filters.body) query.body = params.filters.body;
|
|
11857
|
+
if (params.filters.domainId) query.domain_id = params.filters.domainId;
|
|
11858
|
+
if (params.filters.from) query.from = params.filters.from;
|
|
11859
|
+
if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
|
|
11860
|
+
if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
|
|
11861
|
+
if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
|
|
11862
|
+
if (params.filters.replyToSentEmailId) query.reply_to_sent_email_id = params.filters.replyToSentEmailId;
|
|
11863
|
+
if (params.filters.subject) query.subject = params.filters.subject;
|
|
11864
|
+
if (params.filters.to) query.to = params.filters.to;
|
|
11865
|
+
if (params.since) query.date_from = params.since;
|
|
11866
|
+
if (params.cursor) query.cursor = params.cursor;
|
|
11867
|
+
return query;
|
|
11868
|
+
}
|
|
11869
|
+
function encodeReceivedAtSearchCursor(email) {
|
|
11870
|
+
const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
|
|
11871
|
+
return Buffer.from(raw, "utf8").toString("base64url");
|
|
11872
|
+
}
|
|
11873
|
+
function cursorFromRows(rows) {
|
|
11874
|
+
const last = rows.at(-1);
|
|
11875
|
+
return last ? encodeReceivedAtSearchCursor(last) : null;
|
|
11876
|
+
}
|
|
11877
|
+
function collectNewAcceptedEmails(rows, seenIds) {
|
|
11878
|
+
const fresh = [];
|
|
11879
|
+
for (const row of rows) {
|
|
11880
|
+
if (row.status !== "accepted" && row.status !== "completed") continue;
|
|
11881
|
+
if (seenIds.has(row.id)) continue;
|
|
11882
|
+
seenIds.add(row.id);
|
|
11883
|
+
fresh.push(row);
|
|
11884
|
+
}
|
|
11885
|
+
return fresh;
|
|
11886
|
+
}
|
|
11887
|
+
async function fetchEmailSearchPage(params) {
|
|
11888
|
+
const result = await searchEmails({
|
|
11889
|
+
client: params.apiClient.client,
|
|
11890
|
+
query: buildEmailSearchQuery({
|
|
11891
|
+
cursor: params.cursor,
|
|
11892
|
+
filters: params.filters,
|
|
11893
|
+
pageSize: params.pageSize,
|
|
11894
|
+
since: params.since
|
|
11895
|
+
}),
|
|
11896
|
+
responseStyle: "fields"
|
|
11897
|
+
});
|
|
11898
|
+
if (result.error) return {
|
|
11899
|
+
ok: false,
|
|
11900
|
+
error: result.error
|
|
11901
|
+
};
|
|
11902
|
+
const envelope = result.data;
|
|
11903
|
+
const rows = envelope?.data ?? [];
|
|
11904
|
+
return {
|
|
11905
|
+
ok: true,
|
|
11906
|
+
cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
|
|
11907
|
+
rows
|
|
11908
|
+
};
|
|
11909
|
+
}
|
|
11910
|
+
function sleep$1(ms) {
|
|
11911
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11912
|
+
}
|
|
11913
|
+
//#endregion
|
|
11914
|
+
//#region src/oclif/commands/chat.ts
|
|
11915
|
+
const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
|
|
11916
|
+
const DEFAULT_STRICT_PHASE_SECONDS = 60;
|
|
11917
|
+
function cliError$5(message) {
|
|
11918
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
11919
|
+
}
|
|
11920
|
+
async function readStdinToString() {
|
|
11921
|
+
if (process.stdin.isTTY) throw cliError$5("No message provided. Pass the message as the second positional argument or pipe it via stdin.");
|
|
11922
|
+
const chunks = [];
|
|
11923
|
+
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
11924
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
11925
|
+
}
|
|
11926
|
+
var ChatCommand = class ChatCommand extends Command {
|
|
11927
|
+
static description = `Send a message to an address and wait for the reply.
|
|
11928
|
+
|
|
11929
|
+
This is the first-party verb for talking to agents that live behind
|
|
11930
|
+
email addresses. \`primitive send\` is transport (fire-and-forget);
|
|
11931
|
+
\`primitive chat\` is semantic (send + wait for the threaded reply).
|
|
11932
|
+
|
|
11933
|
+
The message body can be given as the second positional argument or
|
|
11934
|
+
piped via stdin. The reply body is written to stdout; --json emits a
|
|
11935
|
+
structured envelope with both sides of the exchange.
|
|
11936
|
+
|
|
11937
|
+
Matching the reply: the wait phase polls inbound mail filtered by
|
|
11938
|
+
the recipient as sender and the send time as a lower bound. The
|
|
11939
|
+
first match is taken; the full inbound row is then fetched for the
|
|
11940
|
+
body. Exits non-zero on timeout.`;
|
|
11941
|
+
static summary = "Chat with an agent over email (send and wait for the reply)";
|
|
11942
|
+
static examples = [
|
|
11943
|
+
"<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
|
|
11944
|
+
"cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
|
|
11945
|
+
"<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
|
|
11946
|
+
"<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
|
|
11947
|
+
];
|
|
11948
|
+
static args = {
|
|
11949
|
+
recipient: Args.string({
|
|
11950
|
+
description: "Address to chat with (e.g. help@agent.acme.dev).",
|
|
11951
|
+
required: true
|
|
11952
|
+
}),
|
|
11953
|
+
message: Args.string({ description: "Message body. If omitted, read from stdin." })
|
|
11954
|
+
};
|
|
11955
|
+
static flags = {
|
|
11956
|
+
"api-key": Flags.string({
|
|
11957
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
11958
|
+
env: "PRIMITIVE_API_KEY"
|
|
11959
|
+
}),
|
|
11960
|
+
"api-base-url-1": Flags.string({
|
|
11961
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
11962
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
11963
|
+
hidden: true
|
|
11964
|
+
}),
|
|
11965
|
+
"api-base-url-2": Flags.string({
|
|
11966
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
11967
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
11968
|
+
hidden: true
|
|
11969
|
+
}),
|
|
11970
|
+
from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
|
|
11971
|
+
subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
|
|
11972
|
+
"in-reply-to": Flags.string({ description: "Message-Id of the parent email to thread this against. Use when continuing a prior conversation from outside the CLI; for an inbound you received via Primitive, prefer `primitive reply --id <inbound-id>`." }),
|
|
11973
|
+
json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply } on stdout instead of just the reply body." }),
|
|
11974
|
+
timeout: Flags.integer({
|
|
11975
|
+
default: DEFAULT_CHAT_TIMEOUT_SECONDS,
|
|
11976
|
+
description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
|
|
11977
|
+
min: 0
|
|
11978
|
+
}),
|
|
11979
|
+
"strict-phase-seconds": Flags.integer({
|
|
11980
|
+
default: DEFAULT_STRICT_PHASE_SECONDS,
|
|
11981
|
+
description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
|
|
11982
|
+
min: 1
|
|
11983
|
+
}),
|
|
11984
|
+
"strict-only": Flags.boolean({ description: "Disable the time-window fallback. Only accept inbounds whose threading headers (In-Reply-To / References) resolve to this send. Recommended when correctness matters more than success rate (e.g. agents talking to agents)." }),
|
|
11985
|
+
interval: Flags.integer({
|
|
11986
|
+
default: 2,
|
|
11987
|
+
description: "Seconds between polls while waiting for the reply.",
|
|
11988
|
+
min: 1
|
|
11989
|
+
}),
|
|
11990
|
+
"page-size": Flags.integer({
|
|
11991
|
+
default: 50,
|
|
11992
|
+
description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
|
|
11993
|
+
max: 100,
|
|
11994
|
+
min: 1,
|
|
11995
|
+
hidden: true
|
|
11996
|
+
}),
|
|
11997
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
11998
|
+
};
|
|
11999
|
+
async run() {
|
|
12000
|
+
const { args, flags } = await this.parse(ChatCommand);
|
|
12001
|
+
const message = args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
|
|
12002
|
+
if (!message.trim()) throw cliError$5("Message body is empty.");
|
|
12003
|
+
await runWithTiming(flags.time, async () => {
|
|
12004
|
+
const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
|
|
12005
|
+
apiKey: flags["api-key"],
|
|
12006
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
12007
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
12008
|
+
configDir: this.config.configDir
|
|
12009
|
+
});
|
|
12010
|
+
const authFailureContext = {
|
|
12011
|
+
auth,
|
|
12012
|
+
baseUrlOverridden,
|
|
12013
|
+
configDir: this.config.configDir
|
|
12014
|
+
};
|
|
12015
|
+
const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
|
|
12016
|
+
const subject = flags.subject ?? deriveSubject(message);
|
|
12017
|
+
const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
12018
|
+
const sendResult = await sendEmail({
|
|
12019
|
+
body: {
|
|
12020
|
+
from,
|
|
12021
|
+
to: args.recipient,
|
|
12022
|
+
subject,
|
|
12023
|
+
body_text: message,
|
|
12024
|
+
...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {}
|
|
12025
|
+
},
|
|
12026
|
+
client: apiClient._sendClient,
|
|
12027
|
+
responseStyle: "fields"
|
|
12028
|
+
});
|
|
12029
|
+
if (sendResult.error) {
|
|
12030
|
+
const errorPayload = extractErrorPayload(sendResult.error);
|
|
12031
|
+
writeErrorWithHints(errorPayload);
|
|
12032
|
+
surfaceUnauthorizedHint({
|
|
12033
|
+
...authFailureContext,
|
|
12034
|
+
payload: errorPayload
|
|
12035
|
+
});
|
|
12036
|
+
process.exitCode = 1;
|
|
12037
|
+
return;
|
|
12038
|
+
}
|
|
12039
|
+
const sent = sendResult.data?.data;
|
|
12040
|
+
if (!sent) throw cliError$5("Send succeeded but the API returned no data.");
|
|
12041
|
+
const reply = await waitForReply({
|
|
12042
|
+
apiClient,
|
|
12043
|
+
authFailureContext,
|
|
12044
|
+
from,
|
|
12045
|
+
interval: flags.interval,
|
|
12046
|
+
pageSize: flags["page-size"],
|
|
12047
|
+
recipient: args.recipient,
|
|
12048
|
+
sentAtIso,
|
|
12049
|
+
sentId: sent.id,
|
|
12050
|
+
strictOnly: flags["strict-only"],
|
|
12051
|
+
strictPhaseSeconds: flags["strict-phase-seconds"],
|
|
12052
|
+
timeoutSeconds: flags.timeout
|
|
12053
|
+
});
|
|
12054
|
+
if (reply === null) {
|
|
12055
|
+
process.stderr.write(`Timed out after ${flags.timeout}s waiting for a reply from ${args.recipient}.\n`);
|
|
12056
|
+
process.exitCode = 1;
|
|
12057
|
+
return;
|
|
12058
|
+
}
|
|
12059
|
+
if (flags.json) {
|
|
12060
|
+
const envelope = {
|
|
12061
|
+
sent,
|
|
12062
|
+
reply
|
|
12063
|
+
};
|
|
12064
|
+
this.log(JSON.stringify(envelope, null, 2));
|
|
12065
|
+
} else {
|
|
12066
|
+
const body = reply.body_text ?? reply.body_html ?? "";
|
|
12067
|
+
this.log(body);
|
|
12068
|
+
}
|
|
12069
|
+
});
|
|
12070
|
+
}
|
|
12071
|
+
};
|
|
12072
|
+
async function waitForReply(params) {
|
|
12073
|
+
const totalDeadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
|
|
12074
|
+
const strictDeadlineFromBudget = Date.now() + params.strictPhaseSeconds * 1e3;
|
|
12075
|
+
const strictDeadline = params.strictOnly ? totalDeadline : totalDeadline === null ? strictDeadlineFromBudget : Math.min(strictDeadlineFromBudget, totalDeadline);
|
|
12076
|
+
const phases = [{
|
|
12077
|
+
label: "strict",
|
|
12078
|
+
filters: { replyToSentEmailId: params.sentId },
|
|
12079
|
+
deadline: strictDeadline
|
|
12080
|
+
}];
|
|
12081
|
+
if (!params.strictOnly) phases.push({
|
|
12082
|
+
label: "fallback",
|
|
12083
|
+
filters: {
|
|
12084
|
+
from: params.recipient,
|
|
12085
|
+
to: params.from
|
|
12086
|
+
},
|
|
12087
|
+
deadline: totalDeadline
|
|
12088
|
+
});
|
|
12089
|
+
let strictFilterUnsupported = false;
|
|
12090
|
+
for (const phase of phases) {
|
|
12091
|
+
if (phase.label === "strict" && strictFilterUnsupported) continue;
|
|
12092
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
12093
|
+
let cursor = null;
|
|
12094
|
+
while (true) {
|
|
12095
|
+
if (phase.deadline !== null && Date.now() >= phase.deadline) break;
|
|
12096
|
+
const page = await fetchEmailSearchPage({
|
|
12097
|
+
apiClient: params.apiClient,
|
|
12098
|
+
cursor,
|
|
12099
|
+
filters: phase.filters,
|
|
12100
|
+
pageSize: params.pageSize,
|
|
12101
|
+
since: params.sentAtIso
|
|
12102
|
+
});
|
|
12103
|
+
if (!page.ok) {
|
|
12104
|
+
const payload = extractErrorPayload(page.error);
|
|
12105
|
+
writeErrorWithHints(payload);
|
|
12106
|
+
surfaceUnauthorizedHint({
|
|
12107
|
+
...params.authFailureContext,
|
|
12108
|
+
payload
|
|
12109
|
+
});
|
|
12110
|
+
throw new Errors.CLIError("Failed to poll for reply.", { exit: 1 });
|
|
12111
|
+
}
|
|
12112
|
+
let lastAccepted;
|
|
12113
|
+
for (let i = page.rows.length - 1; i >= 0; i--) {
|
|
12114
|
+
const row = page.rows[i];
|
|
12115
|
+
if (row.status === "accepted" || row.status === "completed") {
|
|
12116
|
+
lastAccepted = row;
|
|
12117
|
+
break;
|
|
12118
|
+
}
|
|
12119
|
+
}
|
|
12120
|
+
if (lastAccepted) cursor = encodeReceivedAtSearchCursor(lastAccepted);
|
|
12121
|
+
const matches = collectNewAcceptedEmails(page.rows, seenIds);
|
|
12122
|
+
for (const match of matches) {
|
|
12123
|
+
const full = await getEmail({
|
|
12124
|
+
client: params.apiClient.client,
|
|
12125
|
+
path: { id: match.id },
|
|
12126
|
+
responseStyle: "fields"
|
|
12127
|
+
});
|
|
12128
|
+
if (full.error) {
|
|
12129
|
+
const payload = extractErrorPayload(full.error);
|
|
12130
|
+
writeErrorWithHints(payload);
|
|
12131
|
+
surfaceUnauthorizedHint({
|
|
12132
|
+
...params.authFailureContext,
|
|
12133
|
+
payload
|
|
12134
|
+
});
|
|
12135
|
+
throw new Errors.CLIError(`Reply landed but fetching the full body failed (id=${match.id}).`, { exit: 1 });
|
|
12136
|
+
}
|
|
12137
|
+
const envelope = full.data;
|
|
12138
|
+
const detail = envelope?.data ?? envelope ?? null;
|
|
12139
|
+
if (!detail) throw new Errors.CLIError(`Reply landed but the email body could not be loaded (id=${match.id}).`, { exit: 1 });
|
|
12140
|
+
if (phase.label === "strict" && detail.reply_to_sent_email_id !== params.sentId) {
|
|
12141
|
+
if (!strictFilterUnsupported) process.stderr.write(params.strictOnly ? "Strict-phase reply matching is not supported by this Primitive API host; --strict-only requires server support so the command will exit without a match.\n" : "Strict-phase reply matching is not supported by this Primitive API host; falling back to time-window matching.\n");
|
|
12142
|
+
strictFilterUnsupported = true;
|
|
12143
|
+
continue;
|
|
12144
|
+
}
|
|
12145
|
+
return detail;
|
|
12146
|
+
}
|
|
12147
|
+
if (strictFilterUnsupported && phase.label === "strict") break;
|
|
12148
|
+
if (lastAccepted !== void 0) continue;
|
|
12149
|
+
if (phase.deadline !== null && Date.now() >= phase.deadline) break;
|
|
12150
|
+
if (totalDeadline !== null && Date.now() >= totalDeadline) return null;
|
|
12151
|
+
await sleep$1(params.interval * 1e3);
|
|
12152
|
+
}
|
|
12153
|
+
}
|
|
12154
|
+
return null;
|
|
12155
|
+
}
|
|
12156
|
+
//#endregion
|
|
11740
12157
|
//#region src/oclif/commands/config.ts
|
|
11741
12158
|
function loadOrCreateConfig(configDir) {
|
|
11742
12159
|
return loadCliConfig(configDir) ?? emptyCliConfig();
|
|
@@ -11748,8 +12165,7 @@ function redactConfig(config) {
|
|
|
11748
12165
|
};
|
|
11749
12166
|
}
|
|
11750
12167
|
var ConfigSetCommand = class ConfigSetCommand extends Command {
|
|
11751
|
-
static
|
|
11752
|
-
static summary = "Set hidden Primitive CLI request config";
|
|
12168
|
+
static summary = "Set a Primitive CLI request environment";
|
|
11753
12169
|
static flags = {
|
|
11754
12170
|
environment: Flags.string({
|
|
11755
12171
|
char: "e",
|
|
@@ -11783,8 +12199,7 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
|
|
|
11783
12199
|
}
|
|
11784
12200
|
};
|
|
11785
12201
|
var ConfigUseCommand = class ConfigUseCommand extends Command {
|
|
11786
|
-
static
|
|
11787
|
-
static summary = "Switch active Primitive CLI request config";
|
|
12202
|
+
static summary = "Switch the active Primitive CLI request environment";
|
|
11788
12203
|
static args = { environment: Args.string({
|
|
11789
12204
|
description: "Environment name to use",
|
|
11790
12205
|
required: true
|
|
@@ -11802,8 +12217,7 @@ var ConfigUseCommand = class ConfigUseCommand extends Command {
|
|
|
11802
12217
|
}
|
|
11803
12218
|
};
|
|
11804
12219
|
var ConfigListCommand = class ConfigListCommand extends Command {
|
|
11805
|
-
static
|
|
11806
|
-
static summary = "List hidden Primitive CLI request configs";
|
|
12220
|
+
static summary = "List Primitive CLI request environments";
|
|
11807
12221
|
static flags = {
|
|
11808
12222
|
json: Flags.boolean({ description: "Print JSON" }),
|
|
11809
12223
|
"show-secrets": Flags.boolean({ description: "Show header values instead of redacting them" })
|
|
@@ -11833,8 +12247,7 @@ var ConfigListCommand = class ConfigListCommand extends Command {
|
|
|
11833
12247
|
}
|
|
11834
12248
|
};
|
|
11835
12249
|
var ConfigResetCommand = class ConfigResetCommand extends Command {
|
|
11836
|
-
static
|
|
11837
|
-
static summary = "Reset hidden Primitive CLI request config";
|
|
12250
|
+
static summary = "Reset Primitive CLI request environments";
|
|
11838
12251
|
static flags = { environment: Flags.string({
|
|
11839
12252
|
char: "e",
|
|
11840
12253
|
description: "Only remove one environment"
|
|
@@ -11858,6 +12271,11 @@ var ConfigResetCommand = class ConfigResetCommand extends Command {
|
|
|
11858
12271
|
process.stderr.write(`Primitive CLI environment ${environment} removed.\n`);
|
|
11859
12272
|
}
|
|
11860
12273
|
};
|
|
12274
|
+
var ConfigCommand = class extends ConfigListCommand {
|
|
12275
|
+
static hidden = true;
|
|
12276
|
+
static summary = "Manage Primitive CLI request environments";
|
|
12277
|
+
static description = "Manage local Primitive CLI request environments for API endpoint overrides and request headers.";
|
|
12278
|
+
};
|
|
11861
12279
|
//#endregion
|
|
11862
12280
|
//#region src/oclif/commands/doctor.ts
|
|
11863
12281
|
const MIN_NODE_MAJOR = 22;
|
|
@@ -12196,7 +12614,7 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
|
|
|
12196
12614
|
if (result.error) {
|
|
12197
12615
|
const errorPayload = extractErrorPayload(result.error);
|
|
12198
12616
|
writeErrorWithHints(errorPayload);
|
|
12199
|
-
|
|
12617
|
+
surfaceUnauthorizedHint({
|
|
12200
12618
|
auth,
|
|
12201
12619
|
baseUrlOverridden,
|
|
12202
12620
|
configDir: this.config.configDir,
|
|
@@ -12222,104 +12640,6 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
|
|
|
12222
12640
|
}
|
|
12223
12641
|
};
|
|
12224
12642
|
//#endregion
|
|
12225
|
-
//#region src/oclif/commands/emails-poll.ts
|
|
12226
|
-
function quoteDslValue(value) {
|
|
12227
|
-
if (/^[^\s"]+$/.test(value)) return value;
|
|
12228
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
12229
|
-
}
|
|
12230
|
-
function combineQ(q, domain) {
|
|
12231
|
-
const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
|
|
12232
|
-
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
12233
|
-
}
|
|
12234
|
-
function normalizeIsoDate(value, label) {
|
|
12235
|
-
const parsed = new Date(value);
|
|
12236
|
-
if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
|
|
12237
|
-
return parsed.toISOString();
|
|
12238
|
-
}
|
|
12239
|
-
function filtersFromFlags(flags) {
|
|
12240
|
-
return {
|
|
12241
|
-
body: flags.body,
|
|
12242
|
-
domain: flags.domain,
|
|
12243
|
-
domainId: flags["domain-id"],
|
|
12244
|
-
from: flags.from,
|
|
12245
|
-
hasAttachment: flags["has-attachment"],
|
|
12246
|
-
q: flags.q,
|
|
12247
|
-
spamScoreGte: flags["spam-score-gte"],
|
|
12248
|
-
spamScoreLt: flags["spam-score-lt"],
|
|
12249
|
-
subject: flags.subject,
|
|
12250
|
-
to: flags.to
|
|
12251
|
-
};
|
|
12252
|
-
}
|
|
12253
|
-
function sinceFromFlags(flags) {
|
|
12254
|
-
if (flags.since) return normalizeIsoDate(flags.since, "--since");
|
|
12255
|
-
return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
|
|
12256
|
-
}
|
|
12257
|
-
function buildEmailSearchQuery(params) {
|
|
12258
|
-
const query = {
|
|
12259
|
-
include_facets: "false",
|
|
12260
|
-
limit: params.pageSize,
|
|
12261
|
-
snippet: "false",
|
|
12262
|
-
sort: "received_at_asc"
|
|
12263
|
-
};
|
|
12264
|
-
const q = combineQ(params.filters.q, params.filters.domain);
|
|
12265
|
-
if (q) query.q = q;
|
|
12266
|
-
if (params.filters.body) query.body = params.filters.body;
|
|
12267
|
-
if (params.filters.domainId) query.domain_id = params.filters.domainId;
|
|
12268
|
-
if (params.filters.from) query.from = params.filters.from;
|
|
12269
|
-
if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
|
|
12270
|
-
if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
|
|
12271
|
-
if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
|
|
12272
|
-
if (params.filters.subject) query.subject = params.filters.subject;
|
|
12273
|
-
if (params.filters.to) query.to = params.filters.to;
|
|
12274
|
-
if (params.since) query.date_from = params.since;
|
|
12275
|
-
if (params.cursor) query.cursor = params.cursor;
|
|
12276
|
-
return query;
|
|
12277
|
-
}
|
|
12278
|
-
function encodeReceivedAtSearchCursor(email) {
|
|
12279
|
-
const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
|
|
12280
|
-
return Buffer.from(raw, "utf8").toString("base64url");
|
|
12281
|
-
}
|
|
12282
|
-
function cursorFromRows(rows) {
|
|
12283
|
-
const last = rows.at(-1);
|
|
12284
|
-
return last ? encodeReceivedAtSearchCursor(last) : null;
|
|
12285
|
-
}
|
|
12286
|
-
function collectNewAcceptedEmails(rows, seenIds) {
|
|
12287
|
-
const fresh = [];
|
|
12288
|
-
for (const row of rows) {
|
|
12289
|
-
if (row.status !== "accepted" && row.status !== "completed") continue;
|
|
12290
|
-
if (seenIds.has(row.id)) continue;
|
|
12291
|
-
seenIds.add(row.id);
|
|
12292
|
-
fresh.push(row);
|
|
12293
|
-
}
|
|
12294
|
-
return fresh;
|
|
12295
|
-
}
|
|
12296
|
-
async function fetchEmailSearchPage(params) {
|
|
12297
|
-
const result = await searchEmails({
|
|
12298
|
-
client: params.apiClient.client,
|
|
12299
|
-
query: buildEmailSearchQuery({
|
|
12300
|
-
cursor: params.cursor,
|
|
12301
|
-
filters: params.filters,
|
|
12302
|
-
pageSize: params.pageSize,
|
|
12303
|
-
since: params.since
|
|
12304
|
-
}),
|
|
12305
|
-
responseStyle: "fields"
|
|
12306
|
-
});
|
|
12307
|
-
if (result.error) return {
|
|
12308
|
-
ok: false,
|
|
12309
|
-
error: result.error
|
|
12310
|
-
};
|
|
12311
|
-
const envelope = result.data;
|
|
12312
|
-
const rows = envelope?.data ?? [];
|
|
12313
|
-
return {
|
|
12314
|
-
ok: true,
|
|
12315
|
-
cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
|
|
12316
|
-
rows
|
|
12317
|
-
};
|
|
12318
|
-
}
|
|
12319
|
-
function sleep$1(ms) {
|
|
12320
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12321
|
-
}
|
|
12322
|
-
//#endregion
|
|
12323
12643
|
//#region src/oclif/commands/emails-wait.ts
|
|
12324
12644
|
const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
|
|
12325
12645
|
function cliError$4(message) {
|
|
@@ -12372,6 +12692,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
12372
12692
|
min: 1
|
|
12373
12693
|
}),
|
|
12374
12694
|
q: Flags.string({ description: "Full-text search DSL query" }),
|
|
12695
|
+
"reply-to-sent-email-id": Flags.string({ description: "Filter to inbound emails that are threaded replies to a specific outbound send (UUID from a /v1/send-mail response). Combine with --to and --since for the strictest version of the wait-for-reply pattern." }),
|
|
12375
12696
|
since: Flags.string({ description: "Only match emails received on or after this date/time" }),
|
|
12376
12697
|
"spam-score-gte": Flags.integer({ description: "Only match emails with spam score greater than or equal to this value" }),
|
|
12377
12698
|
"spam-score-lt": Flags.integer({ description: "Only match emails with spam score below this value" }),
|
|
@@ -12416,7 +12737,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
12416
12737
|
if (!page.ok) {
|
|
12417
12738
|
const payload = extractErrorPayload(page.error);
|
|
12418
12739
|
writeErrorWithHints(payload);
|
|
12419
|
-
|
|
12740
|
+
surfaceUnauthorizedHint({
|
|
12420
12741
|
auth,
|
|
12421
12742
|
baseUrlOverridden,
|
|
12422
12743
|
configDir: this.config.configDir,
|
|
@@ -12538,7 +12859,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
|
|
|
12538
12859
|
if (!page.ok) {
|
|
12539
12860
|
const payload = extractErrorPayload(page.error);
|
|
12540
12861
|
writeErrorWithHints(payload);
|
|
12541
|
-
|
|
12862
|
+
surfaceUnauthorizedHint({
|
|
12542
12863
|
auth,
|
|
12543
12864
|
baseUrlOverridden,
|
|
12544
12865
|
configDir: this.config.configDir,
|
|
@@ -13260,7 +13581,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
13260
13581
|
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
|
|
13261
13582
|
}
|
|
13262
13583
|
writeErrorWithHints(outcome.payload);
|
|
13263
|
-
|
|
13584
|
+
surfaceUnauthorizedHint({
|
|
13264
13585
|
...authFailureContext,
|
|
13265
13586
|
payload: outcome.payload
|
|
13266
13587
|
});
|
|
@@ -13283,7 +13604,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
13283
13604
|
});
|
|
13284
13605
|
if (waitResult.kind === "error") {
|
|
13285
13606
|
writeErrorWithHints(waitResult.payload);
|
|
13286
|
-
|
|
13607
|
+
surfaceUnauthorizedHint({
|
|
13287
13608
|
...authFailureContext,
|
|
13288
13609
|
payload: waitResult.payload
|
|
13289
13610
|
});
|
|
@@ -13316,8 +13637,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
13316
13637
|
name: "Primitive Team",
|
|
13317
13638
|
url: "https://primitive.dev"
|
|
13318
13639
|
};
|
|
13319
|
-
const SDK_VERSION_RANGE = "^0.
|
|
13320
|
-
const CLI_VERSION_RANGE = "^0.
|
|
13640
|
+
const SDK_VERSION_RANGE = "^0.31.0";
|
|
13641
|
+
const CLI_VERSION_RANGE = "^0.31.0";
|
|
13321
13642
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
13322
13643
|
function renderHandler() {
|
|
13323
13644
|
return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
|
|
@@ -13827,7 +14148,7 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
|
|
|
13827
14148
|
if (result.error) {
|
|
13828
14149
|
const errorPayload = extractErrorPayload(result.error);
|
|
13829
14150
|
writeErrorWithHints(errorPayload);
|
|
13830
|
-
|
|
14151
|
+
surfaceUnauthorizedHint({
|
|
13831
14152
|
auth,
|
|
13832
14153
|
baseUrlOverridden,
|
|
13833
14154
|
configDir: this.config.configDir,
|
|
@@ -14083,7 +14404,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
14083
14404
|
process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
|
|
14084
14405
|
}
|
|
14085
14406
|
writeErrorWithHints(outcome.payload);
|
|
14086
|
-
|
|
14407
|
+
surfaceUnauthorizedHint({
|
|
14087
14408
|
...authFailureContext,
|
|
14088
14409
|
payload: outcome.payload
|
|
14089
14410
|
});
|
|
@@ -14105,7 +14426,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
14105
14426
|
});
|
|
14106
14427
|
if (waitResult.kind === "error") {
|
|
14107
14428
|
writeErrorWithHints(waitResult.payload);
|
|
14108
|
-
|
|
14429
|
+
surfaceUnauthorizedHint({
|
|
14109
14430
|
...authFailureContext,
|
|
14110
14431
|
payload: waitResult.payload
|
|
14111
14432
|
});
|
|
@@ -14306,7 +14627,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
14306
14627
|
if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions redeploy --id <id> --file <bundle>` once you have the bundle.\n");
|
|
14307
14628
|
else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
|
|
14308
14629
|
writeErrorWithHints(outcome.payload);
|
|
14309
|
-
|
|
14630
|
+
surfaceUnauthorizedHint({
|
|
14310
14631
|
...authFailureContext,
|
|
14311
14632
|
payload: outcome.payload
|
|
14312
14633
|
});
|
|
@@ -14494,7 +14815,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
14494
14815
|
if (triggerResult.error) {
|
|
14495
14816
|
const payload = extractErrorPayload(triggerResult.error);
|
|
14496
14817
|
writeErrorWithHints(payload);
|
|
14497
|
-
|
|
14818
|
+
surfaceUnauthorizedHint({
|
|
14498
14819
|
auth,
|
|
14499
14820
|
baseUrlOverridden,
|
|
14500
14821
|
configDir: this.config.configDir,
|
|
@@ -14532,7 +14853,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
14532
14853
|
if (!page.ok) {
|
|
14533
14854
|
const payload = extractErrorPayload(page.error);
|
|
14534
14855
|
writeErrorWithHints(payload);
|
|
14535
|
-
|
|
14856
|
+
surfaceUnauthorizedHint({
|
|
14536
14857
|
auth,
|
|
14537
14858
|
baseUrlOverridden,
|
|
14538
14859
|
configDir: this.config.configDir,
|
|
@@ -14560,7 +14881,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
14560
14881
|
if (result.error) {
|
|
14561
14882
|
const payload = extractErrorPayload(result.error);
|
|
14562
14883
|
writeErrorWithHints(payload);
|
|
14563
|
-
|
|
14884
|
+
surfaceUnauthorizedHint({
|
|
14564
14885
|
auth,
|
|
14565
14886
|
baseUrlOverridden,
|
|
14566
14887
|
configDir: this.config.configDir,
|
|
@@ -14640,22 +14961,17 @@ async function checkExistingLogin(params) {
|
|
|
14640
14961
|
})))(apiClient);
|
|
14641
14962
|
if (!result.error) return { status: "valid" };
|
|
14642
14963
|
const payload = extractErrorPayload(result.error);
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
},
|
|
14651
|
-
baseUrlOverridden: requestConfig.baseUrlOverridden,
|
|
14652
|
-
configDir: params.configDir,
|
|
14653
|
-
payload
|
|
14654
|
-
})) return { status: "removed_stale" };
|
|
14964
|
+
const code = extractErrorCode(payload);
|
|
14965
|
+
const baseUrlDiffersFromSaved = requestConfig.baseUrlOverridden && requestConfig.apiBaseUrl1 !== params.credentials.api_base_url_1;
|
|
14966
|
+
if (code === API_ERROR_CODES.unauthorized && !baseUrlDiffersFromSaved) {
|
|
14967
|
+
deleteCliCredentials(params.configDir);
|
|
14968
|
+
process.stderr.write("Removed saved Primitive CLI credentials because the existing key was rejected during login. Continuing with a fresh login.\n");
|
|
14969
|
+
return { status: "removed_stale" };
|
|
14970
|
+
}
|
|
14655
14971
|
return {
|
|
14656
14972
|
status: "blocked",
|
|
14657
14973
|
payload,
|
|
14658
|
-
message:
|
|
14974
|
+
message: code === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI credentials were rejected by an API URL different from the one they were saved with. Run `primitive logout` to remove them, or switch back to the original environment before logging in again." : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
|
|
14659
14975
|
};
|
|
14660
14976
|
}
|
|
14661
14977
|
var LoginCommand = class LoginCommand extends Command {
|
|
@@ -15040,7 +15356,7 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
15040
15356
|
if (result.error) {
|
|
15041
15357
|
const errorPayload = extractErrorPayload(result.error);
|
|
15042
15358
|
writeErrorWithHints(errorPayload);
|
|
15043
|
-
|
|
15359
|
+
surfaceUnauthorizedHint({
|
|
15044
15360
|
auth,
|
|
15045
15361
|
baseUrlOverridden,
|
|
15046
15362
|
configDir: this.config.configDir,
|
|
@@ -15059,39 +15375,6 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
15059
15375
|
};
|
|
15060
15376
|
//#endregion
|
|
15061
15377
|
//#region src/oclif/commands/send.ts
|
|
15062
|
-
const SUBJECT_MAX_LENGTH = 200;
|
|
15063
|
-
function deriveSubject(body) {
|
|
15064
|
-
for (const line of body.split("\n")) {
|
|
15065
|
-
const trimmed = line.trim();
|
|
15066
|
-
if (!trimmed) continue;
|
|
15067
|
-
return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
|
|
15068
|
-
}
|
|
15069
|
-
return "Message";
|
|
15070
|
-
}
|
|
15071
|
-
function isVerifiedDomain(domain) {
|
|
15072
|
-
return domain.is_active === true;
|
|
15073
|
-
}
|
|
15074
|
-
async function pickDefaultFromAddress(apiClient, authFailureContext) {
|
|
15075
|
-
const result = await listDomains({
|
|
15076
|
-
client: apiClient.client,
|
|
15077
|
-
responseStyle: "fields"
|
|
15078
|
-
});
|
|
15079
|
-
if (result.error) {
|
|
15080
|
-
const errorPayload = extractErrorPayload(result.error);
|
|
15081
|
-
if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
|
|
15082
|
-
writeErrorWithHints(errorPayload);
|
|
15083
|
-
removeStaleSavedCredentialOnUnauthorized({
|
|
15084
|
-
...authFailureContext,
|
|
15085
|
-
payload: errorPayload
|
|
15086
|
-
});
|
|
15087
|
-
throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
|
|
15088
|
-
}
|
|
15089
|
-
throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
|
|
15090
|
-
}
|
|
15091
|
-
const first = result.data?.data?.find(isVerifiedDomain);
|
|
15092
|
-
if (!first) 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` and verify it.");
|
|
15093
|
-
return `agent@${first.domain}`;
|
|
15094
|
-
}
|
|
15095
15378
|
var SendCommand = class SendCommand extends Command {
|
|
15096
15379
|
static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
|
|
15097
15380
|
|
|
@@ -15183,7 +15466,7 @@ var SendCommand = class SendCommand extends Command {
|
|
|
15183
15466
|
if (result.error) {
|
|
15184
15467
|
const errorPayload = extractErrorPayload(result.error);
|
|
15185
15468
|
writeErrorWithHints(errorPayload);
|
|
15186
|
-
|
|
15469
|
+
surfaceUnauthorizedHint({
|
|
15187
15470
|
...authFailureContext,
|
|
15188
15471
|
payload: errorPayload
|
|
15189
15472
|
});
|
|
@@ -15628,7 +15911,7 @@ var WhoamiCommand = class WhoamiCommand extends Command {
|
|
|
15628
15911
|
if (result.error) {
|
|
15629
15912
|
const errorPayload = extractErrorPayload(result.error);
|
|
15630
15913
|
writeErrorWithHints(errorPayload);
|
|
15631
|
-
|
|
15914
|
+
surfaceUnauthorizedHint({
|
|
15632
15915
|
auth,
|
|
15633
15916
|
baseUrlOverridden,
|
|
15634
15917
|
configDir: this.config.configDir,
|
|
@@ -15851,7 +16134,7 @@ const generatedCommands = Object.fromEntries(operationManifest.filter((operation
|
|
|
15851
16134
|
const COMMANDS = {
|
|
15852
16135
|
completion: CompletionCommand,
|
|
15853
16136
|
"list-operations": ListOperationsCommand,
|
|
15854
|
-
config:
|
|
16137
|
+
config: ConfigCommand,
|
|
15855
16138
|
"config:list": ConfigListCommand,
|
|
15856
16139
|
"config:reset": ConfigResetCommand,
|
|
15857
16140
|
"config:set": ConfigSetCommand,
|
|
@@ -15859,6 +16142,7 @@ const COMMANDS = {
|
|
|
15859
16142
|
describe: DescribeCommand,
|
|
15860
16143
|
send: SendCommand,
|
|
15861
16144
|
reply: ReplyCommand,
|
|
16145
|
+
chat: ChatCommand,
|
|
15862
16146
|
login: LoginCommand,
|
|
15863
16147
|
signup: SignupCommand,
|
|
15864
16148
|
logout: LogoutCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"cli": {
|
|
28
28
|
"description": "CLI authentication"
|
|
29
29
|
},
|
|
30
|
+
"config": {
|
|
31
|
+
"description": "Manage local Primitive CLI request environments",
|
|
32
|
+
"hidden": true
|
|
33
|
+
},
|
|
30
34
|
"account": {
|
|
31
35
|
"description": "Manage your account settings, storage, and webhook secret"
|
|
32
36
|
},
|