primcli 1.42.0 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/bin/claude-pending-mail.mjs +31 -6
- package/bin/claude-wake.mjs +7 -2
- package/dist/{interaction-actions-D5o_frTT.js → api-command-CpW39SQw.js} +1674 -1223
- package/dist/{auto-signals-D3NqwgYA.js → auto-signals-oSCnNgts.js} +1 -1
- package/dist/{cli-config-BoZ1lMUQ.js → cli-config-DGrZCUfw.js} +16 -1
- package/dist/{connect-skill-BHxdjtkK.js → connect-skill-BJFxnbwI.js} +2 -2
- package/dist/oclif/auto-signal-worker.js +3 -3
- package/dist/oclif/auto-signals.js +1 -1
- package/dist/oclif/index.js +883 -738
- package/dist/oclif/receiver-heal.js +2 -2
- package/dist/oclif/root-signup-hint.js +1 -1
- package/dist/{repeats-CoK0tpCK-Bx7nmyRi.js → repeats-D2xtLium-Dv34tqei.js} +21 -2
- package/dist/{signal-command-D5YGxi_o.js → signal-command-BbdorL9t.js} +15 -182
- package/dist/skills/primitive-connect/SKILL.md +21 -0
- package/dist/skills/primitive-connect.json +3 -3
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { b as __exportAll } from "./repeats-
|
|
2
|
-
import { C as resolveCliAuth, G as createConfig,
|
|
1
|
+
import { b as __exportAll } from "./repeats-D2xtLium-Dv34tqei.js";
|
|
2
|
+
import { C as resolveCliAuth, F as agentProfilesDirectory, G as createClient, H as requireDefaultLoginProfile, K as createConfig, N as agentProfileDirectory, R as loadConnectedAgentProfile, S as normalizeApiBaseUrl, T as saveSignupCredentials, W as PrimitiveApiClient, _ as deleteCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveCliCredentials, x as loadCliCredentials, y as detectPrimitiveKeyEnvMisname } from "./cli-config-DGrZCUfw.js";
|
|
3
|
+
import { h as writeMailJson, m as withMailLock, p as syncMailDirectory, u as privateMailPermissions } from "./shared-mail-files-SPPcis9g.js";
|
|
3
4
|
import { Command, Errors, Flags } from "@oclif/core";
|
|
4
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { lstatSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
//#region ../packages/api-core/src/api/client.gen.ts
|
|
7
9
|
const client = createClient(createConfig({ baseUrl: "https://api.primitive.dev/v1" }));
|
|
8
10
|
//#endregion
|
|
@@ -3747,1361 +3749,1810 @@ function formatAlreadySentNotice(sent) {
|
|
|
3747
3749
|
return `Already sent: this exact message went out earlier${details.length > 0 ? ` (${details.join(", ")})` : ""}. Nothing new was sent.`;
|
|
3748
3750
|
}
|
|
3749
3751
|
//#endregion
|
|
3750
|
-
//#region src/oclif/
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
const
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3752
|
+
//#region src/oclif/mail-session.ts
|
|
3753
|
+
/** Runtime-provided identity only. Never guess a session from history or cwd. */
|
|
3754
|
+
function currentMailSessionKey(env = process.env) {
|
|
3755
|
+
const codex = env.CODEX_THREAD_ID ?? env.CODEX_SESSION_ID;
|
|
3756
|
+
const claude = env.CLAUDE_CODE_SESSION_ID;
|
|
3757
|
+
if (codex && claude) return null;
|
|
3758
|
+
const value = codex ?? claude;
|
|
3759
|
+
return value && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value) ? `${codex ? "codex" : "claude"}:${value.toLowerCase()}` : null;
|
|
3757
3760
|
}
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
* failed). Reported like ReplyStateUnsupportedError so callers never
|
|
3768
|
-
* read an unverified empty answer as "nothing matched".
|
|
3769
|
-
*/
|
|
3770
|
-
var ReplyStateUnverifiedError = class extends ReplyStateUnsupportedError {
|
|
3771
|
-
constructor(detail) {
|
|
3772
|
-
super(detail);
|
|
3773
|
-
this.message = `Could not verify that the server supports reply state: ${detail} Nothing was treated as matching. Retry the command.`;
|
|
3774
|
-
this.name = "ReplyStateUnverifiedError";
|
|
3775
|
-
this.code = "reply_state_unverified";
|
|
3776
|
-
}
|
|
3761
|
+
//#endregion
|
|
3762
|
+
//#region src/oclif/send-outcome.ts
|
|
3763
|
+
const SEND_OUTCOME_EXIT_CODES = {
|
|
3764
|
+
replied: 0,
|
|
3765
|
+
sent: 0,
|
|
3766
|
+
already_sent: 0,
|
|
3767
|
+
not_sent: 1,
|
|
3768
|
+
sent_awaiting_reply: 3,
|
|
3769
|
+
uncertain: 4
|
|
3777
3770
|
};
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3771
|
+
const DEFINITIVE_SEND_REJECTION_STATUSES = [
|
|
3772
|
+
400,
|
|
3773
|
+
401,
|
|
3774
|
+
402,
|
|
3775
|
+
403,
|
|
3776
|
+
404,
|
|
3777
|
+
413,
|
|
3778
|
+
422,
|
|
3779
|
+
429
|
|
3780
|
+
];
|
|
3781
|
+
const SEND_OUTCOME_HELP = `Outcomes and exit codes, shared by chat, chat reply, send and reply (JSON output carries the name as "outcome"):
|
|
3782
|
+
- exit 0 replied: chat only. The message was sent and a reply arrived.
|
|
3783
|
+
- exit 0 sent: accepted for delivery. A queued status counts as sent.
|
|
3784
|
+
- exit 0 already_sent: an identical earlier send exists (or was deleted: HTTP 410 sent_email_deleted). Nothing new went out. Do not resend.
|
|
3785
|
+
- exit 1 not_sent: the API rejected the request (HTTP 400, 401, 402, 403, 404, 413, 422 or 429), the command failed before sending, or the send record has status agent_failed, gate_denied or canceled. Nothing went out.
|
|
3786
|
+
- exit 2: invalid flags or arguments. Nothing went out.
|
|
3787
|
+
- exit 3 sent_awaiting_reply: chat only. Sent, but no reply before the timeout. Wait; do not resend.
|
|
3788
|
+
- exit 4 uncertain: transport error, conflict, server error, or a send record with status unknown. It may or may not have gone out. Reconcile with primitive sent get --idempotency-key <key> (the key is in the --json envelope) before retrying.`;
|
|
3789
|
+
function sendOutcomeExitCode(outcome) {
|
|
3790
|
+
return SEND_OUTCOME_EXIT_CODES[outcome];
|
|
3782
3791
|
}
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
* nothing either way; callers that asked for reply state follow an
|
|
3786
|
-
* empty search page with ensureSearchReportsReplyState(). `surface` names the
|
|
3787
|
-
* response for the error, e.g. "GET /emails".
|
|
3788
|
-
*/
|
|
3789
|
-
function assertReplyState(rows, surface) {
|
|
3790
|
-
const missing = rows.filter((row) => !hasReplyState(row)).length;
|
|
3791
|
-
if (missing > 0) throw new ReplyStateUnsupportedError(`${surface} returned ${missing} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the \`awaiting\` and \`reply_count\` fields.`);
|
|
3792
|
+
function sendFailureOutcome(httpStatus) {
|
|
3793
|
+
return httpStatus !== void 0 && DEFINITIVE_SEND_REJECTION_STATUSES.includes(httpStatus) ? "not_sent" : "uncertain";
|
|
3792
3794
|
}
|
|
3793
|
-
const
|
|
3795
|
+
const SENT_EMAIL_DELETED_CODE = "sent_email_deleted";
|
|
3796
|
+
function errorPayloadCode(payload) {
|
|
3797
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
3798
|
+
const code = payload.code;
|
|
3799
|
+
return typeof code === "string" ? code : void 0;
|
|
3800
|
+
}
|
|
3801
|
+
function isDeletedEarlierSend(httpStatus, errorPayload) {
|
|
3802
|
+
return httpStatus === 410 && errorPayloadCode(errorPayload) === SENT_EMAIL_DELETED_CODE;
|
|
3803
|
+
}
|
|
3804
|
+
/** Classify an API error from a send request. */
|
|
3805
|
+
function classifySendError(httpStatus, errorPayload) {
|
|
3806
|
+
return isDeletedEarlierSend(httpStatus, errorPayload) ? "already_sent" : sendFailureOutcome(httpStatus);
|
|
3807
|
+
}
|
|
3808
|
+
function formatDeletedEarlierSendNotice() {
|
|
3809
|
+
return "Already sent: an earlier send matching this request went out and its record was later deleted, so the API refused this one (HTTP 410 sent_email_deleted). Nothing new was sent. Do not resend or change the idempotency key to get around this.";
|
|
3810
|
+
}
|
|
3811
|
+
/** Read the HTTP status from a `responseStyle: "fields"` API result. */
|
|
3812
|
+
function apiResultHttpStatus(result) {
|
|
3813
|
+
if (result === null || typeof result !== "object") return void 0;
|
|
3814
|
+
if (!("response" in result)) return void 0;
|
|
3815
|
+
const response = result.response;
|
|
3816
|
+
return typeof response?.status === "number" ? response.status : void 0;
|
|
3817
|
+
}
|
|
3818
|
+
const SEND_STATUSES_THAT_DID_NOT_GO_OUT = new Set([
|
|
3819
|
+
"agent_failed",
|
|
3820
|
+
"canceled",
|
|
3821
|
+
"gate_denied"
|
|
3822
|
+
]);
|
|
3823
|
+
const SEND_STATUSES_WITH_UNKNOWN_OUTCOME = new Set(["unknown"]);
|
|
3794
3824
|
/**
|
|
3795
|
-
*
|
|
3796
|
-
*
|
|
3797
|
-
*
|
|
3798
|
-
*
|
|
3825
|
+
* Classify a send record the API returned with a 2xx. A 2xx alone does
|
|
3826
|
+
* not mean a message went out: the record's status can show the
|
|
3827
|
+
* attempt was rejected or that its result is indeterminate. The same
|
|
3828
|
+
* holds for an idempotent replay, which returns the earlier attempt's
|
|
3829
|
+
* record as it stands.
|
|
3799
3830
|
*/
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
this.code = AWAITING_REJECTED_UNSUPPORTED_CODE;
|
|
3806
|
-
}
|
|
3807
|
-
};
|
|
3831
|
+
function successfulSendOutcome(sent) {
|
|
3832
|
+
if (SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(sent.status)) return "not_sent";
|
|
3833
|
+
if (SEND_STATUSES_WITH_UNKNOWN_OUTCOME.has(sent.status)) return "uncertain";
|
|
3834
|
+
return sent.idempotent_replay === true ? "already_sent" : "sent";
|
|
3835
|
+
}
|
|
3808
3836
|
/**
|
|
3809
|
-
*
|
|
3810
|
-
*
|
|
3837
|
+
* Summary for a 2xx send record whose status shows it did not go out
|
|
3838
|
+
* (`not_sent`) or that its result is indeterminate (`uncertain`).
|
|
3811
3839
|
*/
|
|
3812
|
-
function
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3840
|
+
function formatSendRecordFailureSummary(noun, outcome, sent) {
|
|
3841
|
+
const record = sent.idempotent_replay === true ? `the earlier identical attempt (sent id ${sent.id}) has status ${sent.status}` : `the send record (id ${sent.id}) has status ${sent.status}`;
|
|
3842
|
+
if (outcome === "not_sent") return `${noun} not sent: ${record}. Nothing went out; fix the problem before retrying.`;
|
|
3843
|
+
return `${noun} send outcome uncertain: ${record}. It may or may not have gone out. Do not resend blindly; check sent history first.`;
|
|
3816
3844
|
}
|
|
3817
|
-
function
|
|
3818
|
-
if (
|
|
3819
|
-
|
|
3820
|
-
return
|
|
3845
|
+
function describeSentStatus(status) {
|
|
3846
|
+
if (status === "queued") return "queued for delivery";
|
|
3847
|
+
if (status === "delivered") return "delivered";
|
|
3848
|
+
return `status ${status}`;
|
|
3821
3849
|
}
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
* query parameter or the `awaiting:` search term as unknown. A current
|
|
3825
|
-
* server accepts both, and the CLI only ever sends valid values.
|
|
3826
|
-
*/
|
|
3827
|
-
function isAwaitingRejectedError(payload) {
|
|
3828
|
-
const body = errorBody(payload);
|
|
3829
|
-
if (body.code !== "validation_error") return false;
|
|
3830
|
-
const message = typeof body.message === "string" ? body.message : JSON.stringify(payload);
|
|
3831
|
-
return /awaiting/i.test(message) && /unrecogni[sz]ed|unknown/i.test(message);
|
|
3850
|
+
function formatSentSummary(noun, sent) {
|
|
3851
|
+
return `${noun} sent (${describeSentStatus(sent.status)}, id ${sent.id}). Do not resend.`;
|
|
3832
3852
|
}
|
|
3833
|
-
function
|
|
3834
|
-
return
|
|
3853
|
+
function formatSendFailureSummary(noun, outcome, httpStatus) {
|
|
3854
|
+
if (outcome === "not_sent") return `${noun} not sent: ${httpStatus === void 0 ? "the request failed before sending" : `the API rejected the request (HTTP ${httpStatus})`}. Nothing went out; fix the problem before retrying.`;
|
|
3855
|
+
return `${noun} send outcome uncertain (${httpStatus === void 0 ? "no response from the API" : `HTTP ${httpStatus}`}): it may or may not have gone out. Do not resend blindly; check sent history first.`;
|
|
3835
3856
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
if (!hasReplyState(row)) return "-";
|
|
3839
|
-
return row.awaiting;
|
|
3857
|
+
function priorRepliesThatWentOut(replies, options = {}) {
|
|
3858
|
+
return (replies ?? []).filter((reply) => !SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(reply.status) && reply.id !== options.excludeSentId);
|
|
3840
3859
|
}
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
if (!
|
|
3844
|
-
return
|
|
3860
|
+
function formatPriorRepliesWarning(prior) {
|
|
3861
|
+
const latest = prior.at(-1);
|
|
3862
|
+
if (!latest) return null;
|
|
3863
|
+
return `This email already has ${prior.length === 1 ? "1 outgoing email" : `${prior.length} outgoing emails`}, most recently at ${latest.created_at} (sent id ${latest.id}). These may include activity updates and do not prove a completed answer. Sending this reply.`;
|
|
3845
3864
|
}
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
searchEmails: "GET /emails/search"
|
|
3849
|
-
};
|
|
3850
|
-
/**
|
|
3851
|
-
* For the generated `emails list` / `emails search` commands: the
|
|
3852
|
-
* surface name when this call asked for reply state (an `awaiting`
|
|
3853
|
-
* parameter or an `awaiting:` search term), else null.
|
|
3854
|
-
*/
|
|
3855
|
-
function replyStateSurfaceForOperation(sdkName, query) {
|
|
3856
|
-
const surface = REPLY_STATE_SURFACES[sdkName];
|
|
3857
|
-
if (!surface || !query) return null;
|
|
3858
|
-
const q = typeof query.q === "string" ? query.q : void 0;
|
|
3859
|
-
return query.awaiting !== void 0 || queryUsesAwaiting(q) ? surface : null;
|
|
3865
|
+
function formatPriorRepliesCheckSkipped(emailId, reason) {
|
|
3866
|
+
return `Could not check whether you already replied to email ${emailId} (${reason}). Prior-reply check skipped; sending the reply anyway.`;
|
|
3860
3867
|
}
|
|
3861
|
-
const searchSupportProbes = /* @__PURE__ */ new WeakMap();
|
|
3862
3868
|
/**
|
|
3863
|
-
*
|
|
3864
|
-
*
|
|
3865
|
-
*
|
|
3866
|
-
*
|
|
3867
|
-
* means the mailbox is empty, so the empty answer stands.
|
|
3869
|
+
* Look up the inbound email and report replies to it that already
|
|
3870
|
+
* went out. Never throws: the check is advisory and must not block a
|
|
3871
|
+
* send, so every failure comes back as `skipped` with a reason the
|
|
3872
|
+
* caller surfaces.
|
|
3868
3873
|
*/
|
|
3869
|
-
function
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
client: apiClient.client,
|
|
3876
|
-
query: {
|
|
3877
|
-
limit: 1,
|
|
3878
|
-
include_facets: "false",
|
|
3879
|
-
snippet: "false"
|
|
3880
|
-
},
|
|
3881
|
-
responseStyle: "fields"
|
|
3882
|
-
});
|
|
3883
|
-
if (result.error) throw new ReplyStateUnverifiedError("the empty result could not be checked: reading one unfiltered row from GET /emails/search failed.");
|
|
3884
|
-
const rows = result.data?.data;
|
|
3885
|
-
assertReplyState(rows ?? [], "GET /emails/search");
|
|
3886
|
-
})();
|
|
3887
|
-
searchSupportProbes.set(key, probe);
|
|
3888
|
-
probe.catch(() => {
|
|
3889
|
-
if (searchSupportProbes.get(key) === probe) searchSupportProbes.delete(key);
|
|
3874
|
+
async function checkPriorReplies(params) {
|
|
3875
|
+
try {
|
|
3876
|
+
const result = await getEmail({
|
|
3877
|
+
client: params.client,
|
|
3878
|
+
path: { id: params.emailId },
|
|
3879
|
+
responseStyle: "fields"
|
|
3890
3880
|
});
|
|
3881
|
+
if (result.error) {
|
|
3882
|
+
const status = apiResultHttpStatus(result);
|
|
3883
|
+
return {
|
|
3884
|
+
status: "skipped",
|
|
3885
|
+
reason: status === void 0 ? "the email lookup failed" : `the email lookup returned HTTP ${status}`
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
const envelope = result.data;
|
|
3889
|
+
const detail = envelope?.data ?? envelope;
|
|
3890
|
+
if (!detail || !Array.isArray(detail.replies)) return {
|
|
3891
|
+
status: "skipped",
|
|
3892
|
+
reason: "the email lookup returned no reply history"
|
|
3893
|
+
};
|
|
3894
|
+
return {
|
|
3895
|
+
status: "checked",
|
|
3896
|
+
prior: priorRepliesThatWentOut(detail.replies),
|
|
3897
|
+
detail
|
|
3898
|
+
};
|
|
3899
|
+
} catch (error) {
|
|
3900
|
+
return {
|
|
3901
|
+
status: "skipped",
|
|
3902
|
+
reason: `the email lookup failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3903
|
+
};
|
|
3891
3904
|
}
|
|
3892
|
-
return probe;
|
|
3893
3905
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
accessDenied: "access_denied",
|
|
3898
|
-
authorizationPending: "authorization_pending",
|
|
3899
|
-
expiredToken: "expired_token",
|
|
3900
|
-
invalidDeviceCode: "invalid_device_code",
|
|
3901
|
-
notFound: "not_found",
|
|
3902
|
-
slowDown: "slow_down",
|
|
3903
|
-
unauthorized: "unauthorized"
|
|
3904
|
-
};
|
|
3905
|
-
function flagName(parameterName) {
|
|
3906
|
-
return parameterName.replace(/_/g, "-");
|
|
3906
|
+
function shellQuote(value) {
|
|
3907
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
3908
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
3907
3909
|
}
|
|
3908
|
-
function
|
|
3909
|
-
return
|
|
3910
|
+
function commandFromArgv(argv) {
|
|
3911
|
+
return argv.map(shellQuote).join(" ");
|
|
3910
3912
|
}
|
|
3911
|
-
|
|
3912
|
-
const
|
|
3913
|
-
if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
|
|
3914
|
-
const value = Number(trimmed);
|
|
3915
|
-
if (!Number.isFinite(value)) throw new Errors.CLIError(`Expected a number but received: ${input}`);
|
|
3916
|
-
if (options.min !== void 0 && value < options.min) throw new Errors.CLIError(`Expected a number greater than or equal to ${options.min} but received: ${input}`);
|
|
3917
|
-
if (options.max !== void 0 && value > options.max) throw new Errors.CLIError(`Expected a number less than or equal to ${options.max} but received: ${input}`);
|
|
3918
|
-
return value;
|
|
3919
|
-
} });
|
|
3920
|
-
function numericFlagOptions(parameter) {
|
|
3913
|
+
function buildFollowUpCommand(kind, description, argv, options = {}) {
|
|
3914
|
+
const requiresMessage = options.requiresMessage ?? false;
|
|
3921
3915
|
return {
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
const requiredArr = Array.isArray(schema.required) ? schema.required.filter((k) => typeof k === "string") : [];
|
|
3932
|
-
const required = new Set(requiredArr);
|
|
3933
|
-
const fields = [];
|
|
3934
|
-
for (const [name, raw] of Object.entries(properties)) {
|
|
3935
|
-
const propSchema = raw && typeof raw === "object" ? raw : {};
|
|
3936
|
-
const t = propSchema.type;
|
|
3937
|
-
let displayType = "any";
|
|
3938
|
-
let kind = "complex";
|
|
3939
|
-
if (typeof t === "string") {
|
|
3940
|
-
displayType = t;
|
|
3941
|
-
if (t === "string") kind = "string";
|
|
3942
|
-
else if (t === "integer") kind = "integer";
|
|
3943
|
-
else if (t === "number") kind = "number";
|
|
3944
|
-
else if (t === "boolean") kind = "boolean";
|
|
3945
|
-
else if (t === "array") {
|
|
3946
|
-
const items = propSchema.items;
|
|
3947
|
-
if (items && typeof items === "object") {
|
|
3948
|
-
const itemType = items.type;
|
|
3949
|
-
if (typeof itemType === "string") displayType = `array<${itemType}>`;
|
|
3950
|
-
}
|
|
3951
|
-
kind = "complex";
|
|
3952
|
-
} else kind = "complex";
|
|
3953
|
-
} else if (Array.isArray(t)) {
|
|
3954
|
-
const nonNull = t.filter((s) => s !== "null");
|
|
3955
|
-
if (nonNull.length === 1) {
|
|
3956
|
-
const single = nonNull[0];
|
|
3957
|
-
displayType = `${single}?`;
|
|
3958
|
-
if (single === "string") kind = "string";
|
|
3959
|
-
else if (single === "integer") kind = "integer";
|
|
3960
|
-
else if (single === "number") kind = "number";
|
|
3961
|
-
else if (single === "boolean") kind = "boolean";
|
|
3962
|
-
else kind = "complex";
|
|
3963
|
-
} else {
|
|
3964
|
-
displayType = nonNull.join("|");
|
|
3965
|
-
kind = "complex";
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
const description = typeof propSchema.description === "string" ? propSchema.description.split(/\n\s*\n/)[0].replace(/\s*\n\s*/g, " ").trim() : "";
|
|
3969
|
-
const enumRaw = propSchema.enum;
|
|
3970
|
-
const enumValues = kind === "string" && Array.isArray(enumRaw) ? enumRaw.filter((e) => typeof e === "string") : void 0;
|
|
3971
|
-
fields.push({
|
|
3972
|
-
name,
|
|
3973
|
-
description,
|
|
3974
|
-
required: required.has(name),
|
|
3975
|
-
displayType,
|
|
3976
|
-
kind,
|
|
3977
|
-
...enumValues && enumValues.length > 0 ? { enumValues } : {},
|
|
3978
|
-
...typeof propSchema.maximum === "number" ? { maximum: propSchema.maximum } : {},
|
|
3979
|
-
...typeof propSchema.minimum === "number" ? { minimum: propSchema.minimum } : {}
|
|
3980
|
-
});
|
|
3981
|
-
}
|
|
3982
|
-
return fields.sort((a, b) => {
|
|
3983
|
-
if (a.required !== b.required) return a.required ? -1 : 1;
|
|
3984
|
-
return a.name.localeCompare(b.name);
|
|
3985
|
-
});
|
|
3986
|
-
}
|
|
3987
|
-
/**
|
|
3988
|
-
* Render a "Body fields" summary for the per-command help.
|
|
3989
|
-
*
|
|
3990
|
-
* Most scalar fields are exposed as individual `--flag` flags,
|
|
3991
|
-
* which oclif auto-renders in the FLAGS section above. To avoid
|
|
3992
|
-
* duplicating that, the summary here only documents fields that
|
|
3993
|
-
* MUST go through `--raw-body` (complex types: arrays, objects,
|
|
3994
|
-
* mixed-non-nullable). When an operation has only scalars, the
|
|
3995
|
-
* summary is omitted entirely and oclif's FLAGS section is the
|
|
3996
|
-
* full story.
|
|
3997
|
-
*
|
|
3998
|
-
* For operations with mixed scalar and complex fields, we also
|
|
3999
|
-
* include a short header pointing the agent at the flag form so
|
|
4000
|
-
* the natural reading is "use the flags above; --raw-body for
|
|
4001
|
-
* the leftovers below."
|
|
4002
|
-
*/
|
|
4003
|
-
function renderRequestSchemaSummary(schema) {
|
|
4004
|
-
const fields = extractBodyFields(schema);
|
|
4005
|
-
if (fields.length === 0) return null;
|
|
4006
|
-
const complex = fields.filter((f) => f.kind === "complex");
|
|
4007
|
-
if (complex.length === 0) return null;
|
|
4008
|
-
const nameWidth = Math.min(24, Math.max(...complex.map((f) => f.name.length)));
|
|
4009
|
-
const descMax = 78;
|
|
4010
|
-
const lines = ["Body fields requiring --raw-body JSON (these are not exposed as flags):"];
|
|
4011
|
-
for (const f of complex) {
|
|
4012
|
-
const marker = f.required ? " *" : " ";
|
|
4013
|
-
const padName = f.name.padEnd(nameWidth);
|
|
4014
|
-
const trimmedDesc = f.description.length > descMax ? `${f.description.slice(0, descMax - 3)}...` : f.description;
|
|
4015
|
-
const desc = trimmedDesc ? ` ${trimmedDesc}` : "";
|
|
4016
|
-
lines.push(`${marker} ${padName} ${f.displayType}${desc}`);
|
|
4017
|
-
}
|
|
4018
|
-
lines.push("(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)");
|
|
4019
|
-
return lines.join("\n");
|
|
4020
|
-
}
|
|
4021
|
-
function flagForParameter(parameter) {
|
|
4022
|
-
const common = {
|
|
4023
|
-
description: flagDescription(parameter),
|
|
4024
|
-
required: parameter.required
|
|
3916
|
+
argv,
|
|
3917
|
+
description,
|
|
3918
|
+
command: commandFromArgv(argv),
|
|
3919
|
+
kind,
|
|
3920
|
+
placeholders: requiresMessage ? [{
|
|
3921
|
+
description: "Replace with the message body before running.",
|
|
3922
|
+
token: "<message>"
|
|
3923
|
+
}] : [],
|
|
3924
|
+
requires_message: requiresMessage
|
|
4025
3925
|
};
|
|
4026
|
-
if (parameter.type === "boolean") return Flags.boolean(common);
|
|
4027
|
-
if (parameter.type === "integer") return Flags.integer({
|
|
4028
|
-
...common,
|
|
4029
|
-
...numericFlagOptions(parameter)
|
|
4030
|
-
});
|
|
4031
|
-
if (parameter.type === "number") return numberFlag({
|
|
4032
|
-
...common,
|
|
4033
|
-
...numericFlagOptions(parameter)
|
|
4034
|
-
});
|
|
4035
|
-
if (parameter.enum && parameter.enum.length > 0) return Flags.string({
|
|
4036
|
-
...common,
|
|
4037
|
-
options: parameter.enum
|
|
4038
|
-
});
|
|
4039
|
-
return Flags.string(common);
|
|
4040
|
-
}
|
|
4041
|
-
function coerceParameterValue(parameter, value) {
|
|
4042
|
-
if (value === void 0) return;
|
|
4043
|
-
if (parameter.type === "number") {
|
|
4044
|
-
if (typeof value === "number") return value;
|
|
4045
|
-
const parsed = Number(value);
|
|
4046
|
-
if (Number.isNaN(parsed)) throw new Errors.CLIError(`Invalid number for --${parameter.name}: ${value}`);
|
|
4047
|
-
return parsed;
|
|
4048
|
-
}
|
|
4049
|
-
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
|
|
4050
|
-
throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
|
|
4051
|
-
}
|
|
4052
|
-
function cliError(message) {
|
|
4053
|
-
return new Errors.CLIError(message, { exit: 1 });
|
|
4054
3926
|
}
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
}
|
|
3927
|
+
const SENT_HISTORY_CLOCK_SKEW_MS = 300 * 1e3;
|
|
3928
|
+
function sentHistoryWindowStart(attemptStartedAtIso) {
|
|
3929
|
+
const started = Date.parse(attemptStartedAtIso);
|
|
3930
|
+
if (!Number.isFinite(started)) return attemptStartedAtIso;
|
|
3931
|
+
return new Date(started - SENT_HISTORY_CLOCK_SKEW_MS).toISOString();
|
|
4061
3932
|
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
if (
|
|
4067
|
-
|
|
4068
|
-
try {
|
|
4069
|
-
contents = readFileSync(bodyFile, "utf8");
|
|
4070
|
-
} catch (error) {
|
|
4071
|
-
throw cliError(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4072
|
-
}
|
|
4073
|
-
return parseJson(contents, `--body-file ${bodyFile}`);
|
|
4074
|
-
}
|
|
4075
|
-
if (typeof rawBody === "string") return parseJson(rawBody, "--raw-body");
|
|
3933
|
+
/** Header rule for Idempotency-Key: 1 to 255 printable ASCII characters. */
|
|
3934
|
+
const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
|
|
3935
|
+
const IDEMPOTENCY_KEY_FLAG_DESCRIPTION = "Idempotency key for this send (1 to 255 printable ASCII characters). Retrying with the same key returns the original send instead of sending twice. Defaults to a key derived from the request content and the current five-minute window, so an identical retry within that window is deduplicated and a deliberate repeat later still sends. To retry an uncertain send after that, pass the key it reported. Look a send up later with `primitive sent get --idempotency-key <key>`.";
|
|
3936
|
+
function assertValidIdempotencyKey(key) {
|
|
3937
|
+
if (!IDEMPOTENCY_KEY_PATTERN.test(key)) throw new Errors.CLIError("--idempotency-key must be 1 to 255 printable ASCII characters with no spaces.", { exit: 2 });
|
|
3938
|
+
return key;
|
|
4076
3939
|
}
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
}
|
|
3940
|
+
const NON_CONTENT_SEND_FIELDS = new Set(["wait", "wait_timeout_ms"]);
|
|
3941
|
+
function canonicalJson(value) {
|
|
3942
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item ?? null)).join(",")}]`;
|
|
3943
|
+
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
3944
|
+
return JSON.stringify(value ?? null);
|
|
4083
3945
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
3946
|
+
/**
|
|
3947
|
+
* How long a derived key stays the same. It matches the window the API
|
|
3948
|
+
* uses for the key it derives itself when a request carries none, so an
|
|
3949
|
+
* identical send repeated inside this window is deduplicated and a
|
|
3950
|
+
* deliberate repeat later (a digest, a heartbeat, the same short reply
|
|
3951
|
+
* on another day) still goes out, exactly as without a key. An explicit
|
|
3952
|
+
* --idempotency-key never expires.
|
|
3953
|
+
*/
|
|
3954
|
+
const DERIVED_IDEMPOTENCY_WINDOW_MS = 300 * 1e3;
|
|
3955
|
+
/**
|
|
3956
|
+
* The idempotency key a send uses when the caller passes none. It is a
|
|
3957
|
+
* hash of what is sent (and, for a reply, of the email replied to) and
|
|
3958
|
+
* of the current five-minute window, so dedup matches what the API did
|
|
3959
|
+
* before the CLI sent a key, while the CLI knows the key before the
|
|
3960
|
+
* request starts. That lets an uncertain outcome still report the key to
|
|
3961
|
+
* reconcile with, and to retry under with --idempotency-key.
|
|
3962
|
+
*/
|
|
3963
|
+
function deriveSendIdempotencyKey(kind, request, now = Date.now()) {
|
|
3964
|
+
const content = Object.fromEntries(Object.entries(request).filter(([key]) => !NON_CONTENT_SEND_FIELDS.has(key)));
|
|
3965
|
+
return `primitive-${kind}-${createHash("sha256").update(canonicalJson(content)).update(`:t${Math.floor(now / DERIVED_IDEMPOTENCY_WINDOW_MS)}`).digest("hex")}`;
|
|
4090
3966
|
}
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
3967
|
+
/**
|
|
3968
|
+
* Identity fields every send, reply and chat `--json` envelope carries,
|
|
3969
|
+
* whatever the outcome. `sent_email_id` is null until a send record is
|
|
3970
|
+
* known; `idempotency_key` is known before the request starts, so an
|
|
3971
|
+
* uncertain outcome can be reconciled with
|
|
3972
|
+
* `primitive sent get --idempotency-key <key>`.
|
|
3973
|
+
*/
|
|
3974
|
+
function sendIdentityFields(params) {
|
|
4099
3975
|
return {
|
|
4100
|
-
|
|
4101
|
-
|
|
3976
|
+
sent_email_id: params.sent?.id ?? null,
|
|
3977
|
+
idempotency_key: params.idempotencyKey ?? params.sent?.client_idempotency_key ?? null
|
|
4102
3978
|
};
|
|
4103
3979
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
3980
|
+
/**
|
|
3981
|
+
* Commands that inspect an existing send. None of them send again:
|
|
3982
|
+
* a caller that follows them after a failure cannot double-send.
|
|
3983
|
+
*/
|
|
3984
|
+
function buildSendCommandFollowUps(params) {
|
|
3985
|
+
const commands = [];
|
|
3986
|
+
if (params.sentId === null && params.outcome === "uncertain" && params.idempotencyKey) commands.push(buildFollowUpCommand("find_sent_email_by_idempotency_key", "Look up this attempt by its idempotency key before retrying", [
|
|
3987
|
+
"primitive",
|
|
3988
|
+
"sent",
|
|
3989
|
+
"get",
|
|
3990
|
+
"--idempotency-key",
|
|
3991
|
+
params.idempotencyKey
|
|
3992
|
+
]));
|
|
3993
|
+
if (params.sentId !== null) commands.push(buildFollowUpCommand("inspect_sent_email", "Inspect the send and its delivery status", [
|
|
3994
|
+
"primitive",
|
|
3995
|
+
"sent",
|
|
3996
|
+
"get",
|
|
3997
|
+
"--id",
|
|
3998
|
+
params.sentId
|
|
3999
|
+
]));
|
|
4000
|
+
if (params.outcome === "uncertain") commands.push(buildFollowUpCommand("list_recent_sent_emails", "Check sent history for this attempt before retrying", [
|
|
4001
|
+
"primitive",
|
|
4002
|
+
"sent",
|
|
4003
|
+
"list",
|
|
4004
|
+
"--date-from",
|
|
4005
|
+
sentHistoryWindowStart(params.attemptStartedAtIso)
|
|
4006
|
+
]));
|
|
4007
|
+
return commands;
|
|
4115
4008
|
}
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4009
|
+
/**
|
|
4010
|
+
* Classify a `send` / `reply` API result, write the stderr summary,
|
|
4011
|
+
* and print stdout. Without `--json` stdout stays exactly what these
|
|
4012
|
+
* commands always printed (the SendMailResult, or `null`), so
|
|
4013
|
+
* existing `| jq` pipelines keep working; the outcome is on stderr
|
|
4014
|
+
* and in the exit code. With `--json` stdout is always an envelope,
|
|
4015
|
+
* whatever the outcome, and the summary is only its outcome_message.
|
|
4016
|
+
*/
|
|
4017
|
+
function reportSendCommandResult(params) {
|
|
4018
|
+
const httpStatus = apiResultHttpStatus(params.result);
|
|
4019
|
+
let outcome;
|
|
4020
|
+
let outcomeMessage;
|
|
4021
|
+
let sent = null;
|
|
4022
|
+
let errorPayload = null;
|
|
4023
|
+
if (params.result.error) {
|
|
4024
|
+
errorPayload = extractErrorPayload(params.result.error);
|
|
4025
|
+
params.onApiError(errorPayload);
|
|
4026
|
+
const failure = classifySendError(httpStatus, errorPayload);
|
|
4027
|
+
outcome = failure;
|
|
4028
|
+
outcomeMessage = failure === "already_sent" ? formatDeletedEarlierSendNotice() : formatSendFailureSummary(params.noun, failure, httpStatus);
|
|
4029
|
+
if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
|
|
4030
|
+
} else {
|
|
4031
|
+
sent = params.result.data?.data ?? null;
|
|
4032
|
+
if (sent === null) {
|
|
4033
|
+
outcome = "uncertain";
|
|
4034
|
+
outcomeMessage = `${params.noun} send outcome uncertain: the API accepted the request but returned no send record. Do not resend blindly; check sent history first.`;
|
|
4035
|
+
errorPayload = { message: "The API returned no send record." };
|
|
4036
|
+
} else {
|
|
4037
|
+
outcome = successfulSendOutcome(sent);
|
|
4038
|
+
outcomeMessage = outcome === "not_sent" || outcome === "uncertain" ? formatSendRecordFailureSummary(params.noun, outcome, sent) : outcome === "already_sent" ? formatAlreadySentNotice(sent) : formatSentSummary(params.noun, sent);
|
|
4039
|
+
}
|
|
4040
|
+
if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
|
|
4126
4041
|
}
|
|
4042
|
+
if (params.json) {
|
|
4043
|
+
const envelope = {
|
|
4044
|
+
outcome,
|
|
4045
|
+
exit_code: sendOutcomeExitCode(outcome),
|
|
4046
|
+
outcome_message: outcomeMessage,
|
|
4047
|
+
...sendIdentityFields({
|
|
4048
|
+
idempotencyKey: params.idempotencyKey,
|
|
4049
|
+
sent
|
|
4050
|
+
}),
|
|
4051
|
+
sent,
|
|
4052
|
+
http_status: httpStatus ?? null,
|
|
4053
|
+
error: serializeErrorPayload(errorPayload),
|
|
4054
|
+
follow_up_commands: buildSendCommandFollowUps({
|
|
4055
|
+
attemptStartedAtIso: params.attemptStartedAtIso,
|
|
4056
|
+
idempotencyKey: params.idempotencyKey,
|
|
4057
|
+
outcome,
|
|
4058
|
+
sentId: sent?.id ?? null
|
|
4059
|
+
}),
|
|
4060
|
+
...params.extraEnvelopeFields
|
|
4061
|
+
};
|
|
4062
|
+
params.log(JSON.stringify(envelope, null, 2));
|
|
4063
|
+
} else if (!params.result.error) params.log(JSON.stringify(sent, null, 2));
|
|
4064
|
+
return outcome;
|
|
4127
4065
|
}
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
};
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
if (!code) return;
|
|
4139
|
-
if (code in ERROR_CODE_HINTS) {
|
|
4140
|
-
const hint = ERROR_CODE_HINTS[code];
|
|
4141
|
-
process.stderr.write(`${hint}\n`);
|
|
4142
|
-
return;
|
|
4143
|
-
}
|
|
4144
|
-
if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
|
|
4066
|
+
/** JSON-safe form of an API error payload; `Error` objects stringify to `{}`. */
|
|
4067
|
+
function serializeErrorPayload(payload) {
|
|
4068
|
+
if (!(payload instanceof Error)) return payload ?? null;
|
|
4069
|
+
const serialized = {
|
|
4070
|
+
name: payload.name,
|
|
4071
|
+
message: payload.message
|
|
4072
|
+
};
|
|
4073
|
+
const cause = payload.cause;
|
|
4074
|
+
if (cause && typeof cause.code === "string") serialized.code = cause.code;
|
|
4075
|
+
return serialized;
|
|
4145
4076
|
}
|
|
4146
4077
|
/**
|
|
4147
|
-
*
|
|
4148
|
-
*
|
|
4149
|
-
*
|
|
4150
|
-
*
|
|
4151
|
-
*
|
|
4152
|
-
* surface a hint and let the user decide whether to re-authenticate.
|
|
4153
|
-
*
|
|
4154
|
-
* The one legitimate auto-delete case lives in `primitive login`'s
|
|
4155
|
-
* `checkExistingLogin`: the user has explicitly asked to log in,
|
|
4156
|
-
* existing credentials are probed, and if they fail we clean up
|
|
4157
|
-
* before minting a new key. That path calls `deleteCliCredentials`
|
|
4158
|
-
* directly rather than going through this function.
|
|
4078
|
+
* Envelope for a `send` / `reply` that threw instead of returning an
|
|
4079
|
+
* API result. Printed in --json mode so stdout carries an envelope for
|
|
4080
|
+
* every outcome. Before the request starts (bad input, missing auth,
|
|
4081
|
+
* unreadable attachment) nothing went out; after it starts the
|
|
4082
|
+
* outcome is unknown.
|
|
4159
4083
|
*/
|
|
4160
|
-
function
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4084
|
+
function buildThrownSendFailureEnvelope(params) {
|
|
4085
|
+
const message = params.error instanceof Error ? params.error.message : String(params.error);
|
|
4086
|
+
const outcome = params.requestStarted ? "uncertain" : "not_sent";
|
|
4087
|
+
return {
|
|
4088
|
+
outcome,
|
|
4089
|
+
exit_code: params.requestStarted ? sendOutcomeExitCode("uncertain") : thrownErrorExitCode(params.error),
|
|
4090
|
+
outcome_message: formatSendFailureSummary(params.noun, outcome, void 0),
|
|
4091
|
+
...sendIdentityFields({
|
|
4092
|
+
idempotencyKey: params.idempotencyKey,
|
|
4093
|
+
sent: null
|
|
4094
|
+
}),
|
|
4095
|
+
sent: null,
|
|
4096
|
+
http_status: null,
|
|
4097
|
+
error: { message },
|
|
4098
|
+
follow_up_commands: params.requestStarted && params.attemptStartedAtIso ? buildSendCommandFollowUps({
|
|
4099
|
+
attemptStartedAtIso: params.attemptStartedAtIso,
|
|
4100
|
+
idempotencyKey: params.idempotencyKey,
|
|
4101
|
+
outcome,
|
|
4102
|
+
sentId: null
|
|
4103
|
+
}) : [],
|
|
4104
|
+
...params.extraEnvelopeFields
|
|
4105
|
+
};
|
|
4173
4106
|
}
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
const
|
|
4177
|
-
|
|
4178
|
-
return await fn();
|
|
4179
|
-
} finally {
|
|
4180
|
-
process.stderr.write(`[time: ${formatElapsed(Date.now() - start)}]\n`);
|
|
4181
|
-
}
|
|
4107
|
+
/** Exit code oclif will use for a thrown error (2 for usage errors). */
|
|
4108
|
+
function thrownErrorExitCode(error) {
|
|
4109
|
+
const exit = error?.oclif?.exit;
|
|
4110
|
+
return typeof exit === "number" ? exit : 1;
|
|
4182
4111
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
const
|
|
4186
|
-
"
|
|
4187
|
-
"
|
|
4188
|
-
"
|
|
4189
|
-
"
|
|
4190
|
-
"envelope",
|
|
4191
|
-
"output"
|
|
4112
|
+
//#endregion
|
|
4113
|
+
//#region src/oclif/interaction-actions.ts
|
|
4114
|
+
const HINTS = new Set([
|
|
4115
|
+
"status",
|
|
4116
|
+
"card",
|
|
4117
|
+
"none",
|
|
4118
|
+
"pending"
|
|
4192
4119
|
]);
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
if (field.kind === "boolean") return Flags.boolean({
|
|
4199
|
-
...common,
|
|
4200
|
-
allowNo: true
|
|
4201
|
-
});
|
|
4202
|
-
if (field.kind === "integer") return Flags.integer({
|
|
4203
|
-
...common,
|
|
4204
|
-
...numericFlagOptions(field)
|
|
4205
|
-
});
|
|
4206
|
-
if (field.kind === "number") return numberFlag({
|
|
4207
|
-
...common,
|
|
4208
|
-
...numericFlagOptions(field)
|
|
4209
|
-
});
|
|
4210
|
-
if (field.enumValues) return Flags.string({
|
|
4211
|
-
...common,
|
|
4212
|
-
options: field.enumValues
|
|
4213
|
-
});
|
|
4214
|
-
return Flags.string(common);
|
|
4120
|
+
/** A printable `<protocol>/<version>`; anything else is treated as unknown. */
|
|
4121
|
+
const KIND = /^[a-z][a-z0-9._-]{0,63}\/[1-9][0-9]{0,3}$/;
|
|
4122
|
+
const UUID$2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4123
|
+
function record$1(value) {
|
|
4124
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4215
4125
|
}
|
|
4216
|
-
function
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
"
|
|
4223
|
-
|
|
4224
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
4225
|
-
hidden: true
|
|
4226
|
-
}),
|
|
4227
|
-
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
4228
|
-
};
|
|
4229
|
-
if (!operation.binaryResponse) {
|
|
4230
|
-
flags.json = Flags.boolean({ description: "Print exactly one JSON document on stdout, on success and on failure, and nothing on stderr. Stdout keeps its usual shape: the data payload, or with --envelope the full response envelope (data plus meta, including meta.cursor, and empty-result hints in summary). The next-cursor line is not printed; use --envelope to page. A failure prints the error with exit_code. Without --json, the cursor and notices go to stderr." });
|
|
4231
|
-
flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
|
|
4232
|
-
}
|
|
4233
|
-
for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
|
|
4234
|
-
const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
|
|
4235
|
-
if (operation.hasJsonBody) {
|
|
4236
|
-
flags["raw-body"] = Flags.string({ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available." });
|
|
4237
|
-
flags["body-file"] = Flags.string({ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload." });
|
|
4238
|
-
const bodyFields = extractBodyFields(operation.requestSchema);
|
|
4239
|
-
const aliasesForOperation = OPERATION_FLAG_ALIASES[operation.sdkName];
|
|
4240
|
-
for (const field of bodyFields) {
|
|
4241
|
-
if (field.kind === "complex") continue;
|
|
4242
|
-
const name = flagName(field.name);
|
|
4243
|
-
if (RESERVED_FLAG_NAMES.has(name)) continue;
|
|
4244
|
-
if (flags[name] !== void 0) continue;
|
|
4245
|
-
flags[name] = bodyFieldFlag(field, aliasesForOperation?.[field.name]);
|
|
4246
|
-
bodyFieldFlagToProperty.set(name, field.name);
|
|
4247
|
-
}
|
|
4126
|
+
function categoryOf(hint, kind, fyi, candidate) {
|
|
4127
|
+
if (hint === "pending") return candidate ? "pending" : "ordinary";
|
|
4128
|
+
if (hint === "card") switch (kind) {
|
|
4129
|
+
case "x402.payment/1": return "payment";
|
|
4130
|
+
case "primitive.contact/1": return "contact";
|
|
4131
|
+
case "repeat.tick/1": return "repeat";
|
|
4132
|
+
case "repeat.stop/1": return "repeat_stopped";
|
|
4133
|
+
default: return "unsupported";
|
|
4248
4134
|
}
|
|
4249
|
-
if (
|
|
4250
|
-
if (
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4135
|
+
if (fyi) return "fyi";
|
|
4136
|
+
if (hint === "status") return "signal";
|
|
4137
|
+
return "ordinary";
|
|
4138
|
+
}
|
|
4139
|
+
function replyExpectation(category) {
|
|
4140
|
+
switch (category) {
|
|
4141
|
+
case "fyi":
|
|
4142
|
+
case "signal":
|
|
4143
|
+
case "repeat_stopped": return "no_reply";
|
|
4144
|
+
case "payment":
|
|
4145
|
+
case "contact": return "answer_with_command";
|
|
4146
|
+
case "repeat": return "repeat";
|
|
4147
|
+
case "unsupported": return "unsupported";
|
|
4148
|
+
case "pending": return "read_again";
|
|
4149
|
+
default: return "reply";
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* The server's interaction facts for an email read, or null when the server
|
|
4154
|
+
* did not report `interaction_hint` (an older server). An unfamiliar hint is
|
|
4155
|
+
* treated as `none`, as the API documents.
|
|
4156
|
+
*/
|
|
4157
|
+
function readEmailInteraction(detail) {
|
|
4158
|
+
const row = record$1(detail);
|
|
4159
|
+
if (!row || typeof row.interaction_hint !== "string") return null;
|
|
4160
|
+
const hint = HINTS.has(row.interaction_hint) ? row.interaction_hint : "none";
|
|
4161
|
+
const rawKind = row.interaction_kind;
|
|
4162
|
+
const kind = (hint === "card" || hint === "status") && typeof rawKind === "string" && KIND.test(rawKind) ? rawKind : null;
|
|
4163
|
+
const fyi = row.fyi === true || record$1(row.collaboration)?.fyi === true;
|
|
4164
|
+
const category = categoryOf(hint, kind, fyi, row.interaction_candidate === true);
|
|
4165
|
+
const expectation = replyExpectation(category);
|
|
4255
4166
|
return {
|
|
4256
|
-
|
|
4257
|
-
|
|
4167
|
+
hint,
|
|
4168
|
+
kind,
|
|
4169
|
+
fyi,
|
|
4170
|
+
category,
|
|
4171
|
+
plain_reply_completes: expectation === "reply" || expectation === "repeat",
|
|
4172
|
+
no_reply_needed: expectation === "no_reply"
|
|
4258
4173
|
};
|
|
4259
4174
|
}
|
|
4260
|
-
function
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4175
|
+
function replyAction(bin, id) {
|
|
4176
|
+
return buildFollowUpCommand("reply", "Reply to the sender", [
|
|
4177
|
+
bin,
|
|
4178
|
+
"reply",
|
|
4179
|
+
"--id",
|
|
4180
|
+
id,
|
|
4181
|
+
"--body",
|
|
4182
|
+
"<message>"
|
|
4183
|
+
], { requiresMessage: true });
|
|
4268
4184
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4185
|
+
/**
|
|
4186
|
+
* The commands that answer an email, best first. Empty when the email needs
|
|
4187
|
+
* no reply or this CLI cannot answer it. `repeatStoppable: false` (the
|
|
4188
|
+
* repeat's own part says only the sender can stop it) leaves out the stop.
|
|
4189
|
+
*/
|
|
4190
|
+
function interactionNextActions(interaction, emailId, options = {}) {
|
|
4191
|
+
const bin = options.bin ?? "primitive";
|
|
4192
|
+
if (!UUID$2.test(emailId)) return [];
|
|
4193
|
+
const id = emailId.toLowerCase();
|
|
4194
|
+
switch (interaction?.category ?? "ordinary") {
|
|
4195
|
+
case "ordinary": return [replyAction(bin, id)];
|
|
4196
|
+
case "payment": return [buildFollowUpCommand("inspect_payment", "Show the payment request's terms before paying", [
|
|
4197
|
+
bin,
|
|
4198
|
+
"payments",
|
|
4199
|
+
"challenge-from-email",
|
|
4200
|
+
"--id",
|
|
4201
|
+
id
|
|
4202
|
+
]), buildFollowUpCommand("pay", "Pay the payment request (signs with your wallet key and sends the payment)", [
|
|
4203
|
+
bin,
|
|
4204
|
+
"payments",
|
|
4205
|
+
"pay-email",
|
|
4206
|
+
"--in-reply-to",
|
|
4207
|
+
id
|
|
4208
|
+
])];
|
|
4209
|
+
case "contact": return [buildFollowUpCommand("accept_contact", "Accept the contact request, if the owner's policy allows it", [
|
|
4210
|
+
bin,
|
|
4211
|
+
"contacts",
|
|
4212
|
+
"accept",
|
|
4213
|
+
"--id",
|
|
4214
|
+
id
|
|
4215
|
+
])];
|
|
4216
|
+
case "repeat": {
|
|
4217
|
+
const actions = [replyAction(bin, id)];
|
|
4218
|
+
if (options.repeatStoppable !== false) actions.push(buildFollowUpCommand("stop_repeat", "Stop the repeating message once its goal is met", [
|
|
4219
|
+
bin,
|
|
4220
|
+
"repeat",
|
|
4221
|
+
"stop",
|
|
4222
|
+
"--id",
|
|
4223
|
+
id
|
|
4224
|
+
]));
|
|
4225
|
+
return actions;
|
|
4226
|
+
}
|
|
4227
|
+
case "pending": return [buildFollowUpCommand("read_again", "Read the email again once the server has finished checking it", [
|
|
4228
|
+
bin,
|
|
4229
|
+
"emails",
|
|
4230
|
+
"get",
|
|
4231
|
+
"--id",
|
|
4232
|
+
id,
|
|
4233
|
+
"--brief"
|
|
4234
|
+
])];
|
|
4235
|
+
default: return [];
|
|
4274
4236
|
}
|
|
4275
|
-
return values;
|
|
4276
|
-
}
|
|
4277
|
-
function operationOutputPayload(envelope, includeEnvelope) {
|
|
4278
|
-
return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
|
|
4279
4237
|
}
|
|
4280
4238
|
/**
|
|
4281
|
-
*
|
|
4282
|
-
*
|
|
4283
|
-
*
|
|
4239
|
+
* One human line naming how to answer the email, or null for ordinary mail
|
|
4240
|
+
* (and older servers), where `primitive reply` is the answer. Contains only
|
|
4241
|
+
* server-decided values and fixed text, never anything the sender wrote.
|
|
4284
4242
|
*/
|
|
4285
|
-
function
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4243
|
+
function interactionHumanLine(interaction, actions) {
|
|
4244
|
+
if (!interaction) return null;
|
|
4245
|
+
const command = (kind) => actions.find((action) => action.kind === kind)?.command;
|
|
4246
|
+
const kind = interaction.kind ?? "unknown kind";
|
|
4247
|
+
switch (interaction.category) {
|
|
4248
|
+
case "fyi": return "Informational (fyi): no reply needed, not even another fyi.";
|
|
4249
|
+
case "signal": return `Status signal (${kind}): no reply needed.`;
|
|
4250
|
+
case "repeat_stopped": return `Notice that a repeating message stopped (${kind}): no reply needed.`;
|
|
4251
|
+
case "payment": {
|
|
4252
|
+
const inspect = command("inspect_payment");
|
|
4253
|
+
const pay = command("pay");
|
|
4254
|
+
return inspect && pay ? `Payment interaction (${kind}). If it requests payment, review it with ${inspect} and pay with ${pay}. A plain reply does not pay or decline it.` : null;
|
|
4255
|
+
}
|
|
4256
|
+
case "contact": {
|
|
4257
|
+
const accept = command("accept_contact");
|
|
4258
|
+
return accept ? `Contact interaction (${kind}). If it is a contact request, accept it under the owner's policy with ${accept}. A plain reply does not accept it.` : null;
|
|
4259
|
+
}
|
|
4260
|
+
case "repeat": {
|
|
4261
|
+
const stop = command("stop_repeat");
|
|
4262
|
+
return stop ? `Repeating message (${kind}). Reply if it asks for an answer; stop it once its goal is met with ${stop}.` : `Repeating message (${kind}). Reply if it asks for an answer; only the sender can stop it.`;
|
|
4263
|
+
}
|
|
4264
|
+
case "unsupported": return `Interaction (${kind}) that this CLI cannot answer. A plain reply does not complete it.`;
|
|
4265
|
+
case "pending": {
|
|
4266
|
+
const again = command("read_again");
|
|
4267
|
+
return again ? `Primitive has not finished checking this email for an interaction. Read it again before answering: ${again}` : null;
|
|
4268
|
+
}
|
|
4269
|
+
default: return null;
|
|
4270
|
+
}
|
|
4289
4271
|
}
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
const
|
|
4293
|
-
if (!
|
|
4294
|
-
|
|
4272
|
+
/** The label a wake notice carries: the interaction kind, `fyi`, or null. */
|
|
4273
|
+
function wakeInteractionLabel(detail) {
|
|
4274
|
+
const interaction = readEmailInteraction(detail);
|
|
4275
|
+
if (!interaction) return null;
|
|
4276
|
+
if (interaction.hint === "card") return interaction.kind ?? "unknown";
|
|
4277
|
+
if (interaction.fyi) return "fyi";
|
|
4278
|
+
return null;
|
|
4295
4279
|
}
|
|
4296
|
-
|
|
4297
|
-
|
|
4280
|
+
/** The category a wake label stands for, or null for an invalid label. */
|
|
4281
|
+
function wakeLabelCategory(label) {
|
|
4282
|
+
if (!isWakeInteractionLabel(label)) return null;
|
|
4283
|
+
if (label === "fyi") return "fyi";
|
|
4284
|
+
return categoryOf("card", label === "unknown" ? null : label, false, false);
|
|
4298
4285
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
setMemory: "Tip: prefer `primitive memories set <key> <json-value> [--function <function-id>]` for JSON value parsing and scope flags. This raw command exists for callers passing full request JSON.",
|
|
4311
|
-
getMemory: "Tip: prefer `primitive memories get <key> [--function <function-id>]` for the common read path.",
|
|
4312
|
-
deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
|
|
4313
|
-
searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
|
|
4314
|
-
startAgentSignup: "Tip: pass --terms-accepted, and optionally --signup-code <code> if you have one. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
|
|
4315
|
-
verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
|
|
4286
|
+
/**
|
|
4287
|
+
* The fixed wake sentences, keyed by reply expectation. The Claude hook
|
|
4288
|
+
* scripts in bin/ carry copies (they run standalone); a test keeps them equal.
|
|
4289
|
+
*/
|
|
4290
|
+
const WAKE_SENTENCES = {
|
|
4291
|
+
reply: "",
|
|
4292
|
+
read_again: "",
|
|
4293
|
+
no_reply: " It needs no reply.",
|
|
4294
|
+
answer_with_command: " It is an interaction a plain reply does not complete; the brief names the command that answers it.",
|
|
4295
|
+
repeat: " It is a repeating message; the brief says how to answer it and whether you can stop it.",
|
|
4296
|
+
unsupported: " It is an interaction this CLI cannot answer; a plain reply does not complete it."
|
|
4316
4297
|
};
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
signup: data
|
|
4325
|
-
});
|
|
4326
|
-
writeStderr("Credentials saved to the CLI config; `primitive whoami` will work on the next call.\n");
|
|
4327
|
-
} };
|
|
4328
|
-
function createOperationCommand(operation) {
|
|
4329
|
-
const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
|
|
4330
|
-
const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
|
|
4331
|
-
const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
|
|
4332
|
-
const hint = OPERATION_HINTS[operation.sdkName];
|
|
4333
|
-
const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
|
|
4334
|
-
const fullDescription = hint ? `${descriptionWithSchema}\n\n${hint}` : descriptionWithSchema;
|
|
4335
|
-
class OperationCommand extends Command {
|
|
4336
|
-
static description = fullDescription;
|
|
4337
|
-
static flags = flags;
|
|
4338
|
-
static summary = operation.summary ?? `${operation.method} ${operation.path}`;
|
|
4339
|
-
async run() {
|
|
4340
|
-
const { flags } = await this.parse(OperationCommand);
|
|
4341
|
-
const parsedFlags = flags;
|
|
4342
|
-
if ([
|
|
4343
|
-
"startAgentSignup",
|
|
4344
|
-
"verifyAgentSignup",
|
|
4345
|
-
"resendAgentSignupVerification"
|
|
4346
|
-
].includes(operation.operationId)) requireDefaultLoginProfile();
|
|
4347
|
-
await runWithTiming(parsedFlags.time === true, async () => {
|
|
4348
|
-
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
4349
|
-
apiKey: typeof parsedFlags["api-key"] === "string" ? parsedFlags["api-key"] : void 0,
|
|
4350
|
-
apiBaseUrl: typeof parsedFlags["api-base-url"] === "string" ? parsedFlags["api-base-url"] : void 0,
|
|
4351
|
-
configDir: this.config.configDir
|
|
4352
|
-
});
|
|
4353
|
-
let body;
|
|
4354
|
-
if (operation.hasJsonBody) {
|
|
4355
|
-
const explicit = readJsonBody(parsedFlags);
|
|
4356
|
-
const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
|
|
4357
|
-
if (Object.keys(overrides).length > 0) if (explicit === void 0) body = overrides;
|
|
4358
|
-
else if (explicit !== null && typeof explicit === "object" && !Array.isArray(explicit)) body = {
|
|
4359
|
-
...explicit,
|
|
4360
|
-
...overrides
|
|
4361
|
-
};
|
|
4362
|
-
else {
|
|
4363
|
-
const explicitKind = explicit === null ? "null" : Array.isArray(explicit) ? "array" : typeof explicit;
|
|
4364
|
-
const overrideFlags = Object.keys(overrides).map((p) => `--${flagName(p)}`).join(", ");
|
|
4365
|
-
throw new Errors.CLIError(`--raw-body must be a JSON object when also passing per-field flags (got ${explicitKind}); supplied per-field flags: ${overrideFlags}. Either drop --raw-body and rely on the per-field flags, or move every field into the JSON --raw-body and drop the flags.`);
|
|
4366
|
-
}
|
|
4367
|
-
else body = explicit;
|
|
4368
|
-
}
|
|
4369
|
-
if (operation.bodyRequired && body === void 0) throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --raw-body / --body-file.`);
|
|
4370
|
-
const operationFn = sdk_gen_exports[operation.sdkName];
|
|
4371
|
-
const query = collectValues(operation.queryParams, parsedFlags);
|
|
4372
|
-
const replyStateSurface = replyStateSurfaceForOperation(operation.sdkName, query);
|
|
4373
|
-
const automatedSurface = automatedSurfaceForOperation(operation.sdkName, query);
|
|
4374
|
-
const callOperation = () => operationFn({
|
|
4375
|
-
body,
|
|
4376
|
-
client: apiClient.client,
|
|
4377
|
-
parseAs: operation.binaryResponse ? "blob" : "auto",
|
|
4378
|
-
path: collectValues(operation.pathParams, parsedFlags),
|
|
4379
|
-
query,
|
|
4380
|
-
responseStyle: "fields"
|
|
4381
|
-
});
|
|
4382
|
-
const result = ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
|
|
4383
|
-
wait: parsedFlags.wait !== false,
|
|
4384
|
-
errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
|
|
4385
|
-
onFirstWait: () => {
|
|
4386
|
-
process.stderr.write(`Attachment content is not ready yet; waiting up to ${ATTACHMENT_WAIT_MAX_MS / 1e3}s. Pass --no-wait to return immediately.\n`);
|
|
4387
|
-
}
|
|
4388
|
-
}) : await callOperation();
|
|
4389
|
-
if (result.error) {
|
|
4390
|
-
const errorPayload = extractErrorPayload(result.error);
|
|
4391
|
-
writeErrorWithHints(errorPayload);
|
|
4392
|
-
if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
|
|
4393
|
-
if (automatedSurface && isAutomatedRejectedError(errorPayload)) process.stderr.write(`${automatedRejectedError(automatedSurface).message}\n`);
|
|
4394
|
-
surfaceUnauthorizedHint({
|
|
4395
|
-
auth,
|
|
4396
|
-
baseUrlOverridden,
|
|
4397
|
-
configDir: this.config.configDir,
|
|
4398
|
-
payload: errorPayload
|
|
4399
|
-
});
|
|
4400
|
-
const listClient = apiClient.client;
|
|
4401
|
-
const listEndpointsFn = () => listEndpoints({
|
|
4402
|
-
client: listClient,
|
|
4403
|
-
responseStyle: "fields"
|
|
4404
|
-
});
|
|
4405
|
-
await maybeWriteFunctionEndpointRedirect({
|
|
4406
|
-
sdkName: operation.sdkName,
|
|
4407
|
-
errorCode: extractErrorCode(errorPayload),
|
|
4408
|
-
endpointId: typeof parsedFlags.id === "string" ? parsedFlags.id : void 0,
|
|
4409
|
-
listEndpoints: listEndpointsFn,
|
|
4410
|
-
writeStderr: (chunk) => {
|
|
4411
|
-
process.stderr.write(chunk);
|
|
4412
|
-
}
|
|
4413
|
-
});
|
|
4414
|
-
process.exitCode = 1;
|
|
4415
|
-
return;
|
|
4416
|
-
}
|
|
4417
|
-
if (operation.binaryResponse) {
|
|
4418
|
-
const blob = result.data;
|
|
4419
|
-
const bytes = Buffer.from(await blob.arrayBuffer());
|
|
4420
|
-
const output = parsedFlags.output;
|
|
4421
|
-
if (typeof output === "string") {
|
|
4422
|
-
writeFileSync(output, bytes);
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4425
|
-
process.stdout.write(bytes);
|
|
4426
|
-
return;
|
|
4427
|
-
}
|
|
4428
|
-
const envelope = result.data;
|
|
4429
|
-
if (replyStateSurface) try {
|
|
4430
|
-
const rows = Array.isArray(envelope?.data) ? envelope.data : [];
|
|
4431
|
-
assertAwaitingFilterRows(rows, replyStateSurface);
|
|
4432
|
-
if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsReplyState(apiClient);
|
|
4433
|
-
} catch (error) {
|
|
4434
|
-
if (!(error instanceof ReplyStateUnsupportedError)) throw error;
|
|
4435
|
-
process.stderr.write(`${error.message}\n`);
|
|
4436
|
-
process.exitCode = 1;
|
|
4437
|
-
return;
|
|
4438
|
-
}
|
|
4439
|
-
if (automatedSurface) try {
|
|
4440
|
-
const rows = Array.isArray(envelope?.data) ? envelope.data : [];
|
|
4441
|
-
assertAutomatedVerdict(rows, automatedSurface, expectedAutomatedVerdicts(query?.automated === "true" ? true : query?.automated === "false" ? false : void 0, typeof query?.q === "string" ? query.q : void 0));
|
|
4442
|
-
if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsAutomated(apiClient);
|
|
4443
|
-
} catch (error) {
|
|
4444
|
-
if (!(error instanceof AutomatedFilterUnsupportedError)) throw error;
|
|
4445
|
-
process.stderr.write(`${error.message}\n`);
|
|
4446
|
-
process.exitCode = 1;
|
|
4447
|
-
return;
|
|
4448
|
-
}
|
|
4449
|
-
const jsonOutput = parsedFlags.json === true;
|
|
4450
|
-
const withEnvelope = parsedFlags.envelope === true;
|
|
4451
|
-
const cursor = envelope?.meta?.cursor;
|
|
4452
|
-
if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
|
|
4453
|
-
const summary = [];
|
|
4454
|
-
if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
|
|
4455
|
-
const hint = EMPTY_RESULT_HINTS[operation.sdkName];
|
|
4456
|
-
if (hint && jsonOutput) {
|
|
4457
|
-
if (withEnvelope) summary.push(hint);
|
|
4458
|
-
} else if (hint) process.stderr.write(`${hint}\n`);
|
|
4459
|
-
}
|
|
4460
|
-
writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
|
|
4461
|
-
process.stderr.write(chunk);
|
|
4462
|
-
} });
|
|
4463
|
-
const successHook = OPERATION_SUCCESS_HOOKS[operation.sdkName];
|
|
4464
|
-
if (successHook) try {
|
|
4465
|
-
successHook({
|
|
4466
|
-
envelope,
|
|
4467
|
-
configDir: this.config.configDir,
|
|
4468
|
-
apiBaseUrl: auth.apiBaseUrl,
|
|
4469
|
-
writeStderr: (chunk) => {
|
|
4470
|
-
process.stderr.write(chunk);
|
|
4471
|
-
}
|
|
4472
|
-
});
|
|
4473
|
-
} catch (hookError) {
|
|
4474
|
-
const detail = hookError instanceof Error ? hookError.message : String(hookError);
|
|
4475
|
-
process.stderr.write(`Warning: ${operation.sdkName} succeeded but its post-success hook threw (${detail}). The response below is still valid; act on it manually.\n`);
|
|
4476
|
-
}
|
|
4477
|
-
this.log(JSON.stringify(jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope), null, 2));
|
|
4478
|
-
if (isIncompleteDomainVerification(operation, envelope)) {
|
|
4479
|
-
writeIncompleteDomainVerificationHint();
|
|
4480
|
-
process.exitCode = 1;
|
|
4481
|
-
}
|
|
4482
|
-
});
|
|
4483
|
-
}
|
|
4484
|
-
}
|
|
4485
|
-
return OperationCommand;
|
|
4486
|
-
}
|
|
4487
|
-
const EMPTY_RESULT_HINTS = {
|
|
4488
|
-
listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints list` to check.",
|
|
4489
|
-
listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
|
|
4490
|
-
listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
|
|
4491
|
-
listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
|
|
4492
|
-
listFilters: "(no results) No filter rules configured.",
|
|
4493
|
-
listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
|
|
4494
|
-
};
|
|
4495
|
-
function canonicalizeCliReferences(description) {
|
|
4496
|
-
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'`");
|
|
4298
|
+
/**
|
|
4299
|
+
* The sentence a wake line adds after its read command for an interaction
|
|
4300
|
+
* label: empty for ordinary mail and for anything that is not a valid label.
|
|
4301
|
+
*/
|
|
4302
|
+
function wakeInteractionSentence(label) {
|
|
4303
|
+
const category = wakeLabelCategory(label);
|
|
4304
|
+
return category ? WAKE_SENTENCES[replyExpectation(category)] : "";
|
|
4497
4305
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
replied: 0,
|
|
4502
|
-
sent: 0,
|
|
4503
|
-
already_sent: 0,
|
|
4504
|
-
not_sent: 1,
|
|
4505
|
-
sent_awaiting_reply: 3,
|
|
4506
|
-
uncertain: 4
|
|
4507
|
-
};
|
|
4508
|
-
const DEFINITIVE_SEND_REJECTION_STATUSES = [
|
|
4509
|
-
400,
|
|
4510
|
-
401,
|
|
4511
|
-
402,
|
|
4512
|
-
403,
|
|
4513
|
-
404,
|
|
4514
|
-
413,
|
|
4515
|
-
422,
|
|
4516
|
-
429
|
|
4517
|
-
];
|
|
4518
|
-
const SEND_OUTCOME_HELP = `Outcomes and exit codes, shared by chat, chat reply, send and reply (JSON output carries the name as "outcome"):
|
|
4519
|
-
- exit 0 replied: chat only. The message was sent and a reply arrived.
|
|
4520
|
-
- exit 0 sent: accepted for delivery. A queued status counts as sent.
|
|
4521
|
-
- exit 0 already_sent: an identical earlier send exists (or was deleted: HTTP 410 sent_email_deleted). Nothing new went out. Do not resend.
|
|
4522
|
-
- exit 1 not_sent: the API rejected the request (HTTP 400, 401, 402, 403, 404, 413, 422 or 429), the command failed before sending, or the send record has status agent_failed, gate_denied or canceled. Nothing went out.
|
|
4523
|
-
- exit 2: invalid flags or arguments. Nothing went out.
|
|
4524
|
-
- exit 3 sent_awaiting_reply: chat only. Sent, but no reply before the timeout. Wait; do not resend.
|
|
4525
|
-
- exit 4 uncertain: transport error, conflict, server error, or a send record with status unknown. It may or may not have gone out. Reconcile with primitive sent get --idempotency-key <key> (the key is in the --json envelope) before retrying.`;
|
|
4526
|
-
function sendOutcomeExitCode(outcome) {
|
|
4527
|
-
return SEND_OUTCOME_EXIT_CODES[outcome];
|
|
4306
|
+
/** Accepts only labels `wakeInteractionLabel` can produce. */
|
|
4307
|
+
function isWakeInteractionLabel(value) {
|
|
4308
|
+
return typeof value === "string" && (value === "fyi" || value === "unknown" || KIND.test(value));
|
|
4528
4309
|
}
|
|
4529
|
-
|
|
4530
|
-
|
|
4310
|
+
/**
|
|
4311
|
+
* Warning for a plain (prose) reply to `detail`, or null when a plain reply
|
|
4312
|
+
* is a fine answer. Never refuses: the caller still sends.
|
|
4313
|
+
*/
|
|
4314
|
+
function replyInteractionWarning(detail, emailId) {
|
|
4315
|
+
const interaction = readEmailInteraction(detail);
|
|
4316
|
+
if (!interaction || !UUID$2.test(emailId)) return null;
|
|
4317
|
+
const id = emailId.toLowerCase();
|
|
4318
|
+
const kind = interaction.kind;
|
|
4319
|
+
const label = kind ?? "unknown kind";
|
|
4320
|
+
const base = {
|
|
4321
|
+
email_id: id,
|
|
4322
|
+
kind,
|
|
4323
|
+
category: interaction.category
|
|
4324
|
+
};
|
|
4325
|
+
const expected = interactionNextActions(interaction, id);
|
|
4326
|
+
const commandFor = (actionKind) => expected.find((action) => action.kind === actionKind)?.command;
|
|
4327
|
+
const expectation = replyExpectation(interaction.category);
|
|
4328
|
+
if (expectation === "reply" || expectation === "repeat") return null;
|
|
4329
|
+
switch (interaction.category) {
|
|
4330
|
+
case "payment": return {
|
|
4331
|
+
...base,
|
|
4332
|
+
code: "interaction_not_answered",
|
|
4333
|
+
message: `Warning: email ${id} is a payment interaction (${label}). A plain reply does not pay or decline it; sending anyway. To pay: ${commandFor("pay")}`,
|
|
4334
|
+
expected
|
|
4335
|
+
};
|
|
4336
|
+
case "contact": return {
|
|
4337
|
+
...base,
|
|
4338
|
+
code: "interaction_not_answered",
|
|
4339
|
+
message: `Warning: email ${id} is a contact interaction (${label}). A plain reply does not accept it; sending anyway. To accept: ${commandFor("accept_contact")}`,
|
|
4340
|
+
expected
|
|
4341
|
+
};
|
|
4342
|
+
case "unsupported": return {
|
|
4343
|
+
...base,
|
|
4344
|
+
code: "interaction_not_answered",
|
|
4345
|
+
message: `Warning: email ${id} is an interaction (${label}) that this CLI cannot answer. A plain reply does not complete it; sending anyway.`,
|
|
4346
|
+
expected
|
|
4347
|
+
};
|
|
4348
|
+
case "fyi": return {
|
|
4349
|
+
...base,
|
|
4350
|
+
code: "reply_not_needed",
|
|
4351
|
+
message: `Warning: email ${id} is informational (fyi) and needs no reply; sending anyway.`,
|
|
4352
|
+
expected
|
|
4353
|
+
};
|
|
4354
|
+
case "signal": return {
|
|
4355
|
+
...base,
|
|
4356
|
+
code: "reply_not_needed",
|
|
4357
|
+
message: `Warning: email ${id} is a status signal (${label}) and needs no reply; sending anyway.`,
|
|
4358
|
+
expected
|
|
4359
|
+
};
|
|
4360
|
+
case "repeat_stopped": return {
|
|
4361
|
+
...base,
|
|
4362
|
+
code: "reply_not_needed",
|
|
4363
|
+
message: `Warning: email ${id} is a notice that a repeating message stopped (${label}) and needs no reply; sending anyway.`,
|
|
4364
|
+
expected
|
|
4365
|
+
};
|
|
4366
|
+
default: return null;
|
|
4367
|
+
}
|
|
4531
4368
|
}
|
|
4532
|
-
const
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
const
|
|
4536
|
-
|
|
4369
|
+
const UUID$1 = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/;
|
|
4370
|
+
const FILE = /^pending-mail-([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\.json$/;
|
|
4371
|
+
function sessionId(value) {
|
|
4372
|
+
const id = value.toLowerCase();
|
|
4373
|
+
if (!UUID$1.test(id)) throw new Error("A pending mail session must be a UUID.");
|
|
4374
|
+
return id;
|
|
4537
4375
|
}
|
|
4538
|
-
function
|
|
4539
|
-
return
|
|
4376
|
+
function pendingMailPath(configDir, profileName, session) {
|
|
4377
|
+
return join(agentProfileDirectory(configDir, profileName), `pending-mail-${sessionId(session)}.json`);
|
|
4540
4378
|
}
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
return isDeletedEarlierSend(httpStatus, errorPayload) ? "already_sent" : sendFailureOutcome(httpStatus);
|
|
4379
|
+
function lockDirectory(configDir, profileName) {
|
|
4380
|
+
return join(agentProfileDirectory(configDir, profileName), ".pending-mail-lock");
|
|
4544
4381
|
}
|
|
4545
|
-
function
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
* Classify a send record the API returned with a 2xx. A 2xx alone does
|
|
4563
|
-
* not mean a message went out: the record's status can show the
|
|
4564
|
-
* attempt was rejected or that its result is indeterminate. The same
|
|
4565
|
-
* holds for an idempotent replay, which returns the earlier attempt's
|
|
4566
|
-
* record as it stands.
|
|
4567
|
-
*/
|
|
4568
|
-
function successfulSendOutcome(sent) {
|
|
4569
|
-
if (SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(sent.status)) return "not_sent";
|
|
4570
|
-
if (SEND_STATUSES_WITH_UNKNOWN_OUTCOME.has(sent.status)) return "uncertain";
|
|
4571
|
-
return sent.idempotent_replay === true ? "already_sent" : "sent";
|
|
4382
|
+
function notice(value) {
|
|
4383
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4384
|
+
const row = value;
|
|
4385
|
+
const kind = row.kind ?? "mail";
|
|
4386
|
+
if (kind !== "mail" && kind !== "status" || (kind === "status" ? typeof row.ref_sent_email_id !== "string" || !UUID$1.test(row.ref_sent_email_id) : row.ref_sent_email_id !== void 0) || typeof row.email_id !== "string" || !UUID$1.test(row.email_id) || typeof row.received_at !== "string" || !Number.isFinite(Date.parse(row.received_at)) || typeof row.sender !== "string" || row.sender.length > 254 || !/^[^\s<>@"]+@[^\s<>@"]+\.[^\s<>@"]+$/.test(row.sender) || !(row.thread_id === null || typeof row.thread_id === "string" && UUID$1.test(row.thread_id)) || typeof row.in_thread !== "boolean" || !(row.newer === null || typeof row.newer === "number" && Number.isSafeInteger(row.newer) && row.newer >= 0)) return null;
|
|
4387
|
+
return {
|
|
4388
|
+
kind,
|
|
4389
|
+
email_id: row.email_id,
|
|
4390
|
+
received_at: row.received_at,
|
|
4391
|
+
sender: row.sender,
|
|
4392
|
+
thread_id: row.thread_id,
|
|
4393
|
+
in_thread: row.in_thread,
|
|
4394
|
+
newer: row.newer,
|
|
4395
|
+
...kind === "status" ? { ref_sent_email_id: row.ref_sent_email_id } : {},
|
|
4396
|
+
...kind === "mail" && isWakeInteractionLabel(row.interaction) ? { interaction: row.interaction } : {},
|
|
4397
|
+
...kind === "mail" && typeof row.not_found_reads === "number" && Number.isSafeInteger(row.not_found_reads) && row.not_found_reads > 0 ? { not_found_reads: row.not_found_reads } : {}
|
|
4398
|
+
};
|
|
4572
4399
|
}
|
|
4573
4400
|
/**
|
|
4574
|
-
*
|
|
4575
|
-
*
|
|
4401
|
+
* Read one session's notices without taking the lock. Writers replace the
|
|
4402
|
+
* file atomically, so a reader sees a whole old or new file, never a partial
|
|
4403
|
+
* one. A corrupt or foreign file reads as empty; only a writer holding the
|
|
4404
|
+
* lock (`repair`) sets it aside with a `.corrupt-<time>` suffix.
|
|
4576
4405
|
*/
|
|
4577
|
-
function
|
|
4578
|
-
const
|
|
4579
|
-
|
|
4580
|
-
|
|
4406
|
+
function readPendingMail(configDir, profileName, session, repair = false) {
|
|
4407
|
+
const path = pendingMailPath(configDir, profileName, session);
|
|
4408
|
+
let text;
|
|
4409
|
+
try {
|
|
4410
|
+
const info = lstatSync(path);
|
|
4411
|
+
if (!info.isFile() || !privateMailPermissions(info)) throw new Error();
|
|
4412
|
+
text = readFileSync(path, "utf8");
|
|
4413
|
+
} catch (error) {
|
|
4414
|
+
if (error.code === "ENOENT") return [];
|
|
4415
|
+
return repair ? setAside(path) : [];
|
|
4416
|
+
}
|
|
4417
|
+
try {
|
|
4418
|
+
const value = JSON.parse(text);
|
|
4419
|
+
if (value?.version !== 1 || value.session_id !== sessionId(session) || !Array.isArray(value.notices)) throw new Error();
|
|
4420
|
+
const notices = value.notices.map(notice);
|
|
4421
|
+
if (notices.some((row) => row === null)) throw new Error();
|
|
4422
|
+
return notices;
|
|
4423
|
+
} catch {
|
|
4424
|
+
return repair ? setAside(path) : [];
|
|
4425
|
+
}
|
|
4581
4426
|
}
|
|
4582
|
-
function
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4427
|
+
function setAside(path) {
|
|
4428
|
+
try {
|
|
4429
|
+
renameSync(path, `${path}.corrupt-${Date.now()}`);
|
|
4430
|
+
} catch {}
|
|
4431
|
+
return [];
|
|
4586
4432
|
}
|
|
4587
|
-
function
|
|
4588
|
-
|
|
4433
|
+
function write(configDir, profileName, session, notices) {
|
|
4434
|
+
const path = pendingMailPath(configDir, profileName, session);
|
|
4435
|
+
if (notices.length === 0) {
|
|
4436
|
+
try {
|
|
4437
|
+
unlinkSync(path);
|
|
4438
|
+
syncMailDirectory(agentProfileDirectory(configDir, profileName));
|
|
4439
|
+
} catch (error) {
|
|
4440
|
+
if (error.code !== "ENOENT") throw error;
|
|
4441
|
+
}
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
writeMailJson(path, {
|
|
4445
|
+
version: 1,
|
|
4446
|
+
session_id: sessionId(session),
|
|
4447
|
+
notices
|
|
4448
|
+
});
|
|
4589
4449
|
}
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4450
|
+
/**
|
|
4451
|
+
* The notice list is full of unread mail. Nothing is dropped: the caller
|
|
4452
|
+
* leaves the event unacknowledged, so the mail stays queued and is
|
|
4453
|
+
* recorded once the session reads some of its notices.
|
|
4454
|
+
*/
|
|
4455
|
+
var PendingMailFullError = class extends Error {
|
|
4456
|
+
constructor() {
|
|
4457
|
+
super(`This session already has 50 unread pending notices. Read some with primitive emails get --id <id>; newer mail stays queued until then.`);
|
|
4458
|
+
}
|
|
4459
|
+
};
|
|
4460
|
+
/**
|
|
4461
|
+
* Add or refresh one notice, deduplicated by email id. Unread mail is never
|
|
4462
|
+
* evicted: a mail notice past PENDING_MAIL_LIMIT fails with
|
|
4463
|
+
* PendingMailFullError. A status notice always fits, using the headroom
|
|
4464
|
+
* above that cap and replacing the oldest status notice when it is full.
|
|
4465
|
+
*/
|
|
4466
|
+
async function recordPendingMail(configDir, profileName, session, value) {
|
|
4467
|
+
const row = notice({
|
|
4468
|
+
...value,
|
|
4469
|
+
email_id: value.email_id.toLowerCase(),
|
|
4470
|
+
...value.ref_sent_email_id ? { ref_sent_email_id: value.ref_sent_email_id.toLowerCase() } : {}
|
|
4471
|
+
});
|
|
4472
|
+
if (!row) throw new Error("Pending mail notice is invalid.");
|
|
4473
|
+
return withMailLock(lockDirectory(configDir, profileName), () => {
|
|
4474
|
+
const next = [...readPendingMail(configDir, profileName, session, true).filter((existing) => existing.email_id !== row.email_id), row];
|
|
4475
|
+
if (row.kind !== "status" && next.filter((existing) => existing.kind !== "status").length > 50) throw new PendingMailFullError();
|
|
4476
|
+
while (next.length > 60) {
|
|
4477
|
+
const status = next.findIndex((existing) => existing !== row && existing.kind === "status");
|
|
4478
|
+
if (status === -1) throw new PendingMailFullError();
|
|
4479
|
+
next.splice(status, 1);
|
|
4480
|
+
}
|
|
4481
|
+
write(configDir, profileName, session, next);
|
|
4482
|
+
return next;
|
|
4483
|
+
});
|
|
4593
4484
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4485
|
+
/** Remove exact email ids from one session's notices; deletes the file when empty. */
|
|
4486
|
+
async function removePendingMail(configDir, profileName, session, emailIds) {
|
|
4487
|
+
const remove = new Set(emailIds.map((id) => id.toLowerCase()));
|
|
4488
|
+
return withMailLock(lockDirectory(configDir, profileName), () => {
|
|
4489
|
+
const current = readPendingMail(configDir, profileName, session, true);
|
|
4490
|
+
const next = current.filter((row) => !remove.has(row.email_id));
|
|
4491
|
+
if (next.length !== current.length) write(configDir, profileName, session, next);
|
|
4492
|
+
return next;
|
|
4493
|
+
});
|
|
4596
4494
|
}
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4495
|
+
/** Session ids that currently have a pending-mail file in the profile. */
|
|
4496
|
+
function pendingMailSessions(configDir, profileName) {
|
|
4497
|
+
try {
|
|
4498
|
+
return readdirSync(agentProfileDirectory(configDir, profileName)).map((name) => FILE.exec(name)?.[1]).filter((id) => id !== void 0);
|
|
4499
|
+
} catch {
|
|
4500
|
+
return [];
|
|
4501
|
+
}
|
|
4601
4502
|
}
|
|
4602
|
-
|
|
4603
|
-
|
|
4503
|
+
/**
|
|
4504
|
+
* Clear one email after a session read it, from that session's file only.
|
|
4505
|
+
* A read with no known runtime session (a terminal or script) clears
|
|
4506
|
+
* nothing: it is not evidence that any session has seen the email, and each
|
|
4507
|
+
* session keeps its notice until it reads the email itself.
|
|
4508
|
+
*/
|
|
4509
|
+
async function clearReadPendingMail(configDir, profileName, session, emailId) {
|
|
4510
|
+
if (!session) return;
|
|
4511
|
+
const sessions = pendingMailSessions(configDir, profileName).filter((id) => id === sessionId(session));
|
|
4512
|
+
for (const id of sessions) await removePendingMail(configDir, profileName, id, [emailId]);
|
|
4604
4513
|
}
|
|
4605
4514
|
/**
|
|
4606
|
-
*
|
|
4607
|
-
*
|
|
4608
|
-
*
|
|
4609
|
-
*
|
|
4515
|
+
* Record that this profile read one of its own pending emails and the API
|
|
4516
|
+
* answered not_found. At PENDING_NOT_FOUND_LIMIT consecutive misses the
|
|
4517
|
+
* notice is removed. A successful read removes the notice outright, so the
|
|
4518
|
+
* count never spans a success.
|
|
4610
4519
|
*/
|
|
4611
|
-
async function
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4520
|
+
async function recordPendingNotFoundRead(configDir, profileName, session, emailId, limit = 3) {
|
|
4521
|
+
const id = emailId.toLowerCase();
|
|
4522
|
+
return withMailLock(lockDirectory(configDir, profileName), () => {
|
|
4523
|
+
const current = readPendingMail(configDir, profileName, session, true);
|
|
4524
|
+
const index = current.findIndex((row) => row.kind === "mail" && row.email_id === id);
|
|
4525
|
+
const row = current[index];
|
|
4526
|
+
if (!row) return { kind: "absent" };
|
|
4527
|
+
const reads = (row.not_found_reads ?? 0) + 1;
|
|
4528
|
+
if (reads >= limit) {
|
|
4529
|
+
write(configDir, profileName, session, current.filter((_, at) => at !== index));
|
|
4620
4530
|
return {
|
|
4621
|
-
|
|
4622
|
-
|
|
4531
|
+
kind: "dropped",
|
|
4532
|
+
reads
|
|
4623
4533
|
};
|
|
4624
4534
|
}
|
|
4625
|
-
const
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
reason: "the email lookup returned no reply history"
|
|
4535
|
+
const next = [...current];
|
|
4536
|
+
next[index] = {
|
|
4537
|
+
...row,
|
|
4538
|
+
not_found_reads: reads
|
|
4630
4539
|
};
|
|
4540
|
+
write(configDir, profileName, session, next);
|
|
4631
4541
|
return {
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
detail
|
|
4542
|
+
kind: "counted",
|
|
4543
|
+
reads
|
|
4635
4544
|
};
|
|
4636
|
-
}
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
/**
|
|
4548
|
+
* Profiles in this config directory that hold a pending mail notice for
|
|
4549
|
+
* the email in the given session. A session can carry several connected
|
|
4550
|
+
* profiles, each with its own notices.
|
|
4551
|
+
*/
|
|
4552
|
+
function pendingMailProfiles(configDir, session, emailId) {
|
|
4553
|
+
const id = emailId.toLowerCase();
|
|
4554
|
+
let names;
|
|
4555
|
+
try {
|
|
4556
|
+
names = readdirSync(join(agentProfilesDirectory(configDir), "profiles"));
|
|
4557
|
+
} catch {
|
|
4558
|
+
return [];
|
|
4559
|
+
}
|
|
4560
|
+
return names.filter((name) => {
|
|
4561
|
+
try {
|
|
4562
|
+
return readPendingMail(configDir, name, session).some((row) => row.kind === "mail" && row.email_id === id);
|
|
4563
|
+
} catch {
|
|
4564
|
+
return false;
|
|
4565
|
+
}
|
|
4566
|
+
}).sort();
|
|
4567
|
+
}
|
|
4568
|
+
//#endregion
|
|
4569
|
+
//#region src/oclif/wake-context.ts
|
|
4570
|
+
const security = [{
|
|
4571
|
+
scheme: "bearer",
|
|
4572
|
+
type: "http"
|
|
4573
|
+
}];
|
|
4574
|
+
const UUID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
4575
|
+
const WAKE_ADDRESS = /^[a-z0-9._%+-]{1,64}@[a-z0-9.-]{1,253}$/;
|
|
4576
|
+
const WAKE_RECIPIENT = /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]{1,64}@[a-z0-9.-]{1,253}$/;
|
|
4577
|
+
const afterUnsupported = /* @__PURE__ */ new WeakSet();
|
|
4578
|
+
/** The bare lowercase address in `Name <addr>` or `addr` form. */
|
|
4579
|
+
function bareAddress(value) {
|
|
4580
|
+
if (typeof value !== "string") return null;
|
|
4581
|
+
const address = (/<([^<>\s]+@[^<>\s]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
|
|
4582
|
+
return /^[^\s<>@]+@[^\s<>@]+$/.test(address) ? address : null;
|
|
4583
|
+
}
|
|
4584
|
+
function record(value) {
|
|
4585
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4586
|
+
}
|
|
4587
|
+
function newerRows(value) {
|
|
4588
|
+
if (!Array.isArray(value)) return void 0;
|
|
4589
|
+
const rows = [];
|
|
4590
|
+
for (const item of value.slice(-20)) {
|
|
4591
|
+
const row = record(item);
|
|
4592
|
+
const from = bareAddress(row?.from);
|
|
4593
|
+
if (!row || typeof row.id !== "string" || !UUID.test(row.id) || !from || typeof row.received_at !== "string" || !Number.isFinite(Date.parse(row.received_at))) return void 0;
|
|
4594
|
+
rows.push({
|
|
4595
|
+
id: row.id.toLowerCase(),
|
|
4596
|
+
from,
|
|
4597
|
+
received_at: row.received_at
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
return rows;
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* Read one thread, asking for mail newer than `afterEmailId`. The newer-mail
|
|
4604
|
+
* fields are feature-detected: an API that does not return them yields a
|
|
4605
|
+
* context without `newerInboundCount`. Returns null when the thread cannot be
|
|
4606
|
+
* read; callers treat that as "unknown", never as an error.
|
|
4607
|
+
*/
|
|
4608
|
+
async function readThreadContext(client, threadId, afterEmailId, signal) {
|
|
4609
|
+
if (!UUID.test(threadId)) return null;
|
|
4610
|
+
const bounded = AbortSignal.any([signal, AbortSignal.timeout(5e3)]);
|
|
4611
|
+
const read = (after) => client.get({
|
|
4612
|
+
security,
|
|
4613
|
+
url: "/threads/{id}",
|
|
4614
|
+
path: { id: threadId },
|
|
4615
|
+
...after ? { query: { after } } : {},
|
|
4616
|
+
signal: bounded,
|
|
4617
|
+
responseStyle: "fields"
|
|
4618
|
+
});
|
|
4619
|
+
try {
|
|
4620
|
+
const askAfter = afterEmailId !== void 0 && UUID.test(afterEmailId) && !afterUnsupported.has(client);
|
|
4621
|
+
let result = await read(askAfter ? afterEmailId : void 0);
|
|
4622
|
+
if (askAfter && result.response?.status === 400) {
|
|
4623
|
+
afterUnsupported.add(client);
|
|
4624
|
+
result = await read();
|
|
4625
|
+
}
|
|
4626
|
+
if (result.error !== void 0) return null;
|
|
4627
|
+
const body = record(result.data);
|
|
4628
|
+
const data = record(body?.data);
|
|
4629
|
+
if (body?.success !== true || !data || !Array.isArray(data.messages)) return null;
|
|
4630
|
+
const messages = data.messages.filter((message) => Boolean(record(message)) && typeof message.id === "string" && ["inbound", "outbound"].includes(message.direction));
|
|
4631
|
+
const count = data.newer_inbound_count;
|
|
4632
|
+
const newerInboundCount = typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
|
|
4633
|
+
const newerInbound = newerInboundCount === void 0 ? void 0 : newerRows(data.newer_inbound);
|
|
4637
4634
|
return {
|
|
4638
|
-
|
|
4639
|
-
|
|
4635
|
+
threadId: threadId.toLowerCase(),
|
|
4636
|
+
messages,
|
|
4637
|
+
truncated: typeof data.message_count === "number" && data.message_count > messages.length,
|
|
4638
|
+
...newerInboundCount === void 0 ? {} : { newerInboundCount },
|
|
4639
|
+
...newerInbound === void 0 ? {} : { newerInbound }
|
|
4640
4640
|
};
|
|
4641
|
+
} catch {
|
|
4642
|
+
signal.throwIfAborted();
|
|
4643
|
+
return null;
|
|
4641
4644
|
}
|
|
4642
4645
|
}
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
return
|
|
4646
|
+
/** Whether `self` has an outbound message in the thread; undefined if unknowable. */
|
|
4647
|
+
function sentInThread(context, self) {
|
|
4648
|
+
if (!context) return void 0;
|
|
4649
|
+
const address = self.toLowerCase();
|
|
4650
|
+
if (context.messages.some((message) => message.direction === "outbound" && bareAddress(message.from) === address)) return true;
|
|
4651
|
+
return context.truncated ? void 0 : false;
|
|
4649
4652
|
}
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
description,
|
|
4655
|
-
command: commandFromArgv(argv),
|
|
4656
|
-
kind,
|
|
4657
|
-
placeholders: requiresMessage ? [{
|
|
4658
|
-
description: "Replace with the message body before running.",
|
|
4659
|
-
token: "<message>"
|
|
4660
|
-
}] : [],
|
|
4661
|
-
requires_message: requiresMessage
|
|
4662
|
-
};
|
|
4653
|
+
/** The latest outbound message from `self` in the thread, if listed. */
|
|
4654
|
+
function latestOwnOutbound(context, self) {
|
|
4655
|
+
const address = self.toLowerCase();
|
|
4656
|
+
return [...context?.messages ?? []].reverse().find((message) => message.direction === "outbound" && bareAddress(message.from) === address);
|
|
4663
4657
|
}
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
const started = Date.parse(attemptStartedAtIso);
|
|
4667
|
-
if (!Number.isFinite(started)) return attemptStartedAtIso;
|
|
4668
|
-
return new Date(started - SENT_HISTORY_CLOCK_SKEW_MS).toISOString();
|
|
4658
|
+
function hasAttachments(detail) {
|
|
4659
|
+
return (detail.parsed?.attachments?.length ?? 0) > 0;
|
|
4669
4660
|
}
|
|
4670
|
-
/**
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4661
|
+
/**
|
|
4662
|
+
* Map server-provided sender facts to one relationship label. The owner and
|
|
4663
|
+
* member labels come only from server admission; `agent` from the server's
|
|
4664
|
+
* connected-agent verification or network admission; `contact` from an
|
|
4665
|
+
* explicit contact allowance.
|
|
4666
|
+
*/
|
|
4667
|
+
function wakeRelationship(input) {
|
|
4668
|
+
if (input.senderRelation) return input.senderRelation;
|
|
4669
|
+
if (input.connectedAgentVerified || input.network) return "agent";
|
|
4670
|
+
if (input.contact) return "contact";
|
|
4671
|
+
return "other";
|
|
4676
4672
|
}
|
|
4677
|
-
const
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4673
|
+
const SERVER_RELATIONSHIPS = {
|
|
4674
|
+
owner: "owner",
|
|
4675
|
+
org_agent: "agent",
|
|
4676
|
+
member: "member",
|
|
4677
|
+
contact: "contact",
|
|
4678
|
+
other: "other"
|
|
4679
|
+
};
|
|
4680
|
+
/** The `collaboration` object of an email read, when the server sends one. */
|
|
4681
|
+
function collaborationOf(detail) {
|
|
4682
|
+
return record(record(detail)?.collaboration);
|
|
4682
4683
|
}
|
|
4683
4684
|
/**
|
|
4684
|
-
*
|
|
4685
|
-
*
|
|
4686
|
-
*
|
|
4687
|
-
*
|
|
4688
|
-
*
|
|
4689
|
-
*
|
|
4685
|
+
* The sender relationship the server computed for this email
|
|
4686
|
+
* (`collaboration.sender_relationship`), mapped to the CLI's labels:
|
|
4687
|
+
* `org_agent` reads as `agent`. When present it is authoritative, `other`
|
|
4688
|
+
* included, so a stale local classification cannot override it. Returns
|
|
4689
|
+
* undefined only for an older server that sends no such field, or an
|
|
4690
|
+
* unrecognized value; the caller then keeps its own derivation.
|
|
4690
4691
|
*/
|
|
4691
|
-
|
|
4692
|
+
function serverRelationship(detail) {
|
|
4693
|
+
const value = collaborationOf(detail)?.sender_relationship;
|
|
4694
|
+
return typeof value === "string" && Object.hasOwn(SERVER_RELATIONSHIPS, value) ? SERVER_RELATIONSHIPS[value] : void 0;
|
|
4695
|
+
}
|
|
4692
4696
|
/**
|
|
4693
|
-
*
|
|
4694
|
-
*
|
|
4695
|
-
*
|
|
4696
|
-
* before the CLI sent a key, while the CLI knows the key before the
|
|
4697
|
-
* request starts. That lets an uncertain outcome still report the key to
|
|
4698
|
-
* reconcile with, and to retry under with --idempotency-key.
|
|
4697
|
+
* True when the server reports this email's thread as muted for the
|
|
4698
|
+
* receiving address (`collaboration.muted`, or `muted` on the email). An
|
|
4699
|
+
* older server sends neither and nothing is muted by it.
|
|
4699
4700
|
*/
|
|
4700
|
-
function
|
|
4701
|
-
|
|
4702
|
-
return `primitive-${kind}-${createHash("sha256").update(canonicalJson(content)).update(`:t${Math.floor(now / DERIVED_IDEMPOTENCY_WINDOW_MS)}`).digest("hex")}`;
|
|
4701
|
+
function serverMuted(detail) {
|
|
4702
|
+
return collaborationOf(detail)?.muted === true || record(detail)?.muted === true;
|
|
4703
4703
|
}
|
|
4704
4704
|
/**
|
|
4705
|
-
*
|
|
4706
|
-
*
|
|
4707
|
-
*
|
|
4708
|
-
* uncertain outcome can be reconciled with
|
|
4709
|
-
* `primitive sent get --idempotency-key <key>`.
|
|
4705
|
+
* An address fit to print as trusted metadata, or `unavailable`. The
|
|
4706
|
+
* sender's address is parsed from a header the sender wrote, so anything
|
|
4707
|
+
* outside a plain charset (quotes, spaces, line breaks) is withheld.
|
|
4710
4708
|
*/
|
|
4711
|
-
function
|
|
4709
|
+
function displayAddress(value) {
|
|
4710
|
+
return WAKE_ADDRESS.test(value) ? value : "unavailable";
|
|
4711
|
+
}
|
|
4712
|
+
/** The metadata clause of a wake line. Addresses outside a plain charset are withheld. */
|
|
4713
|
+
function formatWakeContext(context) {
|
|
4714
|
+
const sender = displayAddress(context.sender);
|
|
4715
|
+
const thread = context.threadId && UUID.test(context.threadId) ? context.threadId.toLowerCase() : "none";
|
|
4716
|
+
const newer = context.newer !== void 0 && Number.isSafeInteger(context.newer) && context.newer >= 0 ? ` newer=${Math.min(context.newer, 9999)}` : "";
|
|
4717
|
+
const interaction = isWakeInteractionLabel(context.interaction) ? ` interaction=${context.interaction}` : "";
|
|
4718
|
+
return `from=${sender} relationship=${context.relationship} thread=${thread} in_thread=${context.inThread ? "yes" : "no"} attachments=${context.attachments ? "yes" : "no"}${newer}${interaction}`;
|
|
4719
|
+
}
|
|
4720
|
+
/** Build a wake context, reading the thread once. Never throws for API failures. */
|
|
4721
|
+
async function describeWake(input) {
|
|
4722
|
+
const { detail } = input;
|
|
4723
|
+
const threadId = typeof detail.thread_id === "string" && UUID.test(detail.thread_id) ? detail.thread_id.toLowerCase() : null;
|
|
4724
|
+
const thread = threadId ? await readThreadContext(input.client, threadId, detail.id, input.signal) : null;
|
|
4725
|
+
const interaction = wakeInteractionLabel(detail);
|
|
4712
4726
|
return {
|
|
4713
|
-
|
|
4714
|
-
|
|
4727
|
+
sender: detail.from_email.trim().toLowerCase(),
|
|
4728
|
+
relationship: serverRelationship(detail) ?? input.relationship,
|
|
4729
|
+
threadId,
|
|
4730
|
+
inThread: input.localInThread || sentInThread(thread, input.self) === true,
|
|
4731
|
+
attachments: hasAttachments(detail),
|
|
4732
|
+
...thread?.newerInboundCount === void 0 ? {} : { newer: thread.newerInboundCount },
|
|
4733
|
+
...interaction ? { interaction } : {}
|
|
4715
4734
|
};
|
|
4716
4735
|
}
|
|
4736
|
+
const WAKE_PROFILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
|
|
4717
4737
|
/**
|
|
4718
|
-
*
|
|
4719
|
-
*
|
|
4738
|
+
* ` to=<address>` naming the address that received a wake, or empty when the
|
|
4739
|
+
* address is unknown or outside the plain character set the hook accepts.
|
|
4740
|
+
* One session can have several connected profiles, each receiving for its
|
|
4741
|
+
* own address, so the wake says which one this mail arrived at.
|
|
4720
4742
|
*/
|
|
4721
|
-
function
|
|
4722
|
-
const
|
|
4723
|
-
|
|
4724
|
-
"primitive",
|
|
4725
|
-
"sent",
|
|
4726
|
-
"get",
|
|
4727
|
-
"--idempotency-key",
|
|
4728
|
-
params.idempotencyKey
|
|
4729
|
-
]));
|
|
4730
|
-
if (params.sentId !== null) commands.push(buildFollowUpCommand("inspect_sent_email", "Inspect the send and its delivery status", [
|
|
4731
|
-
"primitive",
|
|
4732
|
-
"sent",
|
|
4733
|
-
"get",
|
|
4734
|
-
"--id",
|
|
4735
|
-
params.sentId
|
|
4736
|
-
]));
|
|
4737
|
-
if (params.outcome === "uncertain") commands.push(buildFollowUpCommand("list_recent_sent_emails", "Check sent history for this attempt before retrying", [
|
|
4738
|
-
"primitive",
|
|
4739
|
-
"sent",
|
|
4740
|
-
"list",
|
|
4741
|
-
"--date-from",
|
|
4742
|
-
sentHistoryWindowStart(params.attemptStartedAtIso)
|
|
4743
|
-
]));
|
|
4744
|
-
return commands;
|
|
4743
|
+
function wakeRecipientField(address) {
|
|
4744
|
+
const value = typeof address === "string" ? address.toLowerCase() : "";
|
|
4745
|
+
return WAKE_RECIPIENT.test(value) ? ` to=${value}` : "";
|
|
4745
4746
|
}
|
|
4746
4747
|
/**
|
|
4747
|
-
*
|
|
4748
|
-
*
|
|
4749
|
-
*
|
|
4750
|
-
*
|
|
4751
|
-
* and in the exit code. With `--json` stdout is always an envelope,
|
|
4752
|
-
* whatever the outcome, and the summary is only its outcome_message.
|
|
4748
|
+
* The read command a wake suggests. It selects the receiving profile
|
|
4749
|
+
* explicitly: the email is visible only to that profile, and its pending
|
|
4750
|
+
* notice clears only when that profile reads it, so a copy-pasted command
|
|
4751
|
+
* run under another profile would report not_found.
|
|
4753
4752
|
*/
|
|
4754
|
-
function
|
|
4755
|
-
|
|
4756
|
-
let outcome;
|
|
4757
|
-
let outcomeMessage;
|
|
4758
|
-
let sent = null;
|
|
4759
|
-
let errorPayload = null;
|
|
4760
|
-
if (params.result.error) {
|
|
4761
|
-
errorPayload = extractErrorPayload(params.result.error);
|
|
4762
|
-
params.onApiError(errorPayload);
|
|
4763
|
-
const failure = classifySendError(httpStatus, errorPayload);
|
|
4764
|
-
outcome = failure;
|
|
4765
|
-
outcomeMessage = failure === "already_sent" ? formatDeletedEarlierSendNotice() : formatSendFailureSummary(params.noun, failure, httpStatus);
|
|
4766
|
-
if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
|
|
4767
|
-
} else {
|
|
4768
|
-
sent = params.result.data?.data ?? null;
|
|
4769
|
-
if (sent === null) {
|
|
4770
|
-
outcome = "uncertain";
|
|
4771
|
-
outcomeMessage = `${params.noun} send outcome uncertain: the API accepted the request but returned no send record. Do not resend blindly; check sent history first.`;
|
|
4772
|
-
errorPayload = { message: "The API returned no send record." };
|
|
4773
|
-
} else {
|
|
4774
|
-
outcome = successfulSendOutcome(sent);
|
|
4775
|
-
outcomeMessage = outcome === "not_sent" || outcome === "uncertain" ? formatSendRecordFailureSummary(params.noun, outcome, sent) : outcome === "already_sent" ? formatAlreadySentNotice(sent) : formatSentSummary(params.noun, sent);
|
|
4776
|
-
}
|
|
4777
|
-
if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
|
|
4778
|
-
}
|
|
4779
|
-
if (params.json) {
|
|
4780
|
-
const envelope = {
|
|
4781
|
-
outcome,
|
|
4782
|
-
exit_code: sendOutcomeExitCode(outcome),
|
|
4783
|
-
outcome_message: outcomeMessage,
|
|
4784
|
-
...sendIdentityFields({
|
|
4785
|
-
idempotencyKey: params.idempotencyKey,
|
|
4786
|
-
sent
|
|
4787
|
-
}),
|
|
4788
|
-
sent,
|
|
4789
|
-
http_status: httpStatus ?? null,
|
|
4790
|
-
error: serializeErrorPayload(errorPayload),
|
|
4791
|
-
follow_up_commands: buildSendCommandFollowUps({
|
|
4792
|
-
attemptStartedAtIso: params.attemptStartedAtIso,
|
|
4793
|
-
idempotencyKey: params.idempotencyKey,
|
|
4794
|
-
outcome,
|
|
4795
|
-
sentId: sent?.id ?? null
|
|
4796
|
-
}),
|
|
4797
|
-
...params.extraEnvelopeFields
|
|
4798
|
-
};
|
|
4799
|
-
params.log(JSON.stringify(envelope, null, 2));
|
|
4800
|
-
} else if (!params.result.error) params.log(JSON.stringify(sent, null, 2));
|
|
4801
|
-
return outcome;
|
|
4753
|
+
function wakeReadCommand(emailId, profileName) {
|
|
4754
|
+
return `${profileName && WAKE_PROFILE.test(profileName) ? `PRIMITIVE_AGENT_PROFILE=${profileName} ` : ""}primitive emails get --id ${emailId} --brief`;
|
|
4802
4755
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4756
|
+
//#endregion
|
|
4757
|
+
//#region src/oclif/pending-mail-miss.ts
|
|
4758
|
+
function currentProfile(configDir) {
|
|
4759
|
+
try {
|
|
4760
|
+
return resolveCliAuth({ configDir }).connectedAgent?.profileName ?? null;
|
|
4761
|
+
} catch {
|
|
4762
|
+
return null;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
function profileAddress(configDir, profileName) {
|
|
4766
|
+
try {
|
|
4767
|
+
return wakeRecipientField(loadConnectedAgentProfile(configDir, profileName)?.agent_address).replace(/^ to=/, "");
|
|
4768
|
+
} catch {
|
|
4769
|
+
return "";
|
|
4770
|
+
}
|
|
4813
4771
|
}
|
|
4814
4772
|
/**
|
|
4815
|
-
*
|
|
4816
|
-
*
|
|
4817
|
-
*
|
|
4818
|
-
*
|
|
4819
|
-
*
|
|
4773
|
+
* Explain a not_found read of an email this session was told about. One
|
|
4774
|
+
* session can carry several connected profiles, each receiving its own
|
|
4775
|
+
* address, and an email is visible only to the profile that received it.
|
|
4776
|
+
*
|
|
4777
|
+
* - When the email is a pending notice for another profile in this session,
|
|
4778
|
+
* print one line naming that profile and the command that reads it, so
|
|
4779
|
+
* the miss is not mistaken for a message that does not exist.
|
|
4780
|
+
* - When it is a pending notice for the current profile, count the miss and
|
|
4781
|
+
* drop the notice after consecutive misses, with a line saying why, so an
|
|
4782
|
+
* email that was deleted or is no longer visible stops being announced.
|
|
4783
|
+
*
|
|
4784
|
+
* Writes only to stderr and never throws; the read already failed.
|
|
4820
4785
|
*/
|
|
4821
|
-
function
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
...params.extraEnvelopeFields
|
|
4842
|
-
};
|
|
4843
|
-
}
|
|
4844
|
-
/** Exit code oclif will use for a thrown error (2 for usage errors). */
|
|
4845
|
-
function thrownErrorExitCode(error) {
|
|
4846
|
-
const exit = error?.oclif?.exit;
|
|
4847
|
-
return typeof exit === "number" ? exit : 1;
|
|
4786
|
+
async function explainPendingMailNotFound(configDir, emailId, write = (line) => {
|
|
4787
|
+
process.stderr.write(line);
|
|
4788
|
+
}) {
|
|
4789
|
+
try {
|
|
4790
|
+
const runtime = currentMailSessionKey();
|
|
4791
|
+
if (!runtime) return;
|
|
4792
|
+
const session = runtime.slice(runtime.indexOf(":") + 1);
|
|
4793
|
+
const id = emailId.toLowerCase();
|
|
4794
|
+
const holders = pendingMailProfiles(configDir, session, id);
|
|
4795
|
+
if (holders.length === 0) return;
|
|
4796
|
+
const current = currentProfile(configDir);
|
|
4797
|
+
for (const profile of holders) {
|
|
4798
|
+
if (profile === current) continue;
|
|
4799
|
+
const address = profileAddress(configDir, profile);
|
|
4800
|
+
write(`Email ${id} is a pending notice for profile ${profile}${address ? ` (${address})` : ""}, not the profile this command used. Read it with ${wakeReadCommand(id, profile)}.\n`);
|
|
4801
|
+
}
|
|
4802
|
+
if (!current || !holders.includes(current)) return;
|
|
4803
|
+
const outcome = await recordPendingNotFoundRead(configDir, current, session, id);
|
|
4804
|
+
if (outcome.kind === "dropped") write(`Dropped the pending notice for ${id} from profile ${current} after ${outcome.reads} consecutive not_found reads; the email is no longer visible to this profile.\n`);
|
|
4805
|
+
} catch {}
|
|
4848
4806
|
}
|
|
4849
4807
|
//#endregion
|
|
4850
|
-
//#region src/oclif/
|
|
4851
|
-
const
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
]);
|
|
4857
|
-
/** A printable `<protocol>/<version>`; anything else is treated as unknown. */
|
|
4858
|
-
const KIND = /^[a-z][a-z0-9._-]{0,63}\/[1-9][0-9]{0,3}$/;
|
|
4859
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4860
|
-
function record(value) {
|
|
4861
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4808
|
+
//#region src/oclif/reply-state.ts
|
|
4809
|
+
const AWAITING_VALUES = ["you", "them"];
|
|
4810
|
+
const AWAITING_FLAG_DESCRIPTION = "Only emails whose reply state is this value: `you` = waiting on your reply (the thread's latest message is inbound), `them` = you replied last. Queued and scheduled replies count as replied. Delivered mail only: rejected mail is never returned. Fails if the server does not report reply state yet.";
|
|
4811
|
+
const REPLY_STATE_UNSUPPORTED_CODE = "reply_state_unsupported";
|
|
4812
|
+
/** True when a search DSL query uses the `awaiting:` term. */
|
|
4813
|
+
function queryUsesAwaiting(q) {
|
|
4814
|
+
return typeof q === "string" && /(^|[\s(])-?awaiting:/i.test(q);
|
|
4862
4815
|
}
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
case "repeat.tick/1": return "repeat";
|
|
4869
|
-
case "repeat.stop/1": return "repeat_stopped";
|
|
4870
|
-
default: return "unsupported";
|
|
4816
|
+
var ReplyStateUnsupportedError = class extends Error {
|
|
4817
|
+
code = REPLY_STATE_UNSUPPORTED_CODE;
|
|
4818
|
+
constructor(detail) {
|
|
4819
|
+
super(`The server does not support reply state yet: ${detail} Nothing was treated as awaiting your reply. Upgrade to a server that returns \`awaiting\` and \`reply_count\`, or run without reply-state options.`);
|
|
4820
|
+
this.name = "ReplyStateUnsupportedError";
|
|
4871
4821
|
}
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
case "unsupported": return "unsupported";
|
|
4885
|
-
case "pending": return "read_again";
|
|
4886
|
-
default: return "reply";
|
|
4822
|
+
};
|
|
4823
|
+
/**
|
|
4824
|
+
* The server's reply-state support could not be established (a probe
|
|
4825
|
+
* failed). Reported like ReplyStateUnsupportedError so callers never
|
|
4826
|
+
* read an unverified empty answer as "nothing matched".
|
|
4827
|
+
*/
|
|
4828
|
+
var ReplyStateUnverifiedError = class extends ReplyStateUnsupportedError {
|
|
4829
|
+
constructor(detail) {
|
|
4830
|
+
super(detail);
|
|
4831
|
+
this.message = `Could not verify that the server supports reply state: ${detail} Nothing was treated as matching. Retry the command.`;
|
|
4832
|
+
this.name = "ReplyStateUnverifiedError";
|
|
4833
|
+
this.code = "reply_state_unverified";
|
|
4887
4834
|
}
|
|
4835
|
+
};
|
|
4836
|
+
function hasReplyState(row) {
|
|
4837
|
+
if (row === null || typeof row !== "object") return false;
|
|
4838
|
+
const candidate = row;
|
|
4839
|
+
return (candidate.awaiting === "you" || candidate.awaiting === "them") && typeof candidate.reply_count === "number" && (candidate.last_replied_at === null || typeof candidate.last_replied_at === "string");
|
|
4888
4840
|
}
|
|
4889
4841
|
/**
|
|
4890
|
-
*
|
|
4891
|
-
*
|
|
4892
|
-
*
|
|
4842
|
+
* Throw unless every row carries reply state. An empty page proves
|
|
4843
|
+
* nothing either way; callers that asked for reply state follow an
|
|
4844
|
+
* empty search page with ensureSearchReportsReplyState(). `surface` names the
|
|
4845
|
+
* response for the error, e.g. "GET /emails".
|
|
4893
4846
|
*/
|
|
4894
|
-
function
|
|
4895
|
-
const
|
|
4896
|
-
if (
|
|
4897
|
-
const hint = HINTS.has(row.interaction_hint) ? row.interaction_hint : "none";
|
|
4898
|
-
const rawKind = row.interaction_kind;
|
|
4899
|
-
const kind = (hint === "card" || hint === "status") && typeof rawKind === "string" && KIND.test(rawKind) ? rawKind : null;
|
|
4900
|
-
const fyi = row.fyi === true || record(row.collaboration)?.fyi === true;
|
|
4901
|
-
const category = categoryOf(hint, kind, fyi, row.interaction_candidate === true);
|
|
4902
|
-
const expectation = replyExpectation(category);
|
|
4903
|
-
return {
|
|
4904
|
-
hint,
|
|
4905
|
-
kind,
|
|
4906
|
-
fyi,
|
|
4907
|
-
category,
|
|
4908
|
-
plain_reply_completes: expectation === "reply" || expectation === "repeat",
|
|
4909
|
-
no_reply_needed: expectation === "no_reply"
|
|
4910
|
-
};
|
|
4911
|
-
}
|
|
4912
|
-
function replyAction(bin, id) {
|
|
4913
|
-
return buildFollowUpCommand("reply", "Reply to the sender", [
|
|
4914
|
-
bin,
|
|
4915
|
-
"reply",
|
|
4916
|
-
"--id",
|
|
4917
|
-
id,
|
|
4918
|
-
"--body",
|
|
4919
|
-
"<message>"
|
|
4920
|
-
], { requiresMessage: true });
|
|
4847
|
+
function assertReplyState(rows, surface) {
|
|
4848
|
+
const missing = rows.filter((row) => !hasReplyState(row)).length;
|
|
4849
|
+
if (missing > 0) throw new ReplyStateUnsupportedError(`${surface} returned ${missing} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the \`awaiting\` and \`reply_count\` fields.`);
|
|
4921
4850
|
}
|
|
4851
|
+
const AWAITING_REJECTED_UNSUPPORTED_CODE = "awaiting_rejected_unsupported";
|
|
4922
4852
|
/**
|
|
4923
|
-
* The
|
|
4924
|
-
*
|
|
4925
|
-
*
|
|
4853
|
+
* The `awaiting` filter matches delivered mail only: a rejected email
|
|
4854
|
+
* (for example refused over the storage limit) was never delivered, so it
|
|
4855
|
+
* awaits no one. A server that returns one through the filter predates
|
|
4856
|
+
* that, and an agent would be handed mail nobody can answer.
|
|
4926
4857
|
*/
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
case "payment": return [buildFollowUpCommand("inspect_payment", "Show the payment request's terms before paying", [
|
|
4934
|
-
bin,
|
|
4935
|
-
"payments",
|
|
4936
|
-
"challenge-from-email",
|
|
4937
|
-
"--id",
|
|
4938
|
-
id
|
|
4939
|
-
]), buildFollowUpCommand("pay", "Pay the payment request (signs with your wallet key and sends the payment)", [
|
|
4940
|
-
bin,
|
|
4941
|
-
"payments",
|
|
4942
|
-
"pay-email",
|
|
4943
|
-
"--in-reply-to",
|
|
4944
|
-
id
|
|
4945
|
-
])];
|
|
4946
|
-
case "contact": return [buildFollowUpCommand("accept_contact", "Accept the contact request, if the owner's policy allows it", [
|
|
4947
|
-
bin,
|
|
4948
|
-
"contacts",
|
|
4949
|
-
"accept",
|
|
4950
|
-
"--id",
|
|
4951
|
-
id
|
|
4952
|
-
])];
|
|
4953
|
-
case "repeat": {
|
|
4954
|
-
const actions = [replyAction(bin, id)];
|
|
4955
|
-
if (options.repeatStoppable !== false) actions.push(buildFollowUpCommand("stop_repeat", "Stop the repeating message once its goal is met", [
|
|
4956
|
-
bin,
|
|
4957
|
-
"repeat",
|
|
4958
|
-
"stop",
|
|
4959
|
-
"--id",
|
|
4960
|
-
id
|
|
4961
|
-
]));
|
|
4962
|
-
return actions;
|
|
4963
|
-
}
|
|
4964
|
-
case "pending": return [buildFollowUpCommand("read_again", "Read the email again once the server has finished checking it", [
|
|
4965
|
-
bin,
|
|
4966
|
-
"emails",
|
|
4967
|
-
"get",
|
|
4968
|
-
"--id",
|
|
4969
|
-
id,
|
|
4970
|
-
"--brief"
|
|
4971
|
-
])];
|
|
4972
|
-
default: return [];
|
|
4858
|
+
var AwaitingIncludesRejectedError = class extends ReplyStateUnsupportedError {
|
|
4859
|
+
constructor(detail) {
|
|
4860
|
+
super(detail);
|
|
4861
|
+
this.message = `The server's \`awaiting\` filter does not exclude undelivered mail yet: ${detail} Upgrade to a server whose \`awaiting\` filter matches delivered mail only.`;
|
|
4862
|
+
this.name = "AwaitingIncludesRejectedError";
|
|
4863
|
+
this.code = AWAITING_REJECTED_UNSUPPORTED_CODE;
|
|
4973
4864
|
}
|
|
4865
|
+
};
|
|
4866
|
+
/**
|
|
4867
|
+
* For rows returned BY an `awaiting` filter (not the unfiltered probe):
|
|
4868
|
+
* every row carries reply state, and none has status `rejected`.
|
|
4869
|
+
*/
|
|
4870
|
+
function assertAwaitingFilterRows(rows, surface) {
|
|
4871
|
+
assertReplyState(rows, surface);
|
|
4872
|
+
const rejected = rows.filter((row) => row !== null && typeof row === "object" && row.status === "rejected").length;
|
|
4873
|
+
if (rejected > 0) throw new AwaitingIncludesRejectedError(`${surface} returned ${rejected} email${rejected === 1 ? "" : "s"} with status \`rejected\`.`);
|
|
4874
|
+
}
|
|
4875
|
+
function errorBody(payload) {
|
|
4876
|
+
if (payload === null || typeof payload !== "object") return {};
|
|
4877
|
+
const inner = payload.error;
|
|
4878
|
+
return inner !== null && typeof inner === "object" ? inner : payload;
|
|
4974
4879
|
}
|
|
4975
4880
|
/**
|
|
4976
|
-
*
|
|
4977
|
-
*
|
|
4978
|
-
* server
|
|
4881
|
+
* True when an API error is an older server rejecting the `awaiting`
|
|
4882
|
+
* query parameter or the `awaiting:` search term as unknown. A current
|
|
4883
|
+
* server accepts both, and the CLI only ever sends valid values.
|
|
4979
4884
|
*/
|
|
4980
|
-
function
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
const
|
|
4984
|
-
|
|
4985
|
-
case "fyi": return "Informational (fyi): no reply needed, not even another fyi.";
|
|
4986
|
-
case "signal": return `Status signal (${kind}): no reply needed.`;
|
|
4987
|
-
case "repeat_stopped": return `Notice that a repeating message stopped (${kind}): no reply needed.`;
|
|
4988
|
-
case "payment": {
|
|
4989
|
-
const inspect = command("inspect_payment");
|
|
4990
|
-
const pay = command("pay");
|
|
4991
|
-
return inspect && pay ? `Payment interaction (${kind}). If it requests payment, review it with ${inspect} and pay with ${pay}. A plain reply does not pay or decline it.` : null;
|
|
4992
|
-
}
|
|
4993
|
-
case "contact": {
|
|
4994
|
-
const accept = command("accept_contact");
|
|
4995
|
-
return accept ? `Contact interaction (${kind}). If it is a contact request, accept it under the owner's policy with ${accept}. A plain reply does not accept it.` : null;
|
|
4996
|
-
}
|
|
4997
|
-
case "repeat": {
|
|
4998
|
-
const stop = command("stop_repeat");
|
|
4999
|
-
return stop ? `Repeating message (${kind}). Reply if it asks for an answer; stop it once its goal is met with ${stop}.` : `Repeating message (${kind}). Reply if it asks for an answer; only the sender can stop it.`;
|
|
5000
|
-
}
|
|
5001
|
-
case "unsupported": return `Interaction (${kind}) that this CLI cannot answer. A plain reply does not complete it.`;
|
|
5002
|
-
case "pending": {
|
|
5003
|
-
const again = command("read_again");
|
|
5004
|
-
return again ? `Primitive has not finished checking this email for an interaction. Read it again before answering: ${again}` : null;
|
|
5005
|
-
}
|
|
5006
|
-
default: return null;
|
|
5007
|
-
}
|
|
4885
|
+
function isAwaitingRejectedError(payload) {
|
|
4886
|
+
const body = errorBody(payload);
|
|
4887
|
+
if (body.code !== "validation_error") return false;
|
|
4888
|
+
const message = typeof body.message === "string" ? body.message : JSON.stringify(payload);
|
|
4889
|
+
return /awaiting/i.test(message) && /unrecogni[sz]ed|unknown/i.test(message);
|
|
5008
4890
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
const interaction = readEmailInteraction(detail);
|
|
5012
|
-
if (!interaction) return null;
|
|
5013
|
-
if (interaction.hint === "card") return interaction.kind ?? "unknown";
|
|
5014
|
-
if (interaction.fyi) return "fyi";
|
|
5015
|
-
return null;
|
|
4891
|
+
function awaitingRejectedError(surface) {
|
|
4892
|
+
return new ReplyStateUnsupportedError(`${surface} rejected the \`awaiting\` filter as unknown.`);
|
|
5016
4893
|
}
|
|
5017
|
-
/**
|
|
5018
|
-
function
|
|
5019
|
-
if (!
|
|
5020
|
-
|
|
5021
|
-
|
|
4894
|
+
/** Short column value for tables: "you", "them", or "-" when absent. */
|
|
4895
|
+
function formatAwaitingCell(row) {
|
|
4896
|
+
if (!hasReplyState(row)) return "-";
|
|
4897
|
+
return row.awaiting;
|
|
4898
|
+
}
|
|
4899
|
+
/** Reply count for tables, or "-" when the server did not report it. */
|
|
4900
|
+
function formatRepliesCell(row) {
|
|
4901
|
+
if (!hasReplyState(row)) return "-";
|
|
4902
|
+
return String(row.reply_count);
|
|
5022
4903
|
}
|
|
4904
|
+
const REPLY_STATE_SURFACES = {
|
|
4905
|
+
listEmails: "GET /emails",
|
|
4906
|
+
searchEmails: "GET /emails/search"
|
|
4907
|
+
};
|
|
5023
4908
|
/**
|
|
5024
|
-
*
|
|
5025
|
-
*
|
|
4909
|
+
* For the generated `emails list` / `emails search` commands: the
|
|
4910
|
+
* surface name when this call asked for reply state (an `awaiting`
|
|
4911
|
+
* parameter or an `awaiting:` search term), else null.
|
|
5026
4912
|
*/
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
};
|
|
4913
|
+
function replyStateSurfaceForOperation(sdkName, query) {
|
|
4914
|
+
const surface = REPLY_STATE_SURFACES[sdkName];
|
|
4915
|
+
if (!surface || !query) return null;
|
|
4916
|
+
const q = typeof query.q === "string" ? query.q : void 0;
|
|
4917
|
+
return query.awaiting !== void 0 || queryUsesAwaiting(q) ? surface : null;
|
|
4918
|
+
}
|
|
4919
|
+
const searchSupportProbes = /* @__PURE__ */ new WeakMap();
|
|
5035
4920
|
/**
|
|
5036
|
-
*
|
|
5037
|
-
*
|
|
4921
|
+
* GET /emails/search ignores unknown parameters, so an empty page for
|
|
4922
|
+
* `awaiting=...` does not show that the server applied the filter.
|
|
4923
|
+
* Read one unfiltered row: if it lacks reply state the server does not
|
|
4924
|
+
* support it and this throws ReplyStateUnsupportedError. No rows at all
|
|
4925
|
+
* means the mailbox is empty, so the empty answer stands.
|
|
5038
4926
|
*/
|
|
5039
|
-
function
|
|
5040
|
-
const
|
|
5041
|
-
|
|
4927
|
+
function ensureSearchReportsReplyState(apiClient) {
|
|
4928
|
+
const key = apiClient;
|
|
4929
|
+
let probe = searchSupportProbes.get(key);
|
|
4930
|
+
if (!probe) {
|
|
4931
|
+
probe = (async () => {
|
|
4932
|
+
const result = await searchEmails({
|
|
4933
|
+
client: apiClient.client,
|
|
4934
|
+
query: {
|
|
4935
|
+
limit: 1,
|
|
4936
|
+
include_facets: "false",
|
|
4937
|
+
snippet: "false"
|
|
4938
|
+
},
|
|
4939
|
+
responseStyle: "fields"
|
|
4940
|
+
});
|
|
4941
|
+
if (result.error) throw new ReplyStateUnverifiedError("the empty result could not be checked: reading one unfiltered row from GET /emails/search failed.");
|
|
4942
|
+
const rows = result.data?.data;
|
|
4943
|
+
assertReplyState(rows ?? [], "GET /emails/search");
|
|
4944
|
+
})();
|
|
4945
|
+
searchSupportProbes.set(key, probe);
|
|
4946
|
+
probe.catch(() => {
|
|
4947
|
+
if (searchSupportProbes.get(key) === probe) searchSupportProbes.delete(key);
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
return probe;
|
|
5042
4951
|
}
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
4952
|
+
//#endregion
|
|
4953
|
+
//#region src/oclif/api-command.ts
|
|
4954
|
+
const API_ERROR_CODES = {
|
|
4955
|
+
accessDenied: "access_denied",
|
|
4956
|
+
authorizationPending: "authorization_pending",
|
|
4957
|
+
expiredToken: "expired_token",
|
|
4958
|
+
invalidDeviceCode: "invalid_device_code",
|
|
4959
|
+
notFound: "not_found",
|
|
4960
|
+
slowDown: "slow_down",
|
|
4961
|
+
unauthorized: "unauthorized"
|
|
4962
|
+
};
|
|
4963
|
+
function flagName(parameterName) {
|
|
4964
|
+
return parameterName.replace(/_/g, "-");
|
|
4965
|
+
}
|
|
4966
|
+
function flagDescription(parameter) {
|
|
4967
|
+
return parameter.description ?? parameter.name;
|
|
4968
|
+
}
|
|
4969
|
+
const numberFlag = Flags.custom({ async parse(input, _context, options) {
|
|
4970
|
+
const trimmed = input.trim();
|
|
4971
|
+
if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
|
|
4972
|
+
const value = Number(trimmed);
|
|
4973
|
+
if (!Number.isFinite(value)) throw new Errors.CLIError(`Expected a number but received: ${input}`);
|
|
4974
|
+
if (options.min !== void 0 && value < options.min) throw new Errors.CLIError(`Expected a number greater than or equal to ${options.min} but received: ${input}`);
|
|
4975
|
+
if (options.max !== void 0 && value > options.max) throw new Errors.CLIError(`Expected a number less than or equal to ${options.max} but received: ${input}`);
|
|
4976
|
+
return value;
|
|
4977
|
+
} });
|
|
4978
|
+
function numericFlagOptions(parameter) {
|
|
4979
|
+
return {
|
|
4980
|
+
...typeof parameter.default === "number" ? { default: parameter.default } : {},
|
|
4981
|
+
...typeof parameter.maximum === "number" ? { max: parameter.maximum } : {},
|
|
4982
|
+
...typeof parameter.minimum === "number" ? { min: parameter.minimum } : {}
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
4985
|
+
function extractBodyFields(schema) {
|
|
4986
|
+
if (!schema || typeof schema !== "object") return [];
|
|
4987
|
+
const properties = schema.properties;
|
|
4988
|
+
if (!properties || typeof properties !== "object") return [];
|
|
4989
|
+
const requiredArr = Array.isArray(schema.required) ? schema.required.filter((k) => typeof k === "string") : [];
|
|
4990
|
+
const required = new Set(requiredArr);
|
|
4991
|
+
const fields = [];
|
|
4992
|
+
for (const [name, raw] of Object.entries(properties)) {
|
|
4993
|
+
const propSchema = raw && typeof raw === "object" ? raw : {};
|
|
4994
|
+
const t = propSchema.type;
|
|
4995
|
+
let displayType = "any";
|
|
4996
|
+
let kind = "complex";
|
|
4997
|
+
if (typeof t === "string") {
|
|
4998
|
+
displayType = t;
|
|
4999
|
+
if (t === "string") kind = "string";
|
|
5000
|
+
else if (t === "integer") kind = "integer";
|
|
5001
|
+
else if (t === "number") kind = "number";
|
|
5002
|
+
else if (t === "boolean") kind = "boolean";
|
|
5003
|
+
else if (t === "array") {
|
|
5004
|
+
const items = propSchema.items;
|
|
5005
|
+
if (items && typeof items === "object") {
|
|
5006
|
+
const itemType = items.type;
|
|
5007
|
+
if (typeof itemType === "string") displayType = `array<${itemType}>`;
|
|
5008
|
+
}
|
|
5009
|
+
kind = "complex";
|
|
5010
|
+
} else kind = "complex";
|
|
5011
|
+
} else if (Array.isArray(t)) {
|
|
5012
|
+
const nonNull = t.filter((s) => s !== "null");
|
|
5013
|
+
if (nonNull.length === 1) {
|
|
5014
|
+
const single = nonNull[0];
|
|
5015
|
+
displayType = `${single}?`;
|
|
5016
|
+
if (single === "string") kind = "string";
|
|
5017
|
+
else if (single === "integer") kind = "integer";
|
|
5018
|
+
else if (single === "number") kind = "number";
|
|
5019
|
+
else if (single === "boolean") kind = "boolean";
|
|
5020
|
+
else kind = "complex";
|
|
5021
|
+
} else {
|
|
5022
|
+
displayType = nonNull.join("|");
|
|
5023
|
+
kind = "complex";
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
const description = typeof propSchema.description === "string" ? propSchema.description.split(/\n\s*\n/)[0].replace(/\s*\n\s*/g, " ").trim() : "";
|
|
5027
|
+
const enumRaw = propSchema.enum;
|
|
5028
|
+
const enumValues = kind === "string" && Array.isArray(enumRaw) ? enumRaw.filter((e) => typeof e === "string") : void 0;
|
|
5029
|
+
fields.push({
|
|
5030
|
+
name,
|
|
5031
|
+
description,
|
|
5032
|
+
required: required.has(name),
|
|
5033
|
+
displayType,
|
|
5034
|
+
kind,
|
|
5035
|
+
...enumValues && enumValues.length > 0 ? { enumValues } : {},
|
|
5036
|
+
...typeof propSchema.maximum === "number" ? { maximum: propSchema.maximum } : {},
|
|
5037
|
+
...typeof propSchema.minimum === "number" ? { minimum: propSchema.minimum } : {}
|
|
5038
|
+
});
|
|
5039
|
+
}
|
|
5040
|
+
return fields.sort((a, b) => {
|
|
5041
|
+
if (a.required !== b.required) return a.required ? -1 : 1;
|
|
5042
|
+
return a.name.localeCompare(b.name);
|
|
5043
|
+
});
|
|
5046
5044
|
}
|
|
5047
5045
|
/**
|
|
5048
|
-
*
|
|
5049
|
-
*
|
|
5046
|
+
* Render a "Body fields" summary for the per-command help.
|
|
5047
|
+
*
|
|
5048
|
+
* Most scalar fields are exposed as individual `--flag` flags,
|
|
5049
|
+
* which oclif auto-renders in the FLAGS section above. To avoid
|
|
5050
|
+
* duplicating that, the summary here only documents fields that
|
|
5051
|
+
* MUST go through `--raw-body` (complex types: arrays, objects,
|
|
5052
|
+
* mixed-non-nullable). When an operation has only scalars, the
|
|
5053
|
+
* summary is omitted entirely and oclif's FLAGS section is the
|
|
5054
|
+
* full story.
|
|
5055
|
+
*
|
|
5056
|
+
* For operations with mixed scalar and complex fields, we also
|
|
5057
|
+
* include a short header pointing the agent at the flag form so
|
|
5058
|
+
* the natural reading is "use the flags above; --raw-body for
|
|
5059
|
+
* the leftovers below."
|
|
5050
5060
|
*/
|
|
5051
|
-
function
|
|
5052
|
-
const
|
|
5053
|
-
if (
|
|
5054
|
-
const
|
|
5055
|
-
|
|
5056
|
-
const
|
|
5057
|
-
const
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
+
function renderRequestSchemaSummary(schema) {
|
|
5062
|
+
const fields = extractBodyFields(schema);
|
|
5063
|
+
if (fields.length === 0) return null;
|
|
5064
|
+
const complex = fields.filter((f) => f.kind === "complex");
|
|
5065
|
+
if (complex.length === 0) return null;
|
|
5066
|
+
const nameWidth = Math.min(24, Math.max(...complex.map((f) => f.name.length)));
|
|
5067
|
+
const descMax = 78;
|
|
5068
|
+
const lines = ["Body fields requiring --raw-body JSON (these are not exposed as flags):"];
|
|
5069
|
+
for (const f of complex) {
|
|
5070
|
+
const marker = f.required ? " *" : " ";
|
|
5071
|
+
const padName = f.name.padEnd(nameWidth);
|
|
5072
|
+
const trimmedDesc = f.description.length > descMax ? `${f.description.slice(0, descMax - 3)}...` : f.description;
|
|
5073
|
+
const desc = trimmedDesc ? ` ${trimmedDesc}` : "";
|
|
5074
|
+
lines.push(`${marker} ${padName} ${f.displayType}${desc}`);
|
|
5075
|
+
}
|
|
5076
|
+
lines.push("(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)");
|
|
5077
|
+
return lines.join("\n");
|
|
5078
|
+
}
|
|
5079
|
+
function flagForParameter(parameter) {
|
|
5080
|
+
const common = {
|
|
5081
|
+
description: flagDescription(parameter),
|
|
5082
|
+
required: parameter.required
|
|
5061
5083
|
};
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5084
|
+
if (parameter.type === "boolean") return Flags.boolean(common);
|
|
5085
|
+
if (parameter.type === "integer") return Flags.integer({
|
|
5086
|
+
...common,
|
|
5087
|
+
...numericFlagOptions(parameter)
|
|
5088
|
+
});
|
|
5089
|
+
if (parameter.type === "number") return numberFlag({
|
|
5090
|
+
...common,
|
|
5091
|
+
...numericFlagOptions(parameter)
|
|
5092
|
+
});
|
|
5093
|
+
if (parameter.enum && parameter.enum.length > 0) return Flags.string({
|
|
5094
|
+
...common,
|
|
5095
|
+
options: parameter.enum
|
|
5096
|
+
});
|
|
5097
|
+
return Flags.string(common);
|
|
5098
|
+
}
|
|
5099
|
+
function coerceParameterValue(parameter, value) {
|
|
5100
|
+
if (value === void 0) return;
|
|
5101
|
+
if (parameter.type === "number") {
|
|
5102
|
+
if (typeof value === "number") return value;
|
|
5103
|
+
const parsed = Number(value);
|
|
5104
|
+
if (Number.isNaN(parsed)) throw new Errors.CLIError(`Invalid number for --${parameter.name}: ${value}`);
|
|
5105
|
+
return parsed;
|
|
5106
|
+
}
|
|
5107
|
+
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
|
|
5108
|
+
throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
|
|
5109
|
+
}
|
|
5110
|
+
function cliError(message) {
|
|
5111
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
5112
|
+
}
|
|
5113
|
+
function parseJson(source, flagLabel) {
|
|
5114
|
+
try {
|
|
5115
|
+
return JSON.parse(source);
|
|
5116
|
+
} catch (error) {
|
|
5117
|
+
throw cliError(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
function readJsonBody(flags) {
|
|
5121
|
+
const bodyFile = flags["body-file"];
|
|
5122
|
+
const rawBody = flags["raw-body"];
|
|
5123
|
+
if (bodyFile && rawBody) throw cliError("Use either --raw-body or --body-file, not both");
|
|
5124
|
+
if (typeof bodyFile === "string") {
|
|
5125
|
+
let contents;
|
|
5126
|
+
try {
|
|
5127
|
+
contents = readFileSync(bodyFile, "utf8");
|
|
5128
|
+
} catch (error) {
|
|
5129
|
+
throw cliError(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5130
|
+
}
|
|
5131
|
+
return parseJson(contents, `--body-file ${bodyFile}`);
|
|
5132
|
+
}
|
|
5133
|
+
if (typeof rawBody === "string") return parseJson(rawBody, "--raw-body");
|
|
5134
|
+
}
|
|
5135
|
+
function readTextFileFlag(path, flagLabel) {
|
|
5136
|
+
try {
|
|
5137
|
+
return readFileSync(path, "utf8");
|
|
5138
|
+
} catch (error) {
|
|
5139
|
+
throw cliError(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
function extractErrorPayload(raw) {
|
|
5143
|
+
if (raw && typeof raw === "object" && !(raw instanceof Error) && "error" in raw) {
|
|
5144
|
+
const inner = raw.error;
|
|
5145
|
+
if (inner !== null && inner !== void 0) return inner;
|
|
5146
|
+
}
|
|
5147
|
+
return raw;
|
|
5148
|
+
}
|
|
5149
|
+
function extractCauseDetails(cause) {
|
|
5150
|
+
const details = {};
|
|
5151
|
+
let code;
|
|
5152
|
+
if (!cause || typeof cause !== "object") return { details };
|
|
5153
|
+
for (const [key, value] of Object.entries(cause)) if (typeof value === "string" || typeof value === "number") {
|
|
5154
|
+
details[key] = value;
|
|
5155
|
+
if (key === "code" && typeof value === "string") code = value;
|
|
5156
|
+
}
|
|
5157
|
+
return {
|
|
5158
|
+
code,
|
|
5159
|
+
details
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
5162
|
+
function formatErrorPayload(payload) {
|
|
5163
|
+
if (payload instanceof Error) {
|
|
5164
|
+
const { code, details } = extractCauseDetails(payload.cause);
|
|
5165
|
+
const body = {
|
|
5166
|
+
code: code ?? "client_error",
|
|
5167
|
+
message: payload.message || payload.name || String(payload)
|
|
5102
5168
|
};
|
|
5103
|
-
|
|
5169
|
+
if (Object.keys(details).length > 0) body.cause = details;
|
|
5170
|
+
return JSON.stringify(body, null, 2);
|
|
5104
5171
|
}
|
|
5172
|
+
return JSON.stringify(payload, null, 2);
|
|
5173
|
+
}
|
|
5174
|
+
function extractErrorCode(payload) {
|
|
5175
|
+
if (payload instanceof Error) {
|
|
5176
|
+
const { code } = extractCauseDetails(payload.cause);
|
|
5177
|
+
return code;
|
|
5178
|
+
}
|
|
5179
|
+
if (payload && typeof payload === "object") {
|
|
5180
|
+
const inner = payload.error;
|
|
5181
|
+
if (inner && typeof inner === "object" && typeof inner.code === "string") return inner.code;
|
|
5182
|
+
const direct = payload.code;
|
|
5183
|
+
if (typeof direct === "string") return direct;
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
const ERROR_CODE_HINTS = { [API_ERROR_CODES.unauthorized]: "Hint: run `primitive signin`, pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify auth is live." };
|
|
5187
|
+
const NETWORK_ERROR_HINTS = {
|
|
5188
|
+
ENETUNREACH: "Hint: the network is unreachable. If you're behind a proxy and set HTTP(S)_PROXY, re-run with NODE_USE_ENV_PROXY=1 (Node 22+ ignores those env vars by default). `primitive doctor` reports the local environment in one shot.",
|
|
5189
|
+
ECONNREFUSED: "Hint: the server refused the connection. Check that your firewall allows egress to *.primitive.dev, that your PRIMITIVE_API_BASE_URL_* overrides (if any) point at a reachable host, and re-run with NODE_USE_ENV_PROXY=1 if you're behind a proxy. `primitive doctor` reports the local environment in one shot.",
|
|
5190
|
+
ETIMEDOUT: "Hint: the connection timed out. Check egress rules and proxy configuration; if you're behind a proxy, re-run with NODE_USE_ENV_PROXY=1 and HTTPS_PROXY set. `primitive doctor` reports the local environment in one shot.",
|
|
5191
|
+
EAI_AGAIN: "Hint: DNS lookup failed. Check /etc/resolv.conf inside containers, and try `curl -v https://api.primitive.dev/v1/account` to confirm the host resolves. `primitive doctor` reports the local environment in one shot."
|
|
5192
|
+
};
|
|
5193
|
+
function writeErrorWithHints(payload) {
|
|
5194
|
+
process.stderr.write(`${formatErrorPayload(payload)}\n`);
|
|
5195
|
+
const code = extractErrorCode(payload);
|
|
5196
|
+
if (!code) return;
|
|
5197
|
+
if (code in ERROR_CODE_HINTS) {
|
|
5198
|
+
const hint = ERROR_CODE_HINTS[code];
|
|
5199
|
+
process.stderr.write(`${hint}\n`);
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
5202
|
+
if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
|
|
5203
|
+
}
|
|
5204
|
+
/**
|
|
5205
|
+
* Surface a user-facing hint when a request comes back unauthorized.
|
|
5206
|
+
*
|
|
5207
|
+
* Deliberately does NOT mutate the saved credentials.json. Auto-
|
|
5208
|
+
* deleting on any 401 made transient rejections look like permanent
|
|
5209
|
+
* credential failures and forced unnecessary re-login cycles; we
|
|
5210
|
+
* surface a hint and let the user decide whether to re-authenticate.
|
|
5211
|
+
*
|
|
5212
|
+
* The one legitimate auto-delete case lives in `primitive login`'s
|
|
5213
|
+
* `checkExistingLogin`: the user has explicitly asked to log in,
|
|
5214
|
+
* existing credentials are probed, and if they fail we clean up
|
|
5215
|
+
* before minting a new key. That path calls `deleteCliCredentials`
|
|
5216
|
+
* directly rather than going through this function.
|
|
5217
|
+
*/
|
|
5218
|
+
function surfaceUnauthorizedHint(params) {
|
|
5219
|
+
if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return;
|
|
5220
|
+
if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl !== params.auth.credentials.api_base_url) {
|
|
5221
|
+
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, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
process.stderr.write("Your saved Primitive CLI OAuth session 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 signin` to mint a fresh session.\n");
|
|
5225
|
+
}
|
|
5226
|
+
function formatElapsed(ms) {
|
|
5227
|
+
const seconds = ms / 1e3;
|
|
5228
|
+
if (seconds < 60) return `${seconds.toFixed(2)}s`;
|
|
5229
|
+
const minutes = Math.floor(seconds / 60);
|
|
5230
|
+
return `${minutes}m ${(seconds - minutes * 60).toFixed(2)}s`;
|
|
5231
|
+
}
|
|
5232
|
+
async function runWithTiming(enabled, fn) {
|
|
5233
|
+
if (!enabled) return fn();
|
|
5234
|
+
const start = Date.now();
|
|
5235
|
+
try {
|
|
5236
|
+
return await fn();
|
|
5237
|
+
} finally {
|
|
5238
|
+
process.stderr.write(`[time: ${formatElapsed(Date.now() - start)}]\n`);
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
|
|
5242
|
+
const API_BASE_URL_FLAG_DESCRIPTION = "Override the API base URL. Internal testing only; not documented to customers.";
|
|
5243
|
+
const RESERVED_FLAG_NAMES = new Set([
|
|
5244
|
+
"api-key",
|
|
5245
|
+
"api-base-url",
|
|
5246
|
+
"raw-body",
|
|
5247
|
+
"body-file",
|
|
5248
|
+
"envelope",
|
|
5249
|
+
"output"
|
|
5250
|
+
]);
|
|
5251
|
+
function bodyFieldFlag(field, aliases) {
|
|
5252
|
+
const common = {
|
|
5253
|
+
description: field.description || field.name,
|
|
5254
|
+
...aliases && aliases.length > 0 ? { aliases } : {}
|
|
5255
|
+
};
|
|
5256
|
+
if (field.kind === "boolean") return Flags.boolean({
|
|
5257
|
+
...common,
|
|
5258
|
+
allowNo: true
|
|
5259
|
+
});
|
|
5260
|
+
if (field.kind === "integer") return Flags.integer({
|
|
5261
|
+
...common,
|
|
5262
|
+
...numericFlagOptions(field)
|
|
5263
|
+
});
|
|
5264
|
+
if (field.kind === "number") return numberFlag({
|
|
5265
|
+
...common,
|
|
5266
|
+
...numericFlagOptions(field)
|
|
5267
|
+
});
|
|
5268
|
+
if (field.enumValues) return Flags.string({
|
|
5269
|
+
...common,
|
|
5270
|
+
options: field.enumValues
|
|
5271
|
+
});
|
|
5272
|
+
return Flags.string(common);
|
|
5273
|
+
}
|
|
5274
|
+
function buildFlags(operation) {
|
|
5275
|
+
const flags = {
|
|
5276
|
+
"api-key": Flags.string({
|
|
5277
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
5278
|
+
env: "PRIMITIVE_API_KEY"
|
|
5279
|
+
}),
|
|
5280
|
+
"api-base-url": Flags.string({
|
|
5281
|
+
description: "Override the API base URL. Internal testing only; not documented to customers.",
|
|
5282
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
5283
|
+
hidden: true
|
|
5284
|
+
}),
|
|
5285
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
5286
|
+
};
|
|
5287
|
+
if (!operation.binaryResponse) {
|
|
5288
|
+
flags.json = Flags.boolean({ description: "Print exactly one JSON document on stdout, on success and on failure, and nothing on stderr. Stdout keeps its usual shape: the data payload, or with --envelope the full response envelope (data plus meta, including meta.cursor, and empty-result hints in summary). The next-cursor line is not printed; use --envelope to page. A failure prints the error with exit_code. Without --json, the cursor and notices go to stderr." });
|
|
5289
|
+
flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
|
|
5290
|
+
}
|
|
5291
|
+
for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
|
|
5292
|
+
const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
|
|
5293
|
+
if (operation.hasJsonBody) {
|
|
5294
|
+
flags["raw-body"] = Flags.string({ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available." });
|
|
5295
|
+
flags["body-file"] = Flags.string({ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload." });
|
|
5296
|
+
const bodyFields = extractBodyFields(operation.requestSchema);
|
|
5297
|
+
const aliasesForOperation = OPERATION_FLAG_ALIASES[operation.sdkName];
|
|
5298
|
+
for (const field of bodyFields) {
|
|
5299
|
+
if (field.kind === "complex") continue;
|
|
5300
|
+
const name = flagName(field.name);
|
|
5301
|
+
if (RESERVED_FLAG_NAMES.has(name)) continue;
|
|
5302
|
+
if (flags[name] !== void 0) continue;
|
|
5303
|
+
flags[name] = bodyFieldFlag(field, aliasesForOperation?.[field.name]);
|
|
5304
|
+
bodyFieldFlagToProperty.set(name, field.name);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
if (operation.binaryResponse) flags.output = Flags.string({ description: "Write binary response bytes to a file" });
|
|
5308
|
+
if (ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName)) flags.wait = Flags.boolean({
|
|
5309
|
+
allowNo: true,
|
|
5310
|
+
default: true,
|
|
5311
|
+
description: ATTACHMENT_WAIT_FLAG_DESCRIPTION
|
|
5312
|
+
});
|
|
5313
|
+
return {
|
|
5314
|
+
flags,
|
|
5315
|
+
bodyFieldFlagToProperty
|
|
5316
|
+
};
|
|
5317
|
+
}
|
|
5318
|
+
function collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty) {
|
|
5319
|
+
const result = {};
|
|
5320
|
+
for (const [flag, property] of bodyFieldFlagToProperty) {
|
|
5321
|
+
const value = parsedFlags[flag];
|
|
5322
|
+
if (value === void 0) continue;
|
|
5323
|
+
result[property] = value;
|
|
5324
|
+
}
|
|
5325
|
+
return result;
|
|
5326
|
+
}
|
|
5327
|
+
function collectValues(parameters, flags) {
|
|
5328
|
+
const values = {};
|
|
5329
|
+
for (const parameter of parameters) {
|
|
5330
|
+
const value = coerceParameterValue(parameter, flags[flagName(parameter.name)]);
|
|
5331
|
+
if (value !== void 0) values[parameter.name] = value;
|
|
5332
|
+
}
|
|
5333
|
+
return values;
|
|
5334
|
+
}
|
|
5335
|
+
function operationOutputPayload(envelope, includeEnvelope) {
|
|
5336
|
+
return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
|
|
5337
|
+
}
|
|
5338
|
+
/**
|
|
5339
|
+
* The `--json` document for a generated command: the full response
|
|
5340
|
+
* envelope, so pagination metadata travels with the data, plus any
|
|
5341
|
+
* notes the command would otherwise print on stderr.
|
|
5342
|
+
*/
|
|
5343
|
+
function operationJsonDocument(envelope, summary = []) {
|
|
5344
|
+
const document = envelope !== null && typeof envelope === "object" ? { ...envelope } : { data: null };
|
|
5345
|
+
if (summary.length > 0) document.summary = summary.join("\n");
|
|
5346
|
+
return document;
|
|
5347
|
+
}
|
|
5348
|
+
function isIncompleteDomainVerification(operation, envelope) {
|
|
5349
|
+
if (operation.sdkName !== "verifyDomain") return false;
|
|
5350
|
+
const data = envelope?.data;
|
|
5351
|
+
if (!data || typeof data !== "object") return false;
|
|
5352
|
+
return data.verified === false;
|
|
5353
|
+
}
|
|
5354
|
+
function writeIncompleteDomainVerificationHint() {
|
|
5355
|
+
process.stderr.write("Domain verification is incomplete. Add or fix the DNS records shown above, or run `primitive domains zone-file --id <domain-id>` to download the complete zone file, then retry `primitive domains verify --id <domain-id>`.\n");
|
|
5356
|
+
}
|
|
5357
|
+
const OPERATION_HINTS = {
|
|
5358
|
+
addDomain: "Tip: after this returns a domain id, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` when the user wants an importable DNS zone file.",
|
|
5359
|
+
verifyDomain: "Tip: if DNS is still missing, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` to give the user an importable DNS zone file.",
|
|
5360
|
+
downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
|
|
5361
|
+
getInboxStatus: "Tip: prefer `primitive inbox status` for a compact readiness summary and next-step commands.",
|
|
5362
|
+
getSendPermissions: "Tip: this command answers where you may send mail to. To find usable sender domains for --from, run `primitive domains list` or `primitive inbox status` and use an address at an active verified domain.",
|
|
5363
|
+
sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
|
|
5364
|
+
createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
5365
|
+
updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
5366
|
+
createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
|
|
5367
|
+
setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
|
|
5368
|
+
setMemory: "Tip: prefer `primitive memories set <key> <json-value> [--function <function-id>]` for JSON value parsing and scope flags. This raw command exists for callers passing full request JSON.",
|
|
5369
|
+
getMemory: "Tip: prefer `primitive memories get <key> [--function <function-id>]` for the common read path.",
|
|
5370
|
+
deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
|
|
5371
|
+
searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
|
|
5372
|
+
startAgentSignup: "Tip: pass --terms-accepted, and optionally --signup-code <code> if you have one. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
|
|
5373
|
+
verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
|
|
5374
|
+
};
|
|
5375
|
+
const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
|
|
5376
|
+
const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, apiBaseUrl, writeStderr }) => {
|
|
5377
|
+
const data = envelope?.data;
|
|
5378
|
+
if (!data?.access_token || !data?.refresh_token) return;
|
|
5379
|
+
saveSignupCredentials({
|
|
5380
|
+
apiBaseUrl,
|
|
5381
|
+
configDir,
|
|
5382
|
+
signup: data
|
|
5383
|
+
});
|
|
5384
|
+
writeStderr("Credentials saved to the CLI config; `primitive whoami` will work on the next call.\n");
|
|
5385
|
+
} };
|
|
5386
|
+
function createOperationCommand(operation) {
|
|
5387
|
+
const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
|
|
5388
|
+
const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
|
|
5389
|
+
const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
|
|
5390
|
+
const hint = OPERATION_HINTS[operation.sdkName];
|
|
5391
|
+
const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
|
|
5392
|
+
const fullDescription = hint ? `${descriptionWithSchema}\n\n${hint}` : descriptionWithSchema;
|
|
5393
|
+
class OperationCommand extends Command {
|
|
5394
|
+
static description = fullDescription;
|
|
5395
|
+
static flags = flags;
|
|
5396
|
+
static summary = operation.summary ?? `${operation.method} ${operation.path}`;
|
|
5397
|
+
async run() {
|
|
5398
|
+
const { flags } = await this.parse(OperationCommand);
|
|
5399
|
+
const parsedFlags = flags;
|
|
5400
|
+
if ([
|
|
5401
|
+
"startAgentSignup",
|
|
5402
|
+
"verifyAgentSignup",
|
|
5403
|
+
"resendAgentSignupVerification"
|
|
5404
|
+
].includes(operation.operationId)) requireDefaultLoginProfile();
|
|
5405
|
+
await runWithTiming(parsedFlags.time === true, async () => {
|
|
5406
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
5407
|
+
apiKey: typeof parsedFlags["api-key"] === "string" ? parsedFlags["api-key"] : void 0,
|
|
5408
|
+
apiBaseUrl: typeof parsedFlags["api-base-url"] === "string" ? parsedFlags["api-base-url"] : void 0,
|
|
5409
|
+
configDir: this.config.configDir
|
|
5410
|
+
});
|
|
5411
|
+
let body;
|
|
5412
|
+
if (operation.hasJsonBody) {
|
|
5413
|
+
const explicit = readJsonBody(parsedFlags);
|
|
5414
|
+
const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
|
|
5415
|
+
if (Object.keys(overrides).length > 0) if (explicit === void 0) body = overrides;
|
|
5416
|
+
else if (explicit !== null && typeof explicit === "object" && !Array.isArray(explicit)) body = {
|
|
5417
|
+
...explicit,
|
|
5418
|
+
...overrides
|
|
5419
|
+
};
|
|
5420
|
+
else {
|
|
5421
|
+
const explicitKind = explicit === null ? "null" : Array.isArray(explicit) ? "array" : typeof explicit;
|
|
5422
|
+
const overrideFlags = Object.keys(overrides).map((p) => `--${flagName(p)}`).join(", ");
|
|
5423
|
+
throw new Errors.CLIError(`--raw-body must be a JSON object when also passing per-field flags (got ${explicitKind}); supplied per-field flags: ${overrideFlags}. Either drop --raw-body and rely on the per-field flags, or move every field into the JSON --raw-body and drop the flags.`);
|
|
5424
|
+
}
|
|
5425
|
+
else body = explicit;
|
|
5426
|
+
}
|
|
5427
|
+
if (operation.bodyRequired && body === void 0) throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --raw-body / --body-file.`);
|
|
5428
|
+
const operationFn = sdk_gen_exports[operation.sdkName];
|
|
5429
|
+
const query = collectValues(operation.queryParams, parsedFlags);
|
|
5430
|
+
const replyStateSurface = replyStateSurfaceForOperation(operation.sdkName, query);
|
|
5431
|
+
const automatedSurface = automatedSurfaceForOperation(operation.sdkName, query);
|
|
5432
|
+
const callOperation = () => operationFn({
|
|
5433
|
+
body,
|
|
5434
|
+
client: apiClient.client,
|
|
5435
|
+
parseAs: operation.binaryResponse ? "blob" : "auto",
|
|
5436
|
+
path: collectValues(operation.pathParams, parsedFlags),
|
|
5437
|
+
query,
|
|
5438
|
+
responseStyle: "fields"
|
|
5439
|
+
});
|
|
5440
|
+
const result = ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
|
|
5441
|
+
wait: parsedFlags.wait !== false,
|
|
5442
|
+
errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
|
|
5443
|
+
onFirstWait: () => {
|
|
5444
|
+
process.stderr.write(`Attachment content is not ready yet; waiting up to ${ATTACHMENT_WAIT_MAX_MS / 1e3}s. Pass --no-wait to return immediately.\n`);
|
|
5445
|
+
}
|
|
5446
|
+
}) : await callOperation();
|
|
5447
|
+
if (result.error) {
|
|
5448
|
+
const errorPayload = extractErrorPayload(result.error);
|
|
5449
|
+
writeErrorWithHints(errorPayload);
|
|
5450
|
+
if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
|
|
5451
|
+
if (automatedSurface && isAutomatedRejectedError(errorPayload)) process.stderr.write(`${automatedRejectedError(automatedSurface).message}\n`);
|
|
5452
|
+
surfaceUnauthorizedHint({
|
|
5453
|
+
auth,
|
|
5454
|
+
baseUrlOverridden,
|
|
5455
|
+
configDir: this.config.configDir,
|
|
5456
|
+
payload: errorPayload
|
|
5457
|
+
});
|
|
5458
|
+
const listClient = apiClient.client;
|
|
5459
|
+
const listEndpointsFn = () => listEndpoints({
|
|
5460
|
+
client: listClient,
|
|
5461
|
+
responseStyle: "fields"
|
|
5462
|
+
});
|
|
5463
|
+
await maybeWriteFunctionEndpointRedirect({
|
|
5464
|
+
sdkName: operation.sdkName,
|
|
5465
|
+
errorCode: extractErrorCode(errorPayload),
|
|
5466
|
+
endpointId: typeof parsedFlags.id === "string" ? parsedFlags.id : void 0,
|
|
5467
|
+
listEndpoints: listEndpointsFn,
|
|
5468
|
+
writeStderr: (chunk) => {
|
|
5469
|
+
process.stderr.write(chunk);
|
|
5470
|
+
}
|
|
5471
|
+
});
|
|
5472
|
+
if (operation.sdkName === "getEmail" && extractErrorCode(errorPayload) === "not_found" && typeof parsedFlags.id === "string") await explainPendingMailNotFound(this.config.configDir, parsedFlags.id);
|
|
5473
|
+
process.exitCode = 1;
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
5476
|
+
if (operation.binaryResponse) {
|
|
5477
|
+
const blob = result.data;
|
|
5478
|
+
const bytes = Buffer.from(await blob.arrayBuffer());
|
|
5479
|
+
const output = parsedFlags.output;
|
|
5480
|
+
if (typeof output === "string") {
|
|
5481
|
+
writeFileSync(output, bytes);
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5484
|
+
process.stdout.write(bytes);
|
|
5485
|
+
return;
|
|
5486
|
+
}
|
|
5487
|
+
const envelope = result.data;
|
|
5488
|
+
if (replyStateSurface) try {
|
|
5489
|
+
const rows = Array.isArray(envelope?.data) ? envelope.data : [];
|
|
5490
|
+
assertAwaitingFilterRows(rows, replyStateSurface);
|
|
5491
|
+
if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsReplyState(apiClient);
|
|
5492
|
+
} catch (error) {
|
|
5493
|
+
if (!(error instanceof ReplyStateUnsupportedError)) throw error;
|
|
5494
|
+
process.stderr.write(`${error.message}\n`);
|
|
5495
|
+
process.exitCode = 1;
|
|
5496
|
+
return;
|
|
5497
|
+
}
|
|
5498
|
+
if (automatedSurface) try {
|
|
5499
|
+
const rows = Array.isArray(envelope?.data) ? envelope.data : [];
|
|
5500
|
+
assertAutomatedVerdict(rows, automatedSurface, expectedAutomatedVerdicts(query?.automated === "true" ? true : query?.automated === "false" ? false : void 0, typeof query?.q === "string" ? query.q : void 0));
|
|
5501
|
+
if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsAutomated(apiClient);
|
|
5502
|
+
} catch (error) {
|
|
5503
|
+
if (!(error instanceof AutomatedFilterUnsupportedError)) throw error;
|
|
5504
|
+
process.stderr.write(`${error.message}\n`);
|
|
5505
|
+
process.exitCode = 1;
|
|
5506
|
+
return;
|
|
5507
|
+
}
|
|
5508
|
+
const jsonOutput = parsedFlags.json === true;
|
|
5509
|
+
const withEnvelope = parsedFlags.envelope === true;
|
|
5510
|
+
const cursor = envelope?.meta?.cursor;
|
|
5511
|
+
if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
|
|
5512
|
+
const summary = [];
|
|
5513
|
+
if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
|
|
5514
|
+
const hint = EMPTY_RESULT_HINTS[operation.sdkName];
|
|
5515
|
+
if (hint && jsonOutput) {
|
|
5516
|
+
if (withEnvelope) summary.push(hint);
|
|
5517
|
+
} else if (hint) process.stderr.write(`${hint}\n`);
|
|
5518
|
+
}
|
|
5519
|
+
writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
|
|
5520
|
+
process.stderr.write(chunk);
|
|
5521
|
+
} });
|
|
5522
|
+
const successHook = OPERATION_SUCCESS_HOOKS[operation.sdkName];
|
|
5523
|
+
if (successHook) try {
|
|
5524
|
+
successHook({
|
|
5525
|
+
envelope,
|
|
5526
|
+
configDir: this.config.configDir,
|
|
5527
|
+
apiBaseUrl: auth.apiBaseUrl,
|
|
5528
|
+
writeStderr: (chunk) => {
|
|
5529
|
+
process.stderr.write(chunk);
|
|
5530
|
+
}
|
|
5531
|
+
});
|
|
5532
|
+
} catch (hookError) {
|
|
5533
|
+
const detail = hookError instanceof Error ? hookError.message : String(hookError);
|
|
5534
|
+
process.stderr.write(`Warning: ${operation.sdkName} succeeded but its post-success hook threw (${detail}). The response below is still valid; act on it manually.\n`);
|
|
5535
|
+
}
|
|
5536
|
+
this.log(JSON.stringify(jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope), null, 2));
|
|
5537
|
+
if (isIncompleteDomainVerification(operation, envelope)) {
|
|
5538
|
+
writeIncompleteDomainVerificationHint();
|
|
5539
|
+
process.exitCode = 1;
|
|
5540
|
+
}
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
return OperationCommand;
|
|
5545
|
+
}
|
|
5546
|
+
const EMPTY_RESULT_HINTS = {
|
|
5547
|
+
listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints list` to check.",
|
|
5548
|
+
listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
|
|
5549
|
+
listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
|
|
5550
|
+
listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
|
|
5551
|
+
listFilters: "(no results) No filter rules configured.",
|
|
5552
|
+
listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
|
|
5553
|
+
};
|
|
5554
|
+
function canonicalizeCliReferences(description) {
|
|
5555
|
+
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'`");
|
|
5105
5556
|
}
|
|
5106
5557
|
//#endregion
|
|
5107
|
-
export {
|
|
5558
|
+
export { buildThrownSendFailureEnvelope as $, testFunction as $n, getContact as $t, readThreadContext as A, pollCliLogin as An, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as At, recordPendingMail as B, runWakeSchedule as Bn, createFunction as Bt, queryUsesAwaiting as C, listFunctions as Cn, assertAutomatedVerdict as Ct, displayAddress as D, listWakeAuthorizations as Dn, hasAutomatedVerdict as Dt, describeWake as E, listSentEmails as En, expectedAutomatedVerdicts as Et, wakeRecipientField as F, redeemCreditCode as Fn, addDefaultNetworkMember as Ft, replyExpectation as G, setFunctionRoute as Gn, deleteContact as Gt, interactionHumanLine as H, searchMemories as Hn, createWakeAuthorization as Ht, wakeRelationship as I, removeDefaultNetworkMember as In, checkDefaultNetworkContactAdmission as It, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as J, simulateRoute as Jn, deleteWakeAuthorization as Jt, replyInteractionWarning as K, setFunctionSecret as Kn, deleteMemory as Kt, clearReadPendingMail as L, reorderRoutes as Ln, cliLogout as Lt, serverMuted as M, putAgentContact as Mn, createCliApiClient as Mt, serverRelationship as N, putAgentContactPolicy as Nn, refreshStoredCliCredentials as Nt, formatWakeContext as O, listWakeDispatches as On, isAutomatedRejectedError as Ot, wakeReadCommand as P, putContact as Pn, resolveCliApiRequestConfig as Pt, buildFollowUpCommand as Q, stopRepeatFromEmail as Qn, getAgentContactPolicy as Qt, pendingMailPath as R, replyToEmail as Rn, completeWebhookEvent as Rt, isAwaitingRejectedError as S, listFunctionLogs as Sn, AutomatedFilterUnsupportedError as St, bareAddress as T, listRoutes as Tn, ensureSearchReportsAutomated as Tt, interactionNextActions as U, semanticSearch as Un, createWakeSchedule as Ut, removePendingMail as V, searchEmails as Vn, createRoute as Vt, readEmailInteraction as W, sendEmail as Wn, deleteAgentContact as Wt, apiResultHttpStatus as X, startAgentSignup as Xn, downloadEmailAttachmentPart as Xt, SEND_OUTCOME_HELP as Y, startAgentClaim as Yn, deleteWakeSchedule as Yt, assertValidIdempotencyKey as Z, startCliLogin as Zn, getAccount as Zt, awaitingRejectedError as _, listDefaultNetworkAgents as _n, formatAlreadySentNotice as _t, extractErrorCode as a, getFunctionRouting as an, updateWakeAuthorization as ar, formatPriorRepliesWarning as at, formatRepliesCell as b, listEmails as bn, AUTOMATED_FLAG_DESCRIPTION as bt, readTextFileFlag as c, getMemory as cn, verifyAgentSignup as cr, priorRepliesThatWentOut as ct, writeErrorWithHints as d, getSentEmail as dn, sendOutcomeExitCode as dt, getConversation as en, unsetFunctionRoute as er, checkPriorReplies as et, AWAITING_FLAG_DESCRIPTION as f, getThread as fn, sentHistoryWindowStart as ft, assertAwaitingFilterRows as g, listContacts as gn, currentMailSessionKey as gt, ReplyStateUnsupportedError as h, listAgentNetworks as hn, thrownErrorExitCode as ht, createOperationCommand as i, getFunction as in, updateRoute as ir, formatPriorRepliesCheckSkipped as it, sentInThread as j, pullWebhookEvent as jn, createAuthenticatedCliApiClient as jt, latestOwnOutbound as k, listWakeSchedules as kn, queryUsesAutomated as kt, runWithTiming as l, getOrgRoutingTopology as ln, reportSendCommandResult as lt, REPLY_STATE_UNSUPPORTED_CODE as m, listAgentContacts as mn, successfulSendOutcome as mt, API_ERROR_CODES as n, getDefaultNetworkAgent as nn, updateFunction as nr, deriveSendIdempotencyKey as nt, extractErrorPayload as o, getFunctionTestRunTrace as on, updateWakeSchedule as or, formatSendFailureSummary as ot, AWAITING_VALUES as p, getWakeSchedule as pn, serializeErrorPayload as pt, wakeInteractionSentence as q, setMemory as qn, deleteRoute as qt, TIME_FLAG_DESCRIPTION as r, getEmail as rn, updateRepeatingSend as rr, formatDeletedEarlierSendNotice as rt, formatErrorPayload as s, getInboxStatus as sn, verifyAgentClaim as sr, formatSendRecordFailureSummary as st, API_BASE_URL_FLAG_DESCRIPTION as t, getCreditBalance as tn, updateDefaultNetworkMember as tr, classifySendError as tt, surfaceUnauthorizedHint as u, getRepeatingSend as un, sendIdentityFields as ut, ensureSearchReportsReplyState as v, listDefaultNetworkMembers as vn, writeIdempotentReplayBannerIfReplay as vt, explainPendingMailNotFound as w, listRepeatingSends as wn, automatedRejectedError as wt, hasReplyState as x, listEndpoints as xn, AUTOMATED_VALUES as xt, formatAwaitingCell as y, listDomains as yn, AUTOMATED_FILTER_UNSUPPORTED_CODE as yt, readPendingMail as z, resendAgentSignupVerification as zn, createEndpoint as zt };
|