@primitivedotdev/cli 0.30.3 → 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 +458 -175
- package/package.json +1 -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();
|
|
@@ -12197,7 +12614,7 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
|
|
|
12197
12614
|
if (result.error) {
|
|
12198
12615
|
const errorPayload = extractErrorPayload(result.error);
|
|
12199
12616
|
writeErrorWithHints(errorPayload);
|
|
12200
|
-
|
|
12617
|
+
surfaceUnauthorizedHint({
|
|
12201
12618
|
auth,
|
|
12202
12619
|
baseUrlOverridden,
|
|
12203
12620
|
configDir: this.config.configDir,
|
|
@@ -12223,104 +12640,6 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
|
|
|
12223
12640
|
}
|
|
12224
12641
|
};
|
|
12225
12642
|
//#endregion
|
|
12226
|
-
//#region src/oclif/commands/emails-poll.ts
|
|
12227
|
-
function quoteDslValue(value) {
|
|
12228
|
-
if (/^[^\s"]+$/.test(value)) return value;
|
|
12229
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
12230
|
-
}
|
|
12231
|
-
function combineQ(q, domain) {
|
|
12232
|
-
const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
|
|
12233
|
-
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
12234
|
-
}
|
|
12235
|
-
function normalizeIsoDate(value, label) {
|
|
12236
|
-
const parsed = new Date(value);
|
|
12237
|
-
if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
|
|
12238
|
-
return parsed.toISOString();
|
|
12239
|
-
}
|
|
12240
|
-
function filtersFromFlags(flags) {
|
|
12241
|
-
return {
|
|
12242
|
-
body: flags.body,
|
|
12243
|
-
domain: flags.domain,
|
|
12244
|
-
domainId: flags["domain-id"],
|
|
12245
|
-
from: flags.from,
|
|
12246
|
-
hasAttachment: flags["has-attachment"],
|
|
12247
|
-
q: flags.q,
|
|
12248
|
-
spamScoreGte: flags["spam-score-gte"],
|
|
12249
|
-
spamScoreLt: flags["spam-score-lt"],
|
|
12250
|
-
subject: flags.subject,
|
|
12251
|
-
to: flags.to
|
|
12252
|
-
};
|
|
12253
|
-
}
|
|
12254
|
-
function sinceFromFlags(flags) {
|
|
12255
|
-
if (flags.since) return normalizeIsoDate(flags.since, "--since");
|
|
12256
|
-
return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
|
|
12257
|
-
}
|
|
12258
|
-
function buildEmailSearchQuery(params) {
|
|
12259
|
-
const query = {
|
|
12260
|
-
include_facets: "false",
|
|
12261
|
-
limit: params.pageSize,
|
|
12262
|
-
snippet: "false",
|
|
12263
|
-
sort: "received_at_asc"
|
|
12264
|
-
};
|
|
12265
|
-
const q = combineQ(params.filters.q, params.filters.domain);
|
|
12266
|
-
if (q) query.q = q;
|
|
12267
|
-
if (params.filters.body) query.body = params.filters.body;
|
|
12268
|
-
if (params.filters.domainId) query.domain_id = params.filters.domainId;
|
|
12269
|
-
if (params.filters.from) query.from = params.filters.from;
|
|
12270
|
-
if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
|
|
12271
|
-
if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
|
|
12272
|
-
if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
|
|
12273
|
-
if (params.filters.subject) query.subject = params.filters.subject;
|
|
12274
|
-
if (params.filters.to) query.to = params.filters.to;
|
|
12275
|
-
if (params.since) query.date_from = params.since;
|
|
12276
|
-
if (params.cursor) query.cursor = params.cursor;
|
|
12277
|
-
return query;
|
|
12278
|
-
}
|
|
12279
|
-
function encodeReceivedAtSearchCursor(email) {
|
|
12280
|
-
const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
|
|
12281
|
-
return Buffer.from(raw, "utf8").toString("base64url");
|
|
12282
|
-
}
|
|
12283
|
-
function cursorFromRows(rows) {
|
|
12284
|
-
const last = rows.at(-1);
|
|
12285
|
-
return last ? encodeReceivedAtSearchCursor(last) : null;
|
|
12286
|
-
}
|
|
12287
|
-
function collectNewAcceptedEmails(rows, seenIds) {
|
|
12288
|
-
const fresh = [];
|
|
12289
|
-
for (const row of rows) {
|
|
12290
|
-
if (row.status !== "accepted" && row.status !== "completed") continue;
|
|
12291
|
-
if (seenIds.has(row.id)) continue;
|
|
12292
|
-
seenIds.add(row.id);
|
|
12293
|
-
fresh.push(row);
|
|
12294
|
-
}
|
|
12295
|
-
return fresh;
|
|
12296
|
-
}
|
|
12297
|
-
async function fetchEmailSearchPage(params) {
|
|
12298
|
-
const result = await searchEmails({
|
|
12299
|
-
client: params.apiClient.client,
|
|
12300
|
-
query: buildEmailSearchQuery({
|
|
12301
|
-
cursor: params.cursor,
|
|
12302
|
-
filters: params.filters,
|
|
12303
|
-
pageSize: params.pageSize,
|
|
12304
|
-
since: params.since
|
|
12305
|
-
}),
|
|
12306
|
-
responseStyle: "fields"
|
|
12307
|
-
});
|
|
12308
|
-
if (result.error) return {
|
|
12309
|
-
ok: false,
|
|
12310
|
-
error: result.error
|
|
12311
|
-
};
|
|
12312
|
-
const envelope = result.data;
|
|
12313
|
-
const rows = envelope?.data ?? [];
|
|
12314
|
-
return {
|
|
12315
|
-
ok: true,
|
|
12316
|
-
cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
|
|
12317
|
-
rows
|
|
12318
|
-
};
|
|
12319
|
-
}
|
|
12320
|
-
function sleep$1(ms) {
|
|
12321
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12322
|
-
}
|
|
12323
|
-
//#endregion
|
|
12324
12643
|
//#region src/oclif/commands/emails-wait.ts
|
|
12325
12644
|
const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
|
|
12326
12645
|
function cliError$4(message) {
|
|
@@ -12373,6 +12692,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
12373
12692
|
min: 1
|
|
12374
12693
|
}),
|
|
12375
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." }),
|
|
12376
12696
|
since: Flags.string({ description: "Only match emails received on or after this date/time" }),
|
|
12377
12697
|
"spam-score-gte": Flags.integer({ description: "Only match emails with spam score greater than or equal to this value" }),
|
|
12378
12698
|
"spam-score-lt": Flags.integer({ description: "Only match emails with spam score below this value" }),
|
|
@@ -12417,7 +12737,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
12417
12737
|
if (!page.ok) {
|
|
12418
12738
|
const payload = extractErrorPayload(page.error);
|
|
12419
12739
|
writeErrorWithHints(payload);
|
|
12420
|
-
|
|
12740
|
+
surfaceUnauthorizedHint({
|
|
12421
12741
|
auth,
|
|
12422
12742
|
baseUrlOverridden,
|
|
12423
12743
|
configDir: this.config.configDir,
|
|
@@ -12539,7 +12859,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
|
|
|
12539
12859
|
if (!page.ok) {
|
|
12540
12860
|
const payload = extractErrorPayload(page.error);
|
|
12541
12861
|
writeErrorWithHints(payload);
|
|
12542
|
-
|
|
12862
|
+
surfaceUnauthorizedHint({
|
|
12543
12863
|
auth,
|
|
12544
12864
|
baseUrlOverridden,
|
|
12545
12865
|
configDir: this.config.configDir,
|
|
@@ -13261,7 +13581,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
13261
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`);
|
|
13262
13582
|
}
|
|
13263
13583
|
writeErrorWithHints(outcome.payload);
|
|
13264
|
-
|
|
13584
|
+
surfaceUnauthorizedHint({
|
|
13265
13585
|
...authFailureContext,
|
|
13266
13586
|
payload: outcome.payload
|
|
13267
13587
|
});
|
|
@@ -13284,7 +13604,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
13284
13604
|
});
|
|
13285
13605
|
if (waitResult.kind === "error") {
|
|
13286
13606
|
writeErrorWithHints(waitResult.payload);
|
|
13287
|
-
|
|
13607
|
+
surfaceUnauthorizedHint({
|
|
13288
13608
|
...authFailureContext,
|
|
13289
13609
|
payload: waitResult.payload
|
|
13290
13610
|
});
|
|
@@ -13317,8 +13637,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
13317
13637
|
name: "Primitive Team",
|
|
13318
13638
|
url: "https://primitive.dev"
|
|
13319
13639
|
};
|
|
13320
|
-
const SDK_VERSION_RANGE = "^0.
|
|
13321
|
-
const CLI_VERSION_RANGE = "^0.
|
|
13640
|
+
const SDK_VERSION_RANGE = "^0.31.0";
|
|
13641
|
+
const CLI_VERSION_RANGE = "^0.31.0";
|
|
13322
13642
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
13323
13643
|
function renderHandler() {
|
|
13324
13644
|
return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
|
|
@@ -13828,7 +14148,7 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
|
|
|
13828
14148
|
if (result.error) {
|
|
13829
14149
|
const errorPayload = extractErrorPayload(result.error);
|
|
13830
14150
|
writeErrorWithHints(errorPayload);
|
|
13831
|
-
|
|
14151
|
+
surfaceUnauthorizedHint({
|
|
13832
14152
|
auth,
|
|
13833
14153
|
baseUrlOverridden,
|
|
13834
14154
|
configDir: this.config.configDir,
|
|
@@ -14084,7 +14404,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
14084
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`);
|
|
14085
14405
|
}
|
|
14086
14406
|
writeErrorWithHints(outcome.payload);
|
|
14087
|
-
|
|
14407
|
+
surfaceUnauthorizedHint({
|
|
14088
14408
|
...authFailureContext,
|
|
14089
14409
|
payload: outcome.payload
|
|
14090
14410
|
});
|
|
@@ -14106,7 +14426,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
14106
14426
|
});
|
|
14107
14427
|
if (waitResult.kind === "error") {
|
|
14108
14428
|
writeErrorWithHints(waitResult.payload);
|
|
14109
|
-
|
|
14429
|
+
surfaceUnauthorizedHint({
|
|
14110
14430
|
...authFailureContext,
|
|
14111
14431
|
payload: waitResult.payload
|
|
14112
14432
|
});
|
|
@@ -14307,7 +14627,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
14307
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");
|
|
14308
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");
|
|
14309
14629
|
writeErrorWithHints(outcome.payload);
|
|
14310
|
-
|
|
14630
|
+
surfaceUnauthorizedHint({
|
|
14311
14631
|
...authFailureContext,
|
|
14312
14632
|
payload: outcome.payload
|
|
14313
14633
|
});
|
|
@@ -14495,7 +14815,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
14495
14815
|
if (triggerResult.error) {
|
|
14496
14816
|
const payload = extractErrorPayload(triggerResult.error);
|
|
14497
14817
|
writeErrorWithHints(payload);
|
|
14498
|
-
|
|
14818
|
+
surfaceUnauthorizedHint({
|
|
14499
14819
|
auth,
|
|
14500
14820
|
baseUrlOverridden,
|
|
14501
14821
|
configDir: this.config.configDir,
|
|
@@ -14533,7 +14853,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
14533
14853
|
if (!page.ok) {
|
|
14534
14854
|
const payload = extractErrorPayload(page.error);
|
|
14535
14855
|
writeErrorWithHints(payload);
|
|
14536
|
-
|
|
14856
|
+
surfaceUnauthorizedHint({
|
|
14537
14857
|
auth,
|
|
14538
14858
|
baseUrlOverridden,
|
|
14539
14859
|
configDir: this.config.configDir,
|
|
@@ -14561,7 +14881,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
14561
14881
|
if (result.error) {
|
|
14562
14882
|
const payload = extractErrorPayload(result.error);
|
|
14563
14883
|
writeErrorWithHints(payload);
|
|
14564
|
-
|
|
14884
|
+
surfaceUnauthorizedHint({
|
|
14565
14885
|
auth,
|
|
14566
14886
|
baseUrlOverridden,
|
|
14567
14887
|
configDir: this.config.configDir,
|
|
@@ -14641,22 +14961,17 @@ async function checkExistingLogin(params) {
|
|
|
14641
14961
|
})))(apiClient);
|
|
14642
14962
|
if (!result.error) return { status: "valid" };
|
|
14643
14963
|
const payload = extractErrorPayload(result.error);
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
},
|
|
14652
|
-
baseUrlOverridden: requestConfig.baseUrlOverridden,
|
|
14653
|
-
configDir: params.configDir,
|
|
14654
|
-
payload
|
|
14655
|
-
})) 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
|
+
}
|
|
14656
14971
|
return {
|
|
14657
14972
|
status: "blocked",
|
|
14658
14973
|
payload,
|
|
14659
|
-
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."
|
|
14660
14975
|
};
|
|
14661
14976
|
}
|
|
14662
14977
|
var LoginCommand = class LoginCommand extends Command {
|
|
@@ -15041,7 +15356,7 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
15041
15356
|
if (result.error) {
|
|
15042
15357
|
const errorPayload = extractErrorPayload(result.error);
|
|
15043
15358
|
writeErrorWithHints(errorPayload);
|
|
15044
|
-
|
|
15359
|
+
surfaceUnauthorizedHint({
|
|
15045
15360
|
auth,
|
|
15046
15361
|
baseUrlOverridden,
|
|
15047
15362
|
configDir: this.config.configDir,
|
|
@@ -15060,39 +15375,6 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
15060
15375
|
};
|
|
15061
15376
|
//#endregion
|
|
15062
15377
|
//#region src/oclif/commands/send.ts
|
|
15063
|
-
const SUBJECT_MAX_LENGTH = 200;
|
|
15064
|
-
function deriveSubject(body) {
|
|
15065
|
-
for (const line of body.split("\n")) {
|
|
15066
|
-
const trimmed = line.trim();
|
|
15067
|
-
if (!trimmed) continue;
|
|
15068
|
-
return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
|
|
15069
|
-
}
|
|
15070
|
-
return "Message";
|
|
15071
|
-
}
|
|
15072
|
-
function isVerifiedDomain(domain) {
|
|
15073
|
-
return domain.is_active === true;
|
|
15074
|
-
}
|
|
15075
|
-
async function pickDefaultFromAddress(apiClient, authFailureContext) {
|
|
15076
|
-
const result = await listDomains({
|
|
15077
|
-
client: apiClient.client,
|
|
15078
|
-
responseStyle: "fields"
|
|
15079
|
-
});
|
|
15080
|
-
if (result.error) {
|
|
15081
|
-
const errorPayload = extractErrorPayload(result.error);
|
|
15082
|
-
if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
|
|
15083
|
-
writeErrorWithHints(errorPayload);
|
|
15084
|
-
removeStaleSavedCredentialOnUnauthorized({
|
|
15085
|
-
...authFailureContext,
|
|
15086
|
-
payload: errorPayload
|
|
15087
|
-
});
|
|
15088
|
-
throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
|
|
15089
|
-
}
|
|
15090
|
-
throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
|
|
15091
|
-
}
|
|
15092
|
-
const first = result.data?.data?.find(isVerifiedDomain);
|
|
15093
|
-
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.");
|
|
15094
|
-
return `agent@${first.domain}`;
|
|
15095
|
-
}
|
|
15096
15378
|
var SendCommand = class SendCommand extends Command {
|
|
15097
15379
|
static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
|
|
15098
15380
|
|
|
@@ -15184,7 +15466,7 @@ var SendCommand = class SendCommand extends Command {
|
|
|
15184
15466
|
if (result.error) {
|
|
15185
15467
|
const errorPayload = extractErrorPayload(result.error);
|
|
15186
15468
|
writeErrorWithHints(errorPayload);
|
|
15187
|
-
|
|
15469
|
+
surfaceUnauthorizedHint({
|
|
15188
15470
|
...authFailureContext,
|
|
15189
15471
|
payload: errorPayload
|
|
15190
15472
|
});
|
|
@@ -15629,7 +15911,7 @@ var WhoamiCommand = class WhoamiCommand extends Command {
|
|
|
15629
15911
|
if (result.error) {
|
|
15630
15912
|
const errorPayload = extractErrorPayload(result.error);
|
|
15631
15913
|
writeErrorWithHints(errorPayload);
|
|
15632
|
-
|
|
15914
|
+
surfaceUnauthorizedHint({
|
|
15633
15915
|
auth,
|
|
15634
15916
|
baseUrlOverridden,
|
|
15635
15917
|
configDir: this.config.configDir,
|
|
@@ -15860,6 +16142,7 @@ const COMMANDS = {
|
|
|
15860
16142
|
describe: DescribeCommand,
|
|
15861
16143
|
send: SendCommand,
|
|
15862
16144
|
reply: ReplyCommand,
|
|
16145
|
+
chat: ChatCommand,
|
|
15863
16146
|
login: LoginCommand,
|
|
15864
16147
|
signup: SignupCommand,
|
|
15865
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,
|