@primitivedotdev/cli 0.31.6 → 0.31.7

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.
Files changed (2) hide show
  1. package/dist/oclif/index.js +155 -15
  2. package/package.json +1 -1
@@ -13887,6 +13887,21 @@ function resolveChatResponseBody(reply) {
13887
13887
  function matchDescription(strategy) {
13888
13888
  return strategy === "strict" ? "strict, matched by reply_to_sent_email_id" : "fallback, matched by sender/time window";
13889
13889
  }
13890
+ function normalizeEmailAddress(value) {
13891
+ return value.trim().toLowerCase();
13892
+ }
13893
+ function derivedReplySubject(parent) {
13894
+ const subject = parent.subject?.trim();
13895
+ if (!subject) return "Re: (no subject)";
13896
+ return /^re:/i.test(subject) ? subject : `Re: ${subject}`;
13897
+ }
13898
+ function assertParentMatchesRecipient(parent, recipient) {
13899
+ if (normalizeEmailAddress(parent.from_email) === normalizeEmailAddress(recipient)) return;
13900
+ throw cliError$6(`Inbound email ${parent.id} is from ${parent.from_email}, not ${recipient}. Use \`primitive chat ${parent.from_email} --reply <message> --reply-to-email-id ${parent.id}\` or omit --reply-to-email-id to continue the latest inbound from ${recipient}.`);
13901
+ }
13902
+ function emailDetailFromEnvelope(envelope) {
13903
+ return envelope?.data ?? envelope ?? null;
13904
+ }
13890
13905
  function buildCommand(kind, description, argv, options = {}) {
13891
13906
  const requiresMessage = options.requiresMessage ?? false;
13892
13907
  return {
@@ -13907,15 +13922,15 @@ function buildChatFollowUpCommands(context) {
13907
13922
  "primitive",
13908
13923
  "chat",
13909
13924
  context.recipient,
13925
+ "--reply",
13910
13926
  "<message>",
13911
13927
  "--from",
13912
13928
  context.from,
13913
- "--subject",
13914
- context.subject,
13929
+ "--reply-to-email-id",
13930
+ context.reply.id,
13915
13931
  "--timeout",
13916
13932
  String(context.timeoutSeconds)
13917
13933
  ];
13918
- if (context.reply.message_id) continueParts.push("--in-reply-to", context.reply.message_id);
13919
13934
  if (context.json) continueParts.push("--json");
13920
13935
  if (context.quiet) continueParts.push("--quiet");
13921
13936
  if (context.strictOnly) continueParts.push("--strict-only");
@@ -14049,6 +14064,55 @@ function formatChatRecoveryContext(context) {
14049
14064
  for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14050
14065
  return lines.join("\n");
14051
14066
  }
14067
+ async function loadInboundEmailDetail(params) {
14068
+ const result = await getEmail({
14069
+ client: params.apiClient.client,
14070
+ path: { id: params.id },
14071
+ responseStyle: "fields"
14072
+ });
14073
+ if (result.error) {
14074
+ const payload = extractErrorPayload(result.error);
14075
+ writeErrorWithHints(payload);
14076
+ surfaceUnauthorizedHint({
14077
+ ...params.authFailureContext,
14078
+ payload
14079
+ });
14080
+ throw new Errors.CLIError(`Could not load inbound email ${params.id}.`, { exit: 1 });
14081
+ }
14082
+ const detail = emailDetailFromEnvelope(result.data);
14083
+ if (!detail) throw new Errors.CLIError(`Could not load inbound email ${params.id}: the API returned no email body.`, { exit: 1 });
14084
+ return detail;
14085
+ }
14086
+ async function findLatestInboundFromRecipient(params) {
14087
+ const result = await searchEmails({
14088
+ client: params.apiClient.client,
14089
+ query: {
14090
+ from: params.recipient,
14091
+ to: params.from,
14092
+ include_facets: "false",
14093
+ limit: params.pageSize,
14094
+ snippet: "false",
14095
+ sort: "received_at_desc"
14096
+ },
14097
+ responseStyle: "fields"
14098
+ });
14099
+ if (result.error) {
14100
+ const payload = extractErrorPayload(result.error);
14101
+ writeErrorWithHints(payload);
14102
+ surfaceUnauthorizedHint({
14103
+ ...params.authFailureContext,
14104
+ payload
14105
+ });
14106
+ throw new Errors.CLIError("Could not find a prior chat reply.", { exit: 1 });
14107
+ }
14108
+ const row = (result.data?.data ?? []).find((email) => email.status === "accepted" || email.status === "completed");
14109
+ if (!row) return null;
14110
+ return loadInboundEmailDetail({
14111
+ apiClient: params.apiClient,
14112
+ authFailureContext: params.authFailureContext,
14113
+ id: row.id
14114
+ });
14115
+ }
14052
14116
  var ChatCommand = class ChatCommand extends Command {
14053
14117
  static description = `Send a message to an address and wait for the reply.
14054
14118
 
@@ -14062,6 +14126,13 @@ var ChatCommand = class ChatCommand extends Command {
14062
14126
  follow-up commands as templates. The default transcript is for humans;
14063
14127
  agents and scripts should pass --json for parse-safe output.
14064
14128
 
14129
+ To continue an existing chat, pass --reply '<message>'. By default,
14130
+ the CLI replies to the latest inbound email from the recipient to
14131
+ your sender address. For exact continuation, pass
14132
+ --reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
14133
+ reply endpoint, so the reply subject and threading headers are
14134
+ derived from the inbound email instead of copied into CLI flags.
14135
+
14065
14136
  --json emits a structured envelope with both sides of the exchange,
14066
14137
  a direct response_body field, match details, and follow-up command
14067
14138
  metadata such as kind, argv, placeholders, and requires_message.
@@ -14079,6 +14150,8 @@ var ChatCommand = class ChatCommand extends Command {
14079
14150
  static examples = [
14080
14151
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14081
14152
  "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
14153
+ "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14154
+ "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14082
14155
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
14083
14156
  "<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
14084
14157
  ];
@@ -14106,7 +14179,9 @@ var ChatCommand = class ChatCommand extends Command {
14106
14179
  }),
14107
14180
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
14108
14181
  subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
14109
- "in-reply-to": Flags.string({ description: "Message-Id of the parent email to thread this against. Use when continuing a prior conversation from outside the CLI; for an inbound you received via Primitive, prefer `primitive reply --id <inbound-id>`." }),
14182
+ reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
14183
+ "reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
14184
+ "in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
14110
14185
  json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
14111
14186
  quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14112
14187
  timeout: Flags.integer({
@@ -14136,8 +14211,12 @@ var ChatCommand = class ChatCommand extends Command {
14136
14211
  };
14137
14212
  async run() {
14138
14213
  const { args, flags } = await this.parse(ChatCommand);
14139
- const message = args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
14140
- if (!message.trim()) throw cliError$6("Message body is empty.");
14214
+ const replyMode = flags.reply !== void 0 || flags["reply-to-email-id"] !== void 0;
14215
+ if (flags.reply !== void 0 && args.message !== void 0 && args.message !== "") throw cliError$6("Pass the reply body either as --reply or as the positional message, not both.");
14216
+ if (replyMode && flags.subject !== void 0) throw cliError$6("--subject is not used with --reply. Primitive derives the reply subject from the inbound email.");
14217
+ if (replyMode && flags["in-reply-to"] !== void 0) throw cliError$6("Use --reply-to-email-id with --reply instead of raw --in-reply-to.");
14218
+ const message = flags.reply !== void 0 ? flags.reply : args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
14219
+ if (!message.trim()) throw cliError$6(replyMode ? "Reply body is empty." : "Message body is empty.");
14141
14220
  await runWithTiming(flags.time, async () => {
14142
14221
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
14143
14222
  apiKey: flags["api-key"],
@@ -14150,12 +14229,71 @@ var ChatCommand = class ChatCommand extends Command {
14150
14229
  baseUrlOverridden,
14151
14230
  configDir: this.config.configDir
14152
14231
  };
14153
- const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14154
- const subject = flags.subject ?? deriveSubject(message);
14155
- const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
14156
14232
  const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
14157
- progress?.start(`Sending message to ${args.recipient}`);
14158
- const sendResult = await sendEmail({
14233
+ let from;
14234
+ let parentReply;
14235
+ let subject;
14236
+ if (replyMode) {
14237
+ const replyContext = await (async () => {
14238
+ let replyContextFailureMessage = "Could not load reply context.";
14239
+ try {
14240
+ if (flags["reply-to-email-id"] !== void 0) {
14241
+ progress?.start(`Loading reply context for ${flags["reply-to-email-id"]}`);
14242
+ const exactParentReply = await loadInboundEmailDetail({
14243
+ apiClient,
14244
+ authFailureContext,
14245
+ id: flags["reply-to-email-id"]
14246
+ });
14247
+ replyContextFailureMessage = `Inbound email ${flags["reply-to-email-id"]} does not match recipient ${args.recipient}.`;
14248
+ assertParentMatchesRecipient(exactParentReply, args.recipient);
14249
+ return {
14250
+ from: flags.from ?? exactParentReply.to_email,
14251
+ parentReply: exactParentReply
14252
+ };
14253
+ }
14254
+ const replyFrom = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14255
+ progress?.start(`Finding latest inbound email from ${args.recipient}`);
14256
+ const latestParentReply = await findLatestInboundFromRecipient({
14257
+ apiClient,
14258
+ authFailureContext,
14259
+ from: replyFrom,
14260
+ pageSize: flags["page-size"],
14261
+ recipient: args.recipient
14262
+ });
14263
+ if (!latestParentReply) {
14264
+ replyContextFailureMessage = "No prior inbound email found.";
14265
+ throw cliError$6(`No prior inbound email from ${args.recipient} to ${replyFrom}. Start a new chat with \`primitive chat ${args.recipient} <message>\`, pass --from, or pass --reply-to-email-id <inbound-email-id>.`);
14266
+ }
14267
+ replyContextFailureMessage = `Inbound email ${latestParentReply.id} does not match recipient ${args.recipient}.`;
14268
+ assertParentMatchesRecipient(latestParentReply, args.recipient);
14269
+ return {
14270
+ from: replyFrom,
14271
+ parentReply: latestParentReply
14272
+ };
14273
+ } catch (error) {
14274
+ progress?.fail(replyContextFailureMessage);
14275
+ throw error;
14276
+ }
14277
+ })();
14278
+ from = replyContext.from;
14279
+ parentReply = replyContext.parentReply;
14280
+ subject = derivedReplySubject(replyContext.parentReply);
14281
+ } else {
14282
+ from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14283
+ subject = flags.subject ?? deriveSubject(message);
14284
+ }
14285
+ const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
14286
+ if (replyMode) progress?.update(`Sending reply to ${args.recipient}`);
14287
+ else progress?.start(`Sending message to ${args.recipient}`);
14288
+ const sendResult = parentReply !== void 0 ? await replyToEmail({
14289
+ body: {
14290
+ body_text: message,
14291
+ from
14292
+ },
14293
+ client: apiClient.client,
14294
+ path: { id: parentReply.id },
14295
+ responseStyle: "fields"
14296
+ }) : await sendEmail({
14159
14297
  body: {
14160
14298
  from,
14161
14299
  to: args.recipient,
@@ -14167,7 +14305,7 @@ var ChatCommand = class ChatCommand extends Command {
14167
14305
  responseStyle: "fields"
14168
14306
  });
14169
14307
  if (sendResult.error) {
14170
- progress?.fail("Message send failed.");
14308
+ progress?.fail(replyMode ? "Reply send failed." : "Message send failed.");
14171
14309
  const errorPayload = extractErrorPayload(sendResult.error);
14172
14310
  writeErrorWithHints(errorPayload);
14173
14311
  surfaceUnauthorizedHint({
@@ -14182,13 +14320,15 @@ var ChatCommand = class ChatCommand extends Command {
14182
14320
  progress?.fail("Send succeeded but the API returned no data.");
14183
14321
  throw cliError$6("Send succeeded but the API returned no data.");
14184
14322
  }
14185
- progress?.update(`Message sent; waiting for reply from ${args.recipient}`, {
14323
+ const replyAddress = sent.from || from;
14324
+ progress?.update(`${replyMode ? "Reply" : "Message"} sent; waiting for reply from ${args.recipient}`, {
14186
14325
  heartbeatMs: 15e3,
14187
14326
  timeoutSeconds: flags.timeout
14188
14327
  });
14189
14328
  const baseContext = {
14190
- from,
14329
+ from: replyAddress,
14191
14330
  json: flags.json,
14331
+ parentReply,
14192
14332
  quiet: flags.quiet,
14193
14333
  recipient: args.recipient,
14194
14334
  sent,
@@ -14203,7 +14343,7 @@ var ChatCommand = class ChatCommand extends Command {
14203
14343
  replyResult = await waitForReply({
14204
14344
  apiClient,
14205
14345
  authFailureContext,
14206
- from,
14346
+ from: replyAddress,
14207
14347
  interval: flags.interval,
14208
14348
  notice: (message) => {
14209
14349
  if (progress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.31.6",
3
+ "version": "0.31.7",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,