@primitivedotdev/cli 0.31.8 → 0.32.1

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.
@@ -2,10 +2,10 @@ import { Args, Command, Errors, Flags } from "@oclif/core";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
- import { spawn } from "node:child_process";
6
5
  import { hostname } from "node:os";
7
6
  import process$1 from "node:process";
8
7
  import { createInterface } from "node:readline/promises";
8
+ import { spawn } from "node:child_process";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __defProp = Object.defineProperty;
11
11
  var __exportAll = (all, no_symbols) => {
@@ -13133,8 +13133,14 @@ var PrimitiveApiClient = class {
13133
13133
  //#region src/oclif/auth.ts
13134
13134
  const CREDENTIALS_FILE = "credentials.json";
13135
13135
  const CREDENTIALS_LOCK_DIR = "credentials.lock";
13136
+ const CREDENTIALS_LOCK_OWNER_FILE = "owner.json";
13136
13137
  const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
13137
13138
  const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive signin`.";
13139
+ const CREDENTIALS_LOCK_CLEANUP_SIGNALS = [
13140
+ "SIGINT",
13141
+ "SIGTERM",
13142
+ "SIGHUP"
13143
+ ];
13138
13144
  function isRecord$2(value) {
13139
13145
  return value !== null && typeof value === "object" && !Array.isArray(value);
13140
13146
  }
@@ -13181,6 +13187,9 @@ function parseCredentials(raw) {
13181
13187
  function credentialsPath(configDir) {
13182
13188
  return join(configDir, CREDENTIALS_FILE);
13183
13189
  }
13190
+ function credentialsLockPath(configDir) {
13191
+ return join(configDir, CREDENTIALS_LOCK_DIR);
13192
+ }
13184
13193
  function normalize(url, fallback) {
13185
13194
  const trimmed = url?.trim();
13186
13195
  if (!trimmed) return fallback;
@@ -13239,6 +13248,12 @@ function saveCliCredentials(configDir, credentials) {
13239
13248
  function deleteCliCredentials(configDir) {
13240
13249
  rmSync(credentialsPath(configDir), { force: true });
13241
13250
  }
13251
+ function deleteCliCredentialsLock(configDir) {
13252
+ rmSync(credentialsLockPath(configDir), {
13253
+ force: true,
13254
+ recursive: true
13255
+ });
13256
+ }
13242
13257
  function errorCode(error) {
13243
13258
  return error && typeof error === "object" ? error.code : void 0;
13244
13259
  }
@@ -13256,12 +13271,85 @@ function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
13256
13271
  });
13257
13272
  return true;
13258
13273
  }
13274
+ function readCliCredentialsLockOwner(lockPath) {
13275
+ let raw;
13276
+ try {
13277
+ raw = readFileSync(join(lockPath, CREDENTIALS_LOCK_OWNER_FILE), "utf8");
13278
+ } catch (error) {
13279
+ if (errorCode(error) === "ENOENT") return null;
13280
+ throw error;
13281
+ }
13282
+ try {
13283
+ const pid = JSON.parse(raw)?.pid;
13284
+ return Number.isInteger(pid) && pid > 0 ? { pid } : null;
13285
+ } catch {
13286
+ return null;
13287
+ }
13288
+ }
13289
+ function processIsRunning(pid) {
13290
+ try {
13291
+ process.kill(pid, 0);
13292
+ return true;
13293
+ } catch (error) {
13294
+ if (errorCode(error) === "ESRCH") return false;
13295
+ return true;
13296
+ }
13297
+ }
13298
+ function removeRecoverableCliCredentialsLock(params) {
13299
+ const owner = readCliCredentialsLockOwner(params.lockPath);
13300
+ if (owner && params.isRunning(owner.pid)) return false;
13301
+ if (owner) {
13302
+ rmSync(params.lockPath, {
13303
+ force: true,
13304
+ recursive: true
13305
+ });
13306
+ return true;
13307
+ }
13308
+ return removeStaleCliCredentialsLock(params.lockPath, params.staleMs, params.now);
13309
+ }
13310
+ function writeCliCredentialsLockOwner(lockPath) {
13311
+ const ownerPath = join(lockPath, CREDENTIALS_LOCK_OWNER_FILE);
13312
+ writeFileSync(ownerPath, `${JSON.stringify({
13313
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
13314
+ pid: process.pid
13315
+ })}\n`, { mode: 384 });
13316
+ chmodSync(ownerPath, 384);
13317
+ }
13318
+ function installCredentialsLockSignalCleanup(lockPath) {
13319
+ let active = true;
13320
+ const listeners = CREDENTIALS_LOCK_CLEANUP_SIGNALS.map((signal) => {
13321
+ const listener = () => {
13322
+ if (!active) return;
13323
+ active = false;
13324
+ rmSync(lockPath, {
13325
+ force: true,
13326
+ recursive: true
13327
+ });
13328
+ process.exit(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129);
13329
+ };
13330
+ process.once(signal, listener);
13331
+ return {
13332
+ listener,
13333
+ signal
13334
+ };
13335
+ });
13336
+ return () => {
13337
+ if (!active) return;
13338
+ active = false;
13339
+ for (const { listener, signal } of listeners) process.removeListener(signal, listener);
13340
+ };
13341
+ }
13342
+ function credentialsLockInProgressMessage(lockPath) {
13343
+ return `Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry. If no Primitive auth command is still running, run \`primitive logout --force\` to clear local CLI auth state and remove ${lockPath}.`;
13344
+ }
13259
13345
  function acquireCliCredentialsLock(configDir, options = {}) {
13260
13346
  mkdirSync(configDir, {
13261
13347
  mode: 448,
13262
13348
  recursive: true
13263
13349
  });
13264
- const lockPath = join(configDir, CREDENTIALS_LOCK_DIR);
13350
+ const lockPath = credentialsLockPath(configDir);
13351
+ const installSignalHandlers = options.installSignalHandlers ?? true;
13352
+ const isRunning = options.isProcessRunning ?? processIsRunning;
13265
13353
  const now = options.now ?? Date.now;
13266
13354
  const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
13267
13355
  let acquired = false;
@@ -13271,14 +13359,30 @@ function acquireCliCredentialsLock(configDir, options = {}) {
13271
13359
  break;
13272
13360
  } catch (error) {
13273
13361
  if (errorCode(error) !== "EEXIST") throw error;
13274
- if (removeStaleCliCredentialsLock(lockPath, staleMs, now)) continue;
13275
- throw new Error("Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.");
13362
+ if (removeRecoverableCliCredentialsLock({
13363
+ isRunning,
13364
+ lockPath,
13365
+ now,
13366
+ staleMs
13367
+ })) continue;
13368
+ throw new Error(credentialsLockInProgressMessage(lockPath));
13369
+ }
13370
+ if (!acquired) throw new Error(credentialsLockInProgressMessage(lockPath));
13371
+ try {
13372
+ writeCliCredentialsLockOwner(lockPath);
13373
+ } catch (error) {
13374
+ rmSync(lockPath, {
13375
+ force: true,
13376
+ recursive: true
13377
+ });
13378
+ throw error;
13276
13379
  }
13277
- if (!acquired) throw new Error("Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.");
13380
+ const removeSignalCleanup = installSignalHandlers ? installCredentialsLockSignalCleanup(lockPath) : () => void 0;
13278
13381
  let released = false;
13279
13382
  return () => {
13280
13383
  if (released) return;
13281
13384
  released = true;
13385
+ removeSignalCleanup();
13282
13386
  rmSync(lockPath, {
13283
13387
  force: true,
13284
13388
  recursive: true
@@ -13298,7 +13402,7 @@ function resolveCliAuth(params) {
13298
13402
  const credentials = loadCliCredentials(params.configDir);
13299
13403
  if (credentials) return {
13300
13404
  apiKey: credentials.access_token,
13301
- apiBaseUrl1: params.apiBaseUrl1 ? normalizeApiBaseUrl1(params.apiBaseUrl1) : credentials.api_base_url_1,
13405
+ apiBaseUrl1: credentials.api_base_url_1,
13302
13406
  apiBaseUrl2,
13303
13407
  credentials,
13304
13408
  source: "stored"
@@ -14051,7 +14155,10 @@ const RESERVED_FLAG_NAMES = new Set([
14051
14155
  ]);
14052
14156
  function bodyFieldFlag(field) {
14053
14157
  const common = { description: field.description || field.name };
14054
- if (field.kind === "boolean") return Flags.boolean(common);
14158
+ if (field.kind === "boolean") return Flags.boolean({
14159
+ ...common,
14160
+ allowNo: true
14161
+ });
14055
14162
  if (field.kind === "integer") return Flags.integer({
14056
14163
  ...common,
14057
14164
  ...numericFlagOptions(field)
@@ -14084,7 +14191,10 @@ function buildFlags(operation) {
14084
14191
  }),
14085
14192
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14086
14193
  };
14087
- if (!operation.binaryResponse) 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." });
14194
+ if (!operation.binaryResponse) {
14195
+ flags.json = Flags.boolean({ description: "Accepted for consistency with task-focused commands. Generated API commands already print JSON by default." });
14196
+ 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." });
14197
+ }
14088
14198
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
14089
14199
  const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
14090
14200
  if (operation.hasJsonBody) {
@@ -14126,11 +14236,21 @@ function collectValues(parameters, flags) {
14126
14236
  function operationOutputPayload(envelope, includeEnvelope) {
14127
14237
  return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
14128
14238
  }
14239
+ function isIncompleteDomainVerification(operation, envelope) {
14240
+ if (operation.sdkName !== "verifyDomain") return false;
14241
+ const data = envelope?.data;
14242
+ if (!data || typeof data !== "object") return false;
14243
+ return data.verified === false;
14244
+ }
14245
+ function writeIncompleteDomainVerificationHint() {
14246
+ 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");
14247
+ }
14129
14248
  const OPERATION_HINTS = {
14130
14249
  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.",
14131
14250
  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.",
14132
14251
  downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
14133
14252
  getInboxStatus: "Tip: prefer `primitive inbox status` for a compact readiness summary and next-step commands.",
14253
+ 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.",
14134
14254
  sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
14135
14255
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
14136
14256
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
@@ -14233,6 +14353,10 @@ function createOperationCommand(operation) {
14233
14353
  process.stderr.write(chunk);
14234
14354
  } });
14235
14355
  this.log(JSON.stringify(operationOutputPayload(envelope, parsedFlags.envelope === true), null, 2));
14356
+ if (isIncompleteDomainVerification(operation, envelope)) {
14357
+ writeIncompleteDomainVerificationHint();
14358
+ process.exitCode = 1;
14359
+ }
14236
14360
  });
14237
14361
  }
14238
14362
  }
@@ -14348,6 +14472,13 @@ function cursorFromRows(rows) {
14348
14472
  const last = rows.at(-1);
14349
14473
  return last ? encodeReceivedAtSearchCursor(last) : null;
14350
14474
  }
14475
+ function cursorFromAcceptedRows(rows) {
14476
+ for (let i = rows.length - 1; i >= 0; i--) {
14477
+ const row = rows[i];
14478
+ if (row.status === "accepted" || row.status === "completed") return encodeReceivedAtSearchCursor(row);
14479
+ }
14480
+ return null;
14481
+ }
14351
14482
  function collectNewAcceptedEmails(rows, seenIds) {
14352
14483
  const fresh = [];
14353
14484
  for (const row of rows) {
@@ -14560,6 +14691,8 @@ function buildCommand(kind, description, argv, options = {}) {
14560
14691
  }
14561
14692
  function buildChatFollowUpCommands(context) {
14562
14693
  const commands = [];
14694
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS;
14695
+ const shouldPreferStrictContinuation = context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
14563
14696
  const continueParts = [
14564
14697
  "primitive",
14565
14698
  "chat",
@@ -14575,8 +14708,8 @@ function buildChatFollowUpCommands(context) {
14575
14708
  ];
14576
14709
  if (context.json) continueParts.push("--json");
14577
14710
  if (context.quiet) continueParts.push("--quiet");
14578
- if (context.strictOnly) continueParts.push("--strict-only");
14579
- else if (context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
14711
+ if (shouldPreferStrictContinuation) continueParts.push("--strict-only");
14712
+ else if (hasCustomStrictPhase) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
14580
14713
  commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
14581
14714
  commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
14582
14715
  "primitive",
@@ -14611,41 +14744,40 @@ function buildChatFollowUpCommands(context) {
14611
14744
  return commands;
14612
14745
  }
14613
14746
  function buildChatRecoveryCommands(context) {
14614
- return [
14615
- buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
14616
- "primitive",
14617
- "emails",
14618
- "wait",
14619
- "--reply-to-sent-email-id",
14620
- context.sent.id,
14621
- "--to",
14622
- context.from,
14623
- "--since",
14624
- context.sentAtIso,
14625
- "--timeout",
14626
- String(context.timeoutSeconds)
14627
- ]),
14628
- buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
14629
- "primitive",
14630
- "emails",
14631
- "wait",
14632
- "--from",
14633
- context.recipient,
14634
- "--to",
14635
- context.from,
14636
- "--since",
14637
- context.sentAtIso,
14638
- "--timeout",
14639
- String(context.timeoutSeconds)
14640
- ]),
14641
- buildCommand("inspect_sent_email", "Inspect the outbound send", [
14642
- "primitive",
14643
- "sent",
14644
- "get",
14645
- "--id",
14646
- context.sent.id
14647
- ])
14648
- ];
14747
+ const commands = [buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
14748
+ "primitive",
14749
+ "emails",
14750
+ "wait",
14751
+ "--reply-to-sent-email-id",
14752
+ context.sent.id,
14753
+ "--to",
14754
+ context.from,
14755
+ "--since",
14756
+ context.sentAtIso,
14757
+ "--timeout",
14758
+ String(context.timeoutSeconds)
14759
+ ])];
14760
+ if (!context.strictOnly) commands.push(buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
14761
+ "primitive",
14762
+ "emails",
14763
+ "wait",
14764
+ "--from",
14765
+ context.recipient,
14766
+ "--to",
14767
+ context.from,
14768
+ "--since",
14769
+ context.sentAtIso,
14770
+ "--timeout",
14771
+ String(context.timeoutSeconds)
14772
+ ]));
14773
+ commands.push(buildCommand("inspect_sent_email", "Inspect the outbound send", [
14774
+ "primitive",
14775
+ "sent",
14776
+ "get",
14777
+ "--id",
14778
+ context.sent.id
14779
+ ]));
14780
+ return commands;
14649
14781
  }
14650
14782
  function buildChatJsonEnvelope(context) {
14651
14783
  const responseBody = resolveChatResponseBody(context.reply);
@@ -14685,7 +14817,7 @@ function formatChatResponse(context) {
14685
14817
  ];
14686
14818
  if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
14687
14819
  if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
14688
- lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.");
14820
+ lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.", " When shown, --strict-only prefers timing out over matching the wrong reply.");
14689
14821
  for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14690
14822
  lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
14691
14823
  return lines.join("\n");
@@ -14785,13 +14917,14 @@ var ChatCommand = class ChatCommand extends Command {
14785
14917
  --strict-only is not set, it falls back to a weaker sender/time
14786
14918
  window match: from=<recipient>, to=<sender>, and since=<send time>.
14787
14919
  The fallback can catch clients that strip threading headers, but it
14788
- is less exact than strict matching. Progress is written to stderr
14789
- while the CLI waits. Exits non-zero on timeout and prints recovery
14790
- commands when the send succeeded but no reply was returned.`;
14920
+ is less exact than strict matching. Use --strict-only when matching
14921
+ the wrong reply is worse than timing out. Progress is written to
14922
+ stderr while the CLI waits. Exits non-zero on timeout and prints
14923
+ recovery commands when the send succeeded but no reply was returned.`;
14791
14924
  static summary = "Chat with an agent over email (send and wait for the reply)";
14792
14925
  static examples = [
14793
14926
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14794
- "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
14927
+ "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14795
14928
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14796
14929
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14797
14930
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
@@ -14820,7 +14953,10 @@ var ChatCommand = class ChatCommand extends Command {
14820
14953
  hidden: true
14821
14954
  }),
14822
14955
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
14823
- subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
14956
+ subject: Flags.string({
14957
+ description: "Advanced email transport override. Usually omit; chat threading does not depend on the subject.",
14958
+ hidden: true
14959
+ }),
14824
14960
  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." }),
14825
14961
  "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." }),
14826
14962
  "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." }),
@@ -14836,7 +14972,7 @@ var ChatCommand = class ChatCommand extends Command {
14836
14972
  description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
14837
14973
  min: 1
14838
14974
  }),
14839
- "strict-only": Flags.boolean({ description: "Disable the time-window fallback. Only accept inbounds whose threading headers (In-Reply-To / References) resolve to this send. Recommended when correctness matters more than success rate (e.g. agents talking to agents)." }),
14975
+ "strict-only": Flags.boolean({ description: "Disable the time-window fallback. Only accept inbounds whose threading headers (In-Reply-To / References) resolve to this send. Use when matching the wrong reply is worse than timing out." }),
14840
14976
  interval: Flags.integer({
14841
14977
  default: 2,
14842
14978
  description: "Seconds between polls while waiting for the reply.",
@@ -15127,6 +15263,38 @@ function redactConfig(config) {
15127
15263
  environments: Object.fromEntries(Object.entries(config.environments).map(([name, environment]) => [name, redactCliEnvironment(environment)]))
15128
15264
  };
15129
15265
  }
15266
+ function upsertCliEnvironmentAndClearCredentialsIfSwitched(params) {
15267
+ const previousConfig = loadOrCreateConfig(params.configDir);
15268
+ const previousActiveEnvironment = resolveConfigEnvironment(previousConfig);
15269
+ const previousEnvironment = previousActiveEnvironment?.name ?? null;
15270
+ const config = upsertCliEnvironment({
15271
+ apiBaseUrl1: params.apiBaseUrl1,
15272
+ apiBaseUrl2: params.apiBaseUrl2,
15273
+ config: previousConfig,
15274
+ environmentName: params.environmentName,
15275
+ headers: params.headers,
15276
+ unsetHeaders: params.unsetHeaders
15277
+ });
15278
+ const activeEnvironment = resolveConfigEnvironment(config);
15279
+ const environment = activeEnvironment?.name ?? null;
15280
+ const shouldClearCredentials = existsSync(credentialsPath(params.configDir)) && (previousEnvironment !== environment || previousActiveEnvironment?.config.api_base_url_1 !== activeEnvironment?.config.api_base_url_1);
15281
+ let removedCredentials = false;
15282
+ if (shouldClearCredentials) {
15283
+ const releaseLock = acquireCliCredentialsLock(params.configDir);
15284
+ try {
15285
+ saveCliConfig(params.configDir, config);
15286
+ removedCredentials = existsSync(credentialsPath(params.configDir));
15287
+ deleteCliCredentials(params.configDir);
15288
+ } finally {
15289
+ releaseLock();
15290
+ }
15291
+ } else saveCliConfig(params.configDir, config);
15292
+ return {
15293
+ environment,
15294
+ previousEnvironment,
15295
+ removedCredentials
15296
+ };
15297
+ }
15130
15298
  function switchCliEnvironment(configDir, environmentName) {
15131
15299
  const environment = normalizeCliEnvironmentName(environmentName);
15132
15300
  const config = loadOrCreateConfig(configDir);
@@ -15176,16 +15344,16 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
15176
15344
  const { flags } = await this.parse(ConfigSetCommand);
15177
15345
  const headers = flags.header ?? [];
15178
15346
  if (flags["api-base-url-1"] === void 0 && flags["api-base-url-2"] === void 0 && headers.length === 0 && (flags["unset-header"] ?? []).length === 0) throw new Errors.CLIError("Nothing to set. Pass an API base URL, --header, or --unset-header.", { exit: 1 });
15179
- const config = upsertCliEnvironment({
15347
+ const { environment, removedCredentials } = upsertCliEnvironmentAndClearCredentialsIfSwitched({
15180
15348
  apiBaseUrl1: flags["api-base-url-1"],
15181
15349
  apiBaseUrl2: flags["api-base-url-2"],
15182
- config: loadOrCreateConfig(this.config.configDir),
15350
+ configDir: this.config.configDir,
15183
15351
  environmentName: flags.environment,
15184
15352
  headers,
15185
15353
  unsetHeaders: flags["unset-header"]
15186
15354
  });
15187
- saveCliConfig(this.config.configDir, config);
15188
- process.stderr.write(`Primitive CLI environment ${config.current_environment} is active.\n`);
15355
+ process.stderr.write(`Primitive CLI environment ${environment} is active.\n`);
15356
+ if (removedCredentials) process.stderr.write("Removed saved Primitive CLI credentials. Run `primitive signin` to authenticate in the active environment.\n");
15189
15357
  }
15190
15358
  };
15191
15359
  var ConfigUseCommand = class ConfigUseCommand extends Command {
@@ -15875,7 +16043,9 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
15875
16043
  process.exitCode = 1;
15876
16044
  return;
15877
16045
  }
15878
- cursor = page.cursor ?? cursor;
16046
+ const nextCursor = cursorFromAcceptedRows(page.rows);
16047
+ const cursorAdvanced = Boolean(nextCursor && nextCursor !== cursor);
16048
+ if (nextCursor) cursor = nextCursor;
15879
16049
  for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
15880
16050
  if (flags.table) {
15881
16051
  if (!headerPrinted) {
@@ -15887,7 +16057,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
15887
16057
  matched += 1;
15888
16058
  if (matched >= flags.number) return;
15889
16059
  }
15890
- if (page.rows.length > 0) continue;
16060
+ if (cursorAdvanced) continue;
15891
16061
  if (deadline !== null && Date.now() >= deadline) break;
15892
16062
  await sleep$1(flags.interval * 1e3);
15893
16063
  }
@@ -15997,7 +16167,9 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
15997
16167
  process.exitCode = 1;
15998
16168
  return;
15999
16169
  }
16000
- cursor = page.cursor ?? cursor;
16170
+ const nextCursor = cursorFromAcceptedRows(page.rows);
16171
+ const cursorAdvanced = Boolean(nextCursor && nextCursor !== cursor);
16172
+ if (nextCursor) cursor = nextCursor;
16001
16173
  for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
16002
16174
  if (flags.jsonl) this.log(JSON.stringify(email));
16003
16175
  else {
@@ -16010,7 +16182,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
16010
16182
  printed += 1;
16011
16183
  if (flags.number && printed >= flags.number) return;
16012
16184
  }
16013
- if (page.rows.length > 0) continue;
16185
+ if (cursorAdvanced) continue;
16014
16186
  if (deadline !== null && Date.now() >= deadline) break;
16015
16187
  await sleep$1(flags.interval * 1e3);
16016
16188
  }
@@ -16765,8 +16937,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
16765
16937
  name: "Primitive Team",
16766
16938
  url: "https://primitive.dev"
16767
16939
  };
16768
- const SDK_VERSION_RANGE = "^0.31.1";
16769
- const CLI_VERSION_RANGE = "^0.31.1";
16940
+ const SDK_VERSION_RANGE = "^0.32.0";
16941
+ const CLI_VERSION_RANGE = "^0.32.1";
16770
16942
  const ESBUILD_VERSION_RANGE = "^0.27.0";
16771
16943
  function renderHandler() {
16772
16944
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -16792,49 +16964,93 @@ interface Env {
16792
16964
  PRIMITIVE_WEBHOOK_SECRET: string;
16793
16965
  }
16794
16966
 
16795
- // Loop-protection knob. Only used by the isLoop helper below; the
16796
- // handler's outbound reply address is server-defaulted (no
16797
- // from-address parameter is passed to client.reply). Update this if
16798
- // you later switch to sending from a non-managed-domain address so
16799
- // the loop guard recognizes mail returning to it as self-traffic.
16800
- const REPLY_FROM = "you@your-domain.primitive.email";
16967
+ // Optional loop-protection knob. client.reply() server-defaults the
16968
+ // outbound from-address from the inbound recipient, so most handlers
16969
+ // do not need to fill this in. Add any extra addresses your handler
16970
+ // sends from if you later switch to client.send or a custom domain.
16971
+ const EXTRA_SELF_ADDRESSES: string[] = [
16972
+ // "bot@your-domain.example",
16973
+ ];
16974
+
16975
+ function extractEmailAddresses(value: string | null | undefined): string[] {
16976
+ return (
16977
+ value?.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi)?.map((address) =>
16978
+ address.toLowerCase(),
16979
+ ) ?? []
16980
+ );
16981
+ }
16982
+
16983
+ function domainPart(address: string): string | null {
16984
+ const at = address.lastIndexOf("@");
16985
+ return at === -1 ? null : address.slice(at + 1).toLowerCase();
16986
+ }
16987
+
16988
+ function localPart(address: string): string {
16989
+ const at = address.lastIndexOf("@");
16990
+ return at === -1 ? address.toLowerCase() : address.slice(0, at).toLowerCase();
16991
+ }
16992
+
16993
+ function inboundRecipientAddresses(event: EmailReceivedEvent): string[] {
16994
+ return [
16995
+ ...event.email.smtp.rcpt_to.flatMap(extractEmailAddresses),
16996
+ ...extractEmailAddresses(event.email.headers.to),
16997
+ ];
16998
+ }
16999
+
17000
+ function inboundRecipientDomains(event: EmailReceivedEvent): Set<string> {
17001
+ return new Set(
17002
+ inboundRecipientAddresses(event)
17003
+ .map(domainPart)
17004
+ .filter((domain): domain is string => domain !== null),
17005
+ );
17006
+ }
16801
17007
 
16802
17008
  // Loop protection. A newly deployed Function starts as a fallback
16803
- // endpoint for managed *.primitive.email domains that do not have a
16804
- // domain-scoped endpoint. That can include bounces and auto-replies
16805
- // generated by the handler's own outbound traffic. Without this guard
16806
- // the handler can respond to its own bounces and create a fan-out loop.
17009
+ // endpoint for managed domains and can receive bounces or auto-replies
17010
+ // generated by its own outbound mail. Do not hardcode a managed suffix:
17011
+ // staging, production, and custom domains all arrive through the same
17012
+ // webhook shape. Derive the handler's inbound domains from the actual
17013
+ // SMTP recipients instead.
16807
17014
  //
16808
- // The default check returns true when From is on any *.primitive.email
16809
- // address (covers managed-domain fallback mail, the simple
16810
- // self-reply case, and bounces from mailer-daemon@*.primitive.email)
16811
- // or when From contains REPLY_FROM as a case-insensitive substring.
16812
- // Substring matching is deliberate so display-name forms like
16813
- // "Support <support@example.com>" match a bare-address REPLY_FROM,
16814
- // but it also accepts false positives where REPLY_FROM is a suffix
16815
- // of another address (e.g. REPLY_FROM="info@x.com" matches
16816
- // "mr.info@x.com"). For strict equality, parse the address out of the
16817
- // header and exact-match against REPLY_FROM.
17015
+ // The default check skips:
17016
+ // - direct self-mail where From equals one of the inbound recipients;
17017
+ // - mailer-daemon/postmaster bounces from the same domain as the inbound;
17018
+ // - any address explicitly listed in EXTRA_SELF_ADDRESSES.
16818
17019
  //
16819
17020
  // Extend this helper if you need stricter detection. Common additions:
16820
- // - Match the org's signup / account-owner email (not auto-injected
16821
- // into env today; either bake it into a SIGNUP_EMAIL const or read
16822
- // it from a secret you set via \`primitive functions:set-secret\`).
16823
17021
  // - Honor RFC 3834 auto-response headers: skip when
16824
- // \`event.email.headers["auto-submitted"]\` is anything other than
16825
- // "no", or when a \`List-Unsubscribe\` / \`Precedence: bulk\` header
16826
- // is present.
17022
+ // event.email.headers["auto-submitted"] is anything other than "no",
17023
+ // or when a List-Unsubscribe / Precedence: bulk header is present.
16827
17024
  // - Track Message-ID / In-Reply-To chains to break ping-pong loops
16828
17025
  // between two cooperating handlers on different domains.
16829
17026
  export function isLoop(event: EmailReceivedEvent): boolean {
16830
- // event.email.headers.from is the raw RFC 2822 header value, so it
16831
- // may be a bare address ("alice@example.com") or a display-name form
16832
- // ("Alice <alice@example.com>"). Lowercase substring checks match
16833
- // both shapes without needing to parse the bracketed address.
16834
- const from = event.email.headers.from?.toLowerCase() ?? "";
16835
- if (!from) return false;
16836
- if (from.includes(".primitive.email")) return true;
16837
- if (from.includes(REPLY_FROM.toLowerCase())) return true;
17027
+ const fromAddresses = [
17028
+ ...extractEmailAddresses(event.email.headers.from),
17029
+ ...extractEmailAddresses(event.email.smtp.mail_from),
17030
+ ];
17031
+ if (fromAddresses.length === 0) return false;
17032
+
17033
+ const inboundAddresses = new Set(inboundRecipientAddresses(event));
17034
+ const inboundDomains = inboundRecipientDomains(event);
17035
+ const extraSelfAddresses = new Set(
17036
+ EXTRA_SELF_ADDRESSES.map((address) => address.toLowerCase()),
17037
+ );
17038
+
17039
+ for (const from of fromAddresses) {
17040
+ if (inboundAddresses.has(from)) return true;
17041
+ if (extraSelfAddresses.has(from)) return true;
17042
+
17043
+ const fromDomain = domainPart(from);
17044
+ const fromLocal = localPart(from);
17045
+ const sameInboundDomain = fromDomain ? inboundDomains.has(fromDomain) : false;
17046
+ if (
17047
+ sameInboundDomain &&
17048
+ (fromLocal === "mailer-daemon" || fromLocal === "postmaster")
17049
+ ) {
17050
+ return true;
17051
+ }
17052
+ }
17053
+
16838
17054
  return false;
16839
17055
  }
16840
17056
 
@@ -16882,11 +17098,16 @@ export default {
16882
17098
  apiBaseUrl1: env.PRIMITIVE_API_BASE_URL,
16883
17099
  });
16884
17100
 
17101
+ // To add an LLM or another API, store its key as a Function secret.
17102
+ // Example:
17103
+ // export OPENAI_KEY=sk-...
17104
+ // primitive functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy
17105
+
16885
17106
  // Recipient gate
16886
17107
  // https://www.primitive.dev/docs/sending#who-you-can-send-to
16887
17108
  // Even via client.reply, sends to the original sender are
16888
17109
  // subject to the recipient gate. New accounts can send to
16889
- // *.primitive.email addresses, verified domains, addresses that
17110
+ // Primitive-managed domains, verified domains, addresses that
16890
17111
  // have authenticated to you, and other org-member signup emails.
16891
17112
  // Sends to arbitrary external addresses return 403
16892
17113
  // recipient_not_allowed with a structured gates[] array until
@@ -16936,8 +17157,10 @@ function renderPackageJson(name) {
16936
17157
  type: "module",
16937
17158
  scripts: {
16938
17159
  build: "node build.mjs",
16939
- deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js`,
16940
- redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
17160
+ deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js --wait`,
17161
+ "test:function": "primitive functions test --id $PRIMITIVE_FUNCTION_ID --wait --show-sends",
17162
+ logs: "primitive functions logs --id $PRIMITIVE_FUNCTION_ID",
17163
+ redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js --wait"
16941
17164
  },
16942
17165
  dependencies: { "@primitivedotdev/sdk": SDK_VERSION_RANGE },
16943
17166
  devDependencies: {
@@ -17008,13 +17231,47 @@ npm run build
17008
17231
  npm run deploy
17009
17232
  \`\`\`
17010
17233
 
17011
- The deploy step calls \`primitive functions deploy\` (provided by the
17012
- \`@primitivedotdev/cli\` package; install with
17234
+ The deploy step calls \`primitive functions deploy --wait\` (provided
17235
+ by the \`@primitivedotdev/cli\` package; install with
17013
17236
  \`npm install -g @primitivedotdev/cli\` or run via
17014
17237
  \`npx @primitivedotdev/cli@latest <command>\`). It requires
17015
17238
  \`PRIMITIVE_API_KEY\` to be set in your shell (or pass \`--api-key\`).
17016
17239
  Run \`primitive signin\` once to save a key in your CLI config if you
17017
17240
  prefer that to an env var.
17241
+
17242
+ After the first deploy, copy the returned function id into your shell:
17243
+
17244
+ \`\`\`
17245
+ export PRIMITIVE_FUNCTION_ID=<fn-id>
17246
+ \`\`\`
17247
+
17248
+ ## Prove it works
17249
+
17250
+ \`\`\`
17251
+ primitive inbox status
17252
+ npm run test:function
17253
+ npm run logs
17254
+ \`\`\`
17255
+
17256
+ \`npm run test:function\` sends a real test email through MX, waits for
17257
+ the Function to process it, and prints any outbound replies emitted by
17258
+ the handler.
17259
+
17260
+ ## Redeploy
17261
+
17262
+ \`\`\`
17263
+ npm run redeploy
17264
+ \`\`\`
17265
+
17266
+ ## Secrets
17267
+
17268
+ Use secrets for API keys used by your handler. \`--redeploy\` makes the
17269
+ new value visible to the running Function immediately.
17270
+
17271
+ \`\`\`
17272
+ export OPENAI_KEY=sk-...
17273
+ primitive functions set-secret --id "$PRIMITIVE_FUNCTION_ID" --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy
17274
+ \`\`\`
17018
17275
  `;
17019
17276
  }
17020
17277
  function renderEmailReplyTemplateFiles(name) {
@@ -17189,8 +17446,9 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
17189
17446
  this.log("Next:");
17190
17447
  this.log(` cd ${outDir}`);
17191
17448
  this.log(" npm install");
17192
- this.log(" npm run build");
17193
- this.log(` primitive functions deploy --name ${args.name} --file ./dist/handler.js`);
17449
+ this.log(" npm run deploy");
17450
+ this.log(" export PRIMITIVE_FUNCTION_ID=<id-from-deploy-output>");
17451
+ this.log(" npm run test:function");
17194
17452
  }
17195
17453
  };
17196
17454
  //#endregion
@@ -17820,26 +18078,32 @@ var FunctionsTemplatesCommand = class FunctionsTemplatesCommand extends Command
17820
18078
  //#endregion
17821
18079
  //#region src/oclif/commands/functions-test-function.ts
17822
18080
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
17823
- const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
18081
+ const TERMINAL_TEST_TRACE_STATES = new Set([
18082
+ "completed",
18083
+ "failed",
18084
+ "send_failed"
18085
+ ]);
17824
18086
  function buildFunctionTestOutcome(params) {
18087
+ const inbound = params.trace.inbound_email;
17825
18088
  const outcome = {
17826
18089
  elapsed_seconds: params.elapsedSeconds,
17827
18090
  function_id: params.functionId,
17828
18091
  inbound_domain: params.invocation.inbound_domain,
17829
- inbound_id: params.inboundId,
18092
+ inbound_id: inbound?.id ?? null,
17830
18093
  inbound_to: params.invocation.to,
17831
18094
  poll_since: params.invocation.poll_since,
18095
+ state: params.trace.state,
17832
18096
  test_run_id: params.invocation.test_run_id,
17833
18097
  test_send_id: params.invocation.send_id,
17834
18098
  test_subject: params.invocation.subject,
17835
18099
  trace_url: params.invocation.trace_url,
17836
18100
  watch_url: params.invocation.watch_url,
17837
- webhook_attempt_count: params.detail.webhook_attempt_count,
17838
- webhook_last_error: params.detail.webhook_last_error,
17839
- webhook_last_status_code: params.detail.webhook_last_status_code,
17840
- webhook_status: params.detail.webhook_status
18101
+ webhook_attempt_count: inbound?.webhook_attempt_count ?? null,
18102
+ webhook_last_error: inbound?.webhook_last_error ?? null,
18103
+ webhook_last_status_code: inbound?.webhook_last_status_code ?? null,
18104
+ webhook_status: inbound?.webhook_status ?? null
17841
18105
  };
17842
- if (params.showSends) outcome.sent_emails = params.detail.replies;
18106
+ if (params.showSends) outcome.sent_emails = params.trace.replies;
17843
18107
  return outcome;
17844
18108
  }
17845
18109
  function writeFunctionTestProgress(message, writeStderr = (chunk) => {
@@ -17938,7 +18202,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
17938
18202
  required: true
17939
18203
  }),
17940
18204
  "local-part": Flags.string({ description: "Override the synthetic local-part the test inbound is addressed to. Otherwise the runtime picks `__primitive_function_test+<random>`." }),
17941
- wait: Flags.boolean({ description: "Block until the function has processed the test inbound (webhook status is `fired` or `exhausted`) or --timeout elapses. Exits non-zero on timeout or on exhausted retries." }),
18205
+ wait: Flags.boolean({ description: "Block until the function test run reaches `completed`, `failed`, or `send_failed`, or --timeout elapses. Exits non-zero on timeout or terminal failure." }),
17942
18206
  "show-sends": Flags.boolean({ description: "When the wait resolves, also print the outbound emails the function emitted while processing the test inbound (id, status, to, subject). Implies --wait." }),
17943
18207
  timeout: Flags.integer({
17944
18208
  default: DEFAULT_WAIT_TIMEOUT_SECONDS,
@@ -17998,41 +18262,15 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
17998
18262
  const timeoutMs = flags.timeout * 1e3;
17999
18263
  const pollIntervalMs = flags["poll-interval"] * 1e3;
18000
18264
  const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
18001
- writeFunctionTestProgress(`Waiting for test inbound to arrive at ${invocation.to}...`);
18002
- let inboundId;
18003
- while (!isExpired()) {
18004
- const page = await fetchEmailSearchPage({
18005
- apiClient,
18006
- filters: { to: invocation.to },
18007
- pageSize: 25,
18008
- since: invocation.poll_since
18009
- });
18010
- if (!page.ok) {
18011
- const payload = extractErrorPayload(page.error);
18012
- writeErrorWithHints(payload);
18013
- surfaceUnauthorizedHint({
18014
- auth,
18015
- baseUrlOverridden,
18016
- configDir: this.config.configDir,
18017
- payload
18018
- });
18019
- process.exitCode = 1;
18020
- return;
18021
- }
18022
- const found = page.rows[0];
18023
- if (found) {
18024
- inboundId = found.id;
18025
- break;
18026
- }
18027
- await sleep$1(pollIntervalMs);
18028
- }
18029
- if (!inboundId) this.error(`Timed out after ${flags.timeout}s waiting for test inbound ${invocation.to} to land. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
18030
- writeFunctionTestProgress(`Inbound landed (${inboundId}). Waiting for function to run...`);
18031
- let detail;
18265
+ writeFunctionTestProgress(`Waiting for test run ${invocation.test_run_id} to complete for ${invocation.to}...`);
18266
+ let trace;
18032
18267
  while (!isExpired()) {
18033
- const result = await getEmail({
18268
+ const result = await getFunctionTestRunTrace({
18034
18269
  client: apiClient.client,
18035
- path: { id: inboundId },
18270
+ path: {
18271
+ id: flags.id,
18272
+ run_id: invocation.test_run_id
18273
+ },
18036
18274
  responseStyle: "fields"
18037
18275
  });
18038
18276
  if (result.error) {
@@ -18048,24 +18286,22 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18048
18286
  return;
18049
18287
  }
18050
18288
  const fetched = result.data.data;
18051
- if (fetched.webhook_status && TERMINAL_WEBHOOK_STATUSES.has(fetched.webhook_status)) {
18052
- detail = fetched;
18289
+ if (TERMINAL_TEST_TRACE_STATES.has(fetched.state)) {
18290
+ trace = fetched;
18053
18291
  break;
18054
18292
  }
18055
18293
  await sleep$1(pollIntervalMs);
18056
18294
  }
18057
- if (!detail) this.error(`Timed out after ${flags.timeout}s waiting for function webhook to fire for inbound ${inboundId}. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
18058
- const elapsedSeconds = Math.round((Date.now() - startedAt) / 1e3);
18295
+ if (!trace) this.error(`Timed out after ${flags.timeout}s waiting for function test run ${invocation.test_run_id} to complete. Browse ${invocation.watch_url} for the live view, or inspect ${invocation.trace_url}.`, { exit: 2 });
18059
18296
  const outcome = buildFunctionTestOutcome({
18060
- detail,
18061
- elapsedSeconds,
18297
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1e3),
18062
18298
  functionId: flags.id,
18063
- inboundId,
18064
18299
  invocation,
18065
- showSends: shouldShowSends
18300
+ showSends: shouldShowSends,
18301
+ trace
18066
18302
  });
18067
18303
  this.log(JSON.stringify(outcome, null, 2));
18068
- if (detail.webhook_status === "exhausted") process.exitCode = 1;
18304
+ if (trace.state === "failed" || trace.state === "send_failed") process.exitCode = 1;
18069
18305
  });
18070
18306
  }
18071
18307
  };
@@ -18316,7 +18552,7 @@ async function checkExistingLogin(params) {
18316
18552
  message: code === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI OAuth credentials were rejected by an API URL different from the one they were saved with. Run `primitive logout` to remove them, or switch back to the original environment before logging in again." : "A saved Primitive CLI OAuth session exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
18317
18553
  };
18318
18554
  }
18319
- var LoginCommand = class extends Command {
18555
+ var LoginCommand$1 = class extends Command {
18320
18556
  static description = "Log in by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
18321
18557
  static summary = "Log in with browser approval";
18322
18558
  static examples = [
@@ -18340,6 +18576,9 @@ var LoginCommand = class extends Command {
18340
18576
  async run() {
18341
18577
  const commandClass = this.constructor;
18342
18578
  const { flags } = await this.parse(commandClass);
18579
+ await this.runBrowserLogin(flags, this.retryCommand());
18580
+ }
18581
+ async runBrowserLogin(flags, retryCommand = this.retryCommand()) {
18343
18582
  let releaseCredentialsLock;
18344
18583
  try {
18345
18584
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -18347,7 +18586,7 @@ var LoginCommand = class extends Command {
18347
18586
  throw cliError$3(error instanceof Error ? error.message : String(error));
18348
18587
  }
18349
18588
  try {
18350
- await this.runWithCredentialLock(flags, this.retryCommand());
18589
+ await this.runWithCredentialLock(flags, retryCommand);
18351
18590
  } finally {
18352
18591
  releaseCredentialsLock();
18353
18592
  }
@@ -18455,520 +18694,85 @@ var LoginCommand = class extends Command {
18455
18694
  }
18456
18695
  };
18457
18696
  //#endregion
18458
- //#region src/oclif/commands/logout.ts
18697
+ //#region src/oclif/commands/signup.ts
18698
+ const INVALID_VERIFICATION_CODE = "invalid_verification_code";
18699
+ const EXPIRED_TOKEN = "expired_token";
18700
+ const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
18701
+ const SLOW_DOWN = "slow_down";
18702
+ const PENDING_SIGNUP_FILE = "signup.json";
18703
+ const DEFAULT_SIGNUP_COMMAND_COPY = {
18704
+ actionNoun: "signup",
18705
+ actionGerund: "creating a new account",
18706
+ confirmCommand: (email) => `signup confirm ${email} <code>`,
18707
+ resendCommand: (email) => `signup resend ${email}`,
18708
+ startCommand: (email) => `signup ${email}`
18709
+ };
18459
18710
  function cliError$2(message) {
18460
18711
  return new Errors.CLIError(message, { exit: 1 });
18461
18712
  }
18462
18713
  function unwrapData$1(value) {
18463
18714
  return value?.data ?? null;
18464
18715
  }
18465
- function isSavedOAuthSessionExpiredError(error) {
18466
- return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
18716
+ function isRecord(value) {
18717
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18467
18718
  }
18468
- async function runLogoutWithCredentialLock(params) {
18469
- const deps = {
18470
- cliLogout,
18471
- createAuthenticatedCliApiClient,
18472
- ...params.deps
18719
+ function normalizeEmail(email) {
18720
+ return email.trim().toLowerCase();
18721
+ }
18722
+ function pendingSignupFromJson(value) {
18723
+ if (!isRecord(value)) return null;
18724
+ if (typeof value.signup_token !== "string" || typeof value.email !== "string" || typeof value.expires_in !== "number" || typeof value.resend_after !== "number" || typeof value.verification_code_length !== "number" || typeof value.api_base_url_1 !== "string" || typeof value.created_at !== "string" || typeof value.expires_at !== "string") return null;
18725
+ return {
18726
+ api_base_url_1: value.api_base_url_1,
18727
+ created_at: value.created_at,
18728
+ email: value.email,
18729
+ expires_at: value.expires_at,
18730
+ expires_in: value.expires_in,
18731
+ resend_after: value.resend_after,
18732
+ signup_token: value.signup_token,
18733
+ verification_code_length: value.verification_code_length
18473
18734
  };
18474
- let credentials;
18735
+ }
18736
+ function pendingSignupPath(configDir) {
18737
+ return join(configDir, PENDING_SIGNUP_FILE);
18738
+ }
18739
+ function deletePendingAgentSignup(configDir) {
18740
+ rmSync(pendingSignupPath(configDir), { force: true });
18741
+ }
18742
+ function pendingSignupFromStart(start, apiBaseUrl1) {
18743
+ return {
18744
+ ...start,
18745
+ api_base_url_1: apiBaseUrl1,
18746
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
18747
+ expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
18748
+ };
18749
+ }
18750
+ function savePendingAgentSignup(configDir, start, apiBaseUrl1) {
18751
+ mkdirSync(configDir, {
18752
+ mode: 448,
18753
+ recursive: true
18754
+ });
18755
+ const pending = pendingSignupFromStart(start, apiBaseUrl1);
18756
+ const path = pendingSignupPath(configDir);
18757
+ const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
18475
18758
  try {
18476
- credentials = loadCliCredentials(params.configDir);
18759
+ writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
18760
+ chmodSync(tempPath, 384);
18761
+ renameSync(tempPath, path);
18762
+ chmodSync(path, 384);
18763
+ return pending;
18477
18764
  } catch (error) {
18478
- deleteCliCredentials(params.configDir);
18479
- const detail = error instanceof Error ? error.message : String(error);
18480
- process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
18481
- process.exitCode = 1;
18482
- return;
18765
+ rmSync(tempPath, { force: true });
18766
+ throw error;
18483
18767
  }
18484
- if (!credentials) throw cliError$2("Not logged in. Run `primitive signin` to create saved CLI credentials.");
18485
- let authenticated;
18768
+ }
18769
+ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18770
+ const path = pendingSignupPath(configDir);
18771
+ let contents;
18486
18772
  try {
18487
- authenticated = await deps.createAuthenticatedCliApiClient({
18488
- apiBaseUrl1: params.flags["api-base-url-1"],
18489
- configDir: params.configDir,
18490
- credentialsLockHeld: true
18491
- });
18773
+ contents = readFileSync(path, "utf8");
18492
18774
  } catch (error) {
18493
- if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
18494
- process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
18495
- return;
18496
- }
18497
- throw error;
18498
- }
18499
- const freshCredentials = authenticated.auth.credentials ?? credentials;
18500
- const result = await deps.cliLogout({
18501
- body: { key_id: freshCredentials.oauth_grant_id },
18502
- client: authenticated.apiClient.client,
18503
- responseStyle: "fields"
18504
- });
18505
- if (result.error) {
18506
- const payload = extractErrorPayload(result.error);
18507
- const code = extractErrorCode(payload);
18508
- if (code === API_ERROR_CODES.unauthorized || code === API_ERROR_CODES.notFound) {
18509
- deleteCliCredentials(params.configDir);
18510
- writeErrorWithHints(payload);
18511
- process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
18512
- process.exitCode = 1;
18513
- return;
18514
- }
18515
- writeErrorWithHints(payload);
18516
- throw cliError$2("Could not revoke the saved Primitive CLI OAuth grant.");
18517
- }
18518
- const logout = unwrapData$1(result.data);
18519
- deleteCliCredentials(params.configDir);
18520
- const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
18521
- process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
18522
- }
18523
- var LogoutCommand = class LogoutCommand extends Command {
18524
- static description = "Log out by revoking the saved Primitive CLI OAuth grant and deleting local credentials.";
18525
- static summary = "Log out and revoke the saved CLI OAuth grant";
18526
- static examples = ["<%= config.bin %> logout"];
18527
- static flags = { "api-base-url-1": Flags.string({
18528
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18529
- env: "PRIMITIVE_API_BASE_URL_1",
18530
- hidden: true
18531
- }) };
18532
- async run() {
18533
- const { flags } = await this.parse(LogoutCommand);
18534
- let releaseCredentialsLock;
18535
- try {
18536
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
18537
- } catch (error) {
18538
- throw cliError$2(error instanceof Error ? error.message : String(error));
18539
- }
18540
- try {
18541
- await runLogoutWithCredentialLock({
18542
- configDir: this.config.configDir,
18543
- flags
18544
- });
18545
- } finally {
18546
- releaseCredentialsLock();
18547
- }
18548
- }
18549
- };
18550
- //#endregion
18551
- //#region src/oclif/message-body-sources.ts
18552
- function defaultReadFile(path) {
18553
- return readFileSync(path, "utf8");
18554
- }
18555
- function defaultReadStdin() {
18556
- if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
18557
- return readFileSync(0, "utf8");
18558
- }
18559
- function selectedSources(sources) {
18560
- return sources.filter(([, selected]) => selected).map(([label]) => label);
18561
- }
18562
- function readTextFile(path, label, readFile) {
18563
- try {
18564
- return {
18565
- content: readFile(path),
18566
- kind: "ok"
18567
- };
18568
- } catch (error) {
18569
- return {
18570
- kind: "error",
18571
- message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
18572
- };
18573
- }
18574
- }
18575
- function readTextStdin(label, readStdin) {
18576
- try {
18577
- return {
18578
- content: readStdin(),
18579
- kind: "ok"
18580
- };
18581
- } catch (error) {
18582
- return {
18583
- kind: "error",
18584
- message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
18585
- };
18586
- }
18587
- }
18588
- function resolveMessageBodies(input) {
18589
- const bodySources = selectedSources([
18590
- ["--body", input.body !== void 0],
18591
- ["--body-file", input.bodyFile !== void 0],
18592
- ["--body-stdin", input.bodyStdin === true]
18593
- ]);
18594
- if (bodySources.length > 1) return {
18595
- kind: "error",
18596
- message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
18597
- };
18598
- const htmlSources = selectedSources([
18599
- ["--html", input.html !== void 0],
18600
- ["--html-file", input.htmlFile !== void 0],
18601
- ["--html-stdin", input.htmlStdin === true]
18602
- ]);
18603
- if (htmlSources.length > 1) return {
18604
- kind: "error",
18605
- message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
18606
- };
18607
- const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
18608
- if (stdinSources.length > 1) return {
18609
- kind: "error",
18610
- message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
18611
- };
18612
- if (bodySources.length === 0 && htmlSources.length === 0) return {
18613
- kind: "error",
18614
- message: "Either a plain-text body source or an HTML body source is required."
18615
- };
18616
- const readFile = input.readFile ?? defaultReadFile;
18617
- const readStdin = input.readStdin ?? defaultReadStdin;
18618
- let body = input.body;
18619
- let html = input.html;
18620
- if (input.bodyFile !== void 0) {
18621
- const result = readTextFile(input.bodyFile, "--body-file", readFile);
18622
- if (result.kind === "error") return result;
18623
- body = result.content;
18624
- }
18625
- if (input.bodyStdin === true) {
18626
- const result = readTextStdin("--body-stdin", readStdin);
18627
- if (result.kind === "error") return result;
18628
- body = result.content;
18629
- }
18630
- if (input.htmlFile !== void 0) {
18631
- const result = readTextFile(input.htmlFile, "--html-file", readFile);
18632
- if (result.kind === "error") return result;
18633
- html = result.content;
18634
- }
18635
- if (input.htmlStdin === true) {
18636
- const result = readTextStdin("--html-stdin", readStdin);
18637
- if (result.kind === "error") return result;
18638
- html = result.content;
18639
- }
18640
- if (!body && !html) return {
18641
- kind: "error",
18642
- message: "Either a non-empty plain-text body or a non-empty HTML body is required."
18643
- };
18644
- return {
18645
- ...body !== void 0 ? { body } : {},
18646
- ...html !== void 0 ? { html } : {},
18647
- kind: "ok"
18648
- };
18649
- }
18650
- //#endregion
18651
- //#region src/oclif/commands/reply.ts
18652
- var ReplyCommand = class ReplyCommand extends Command {
18653
- static description = `Reply to an inbound email.
18654
-
18655
- The API derives recipients, the Re: subject, and threading headers from the inbound email id. Use \`primitive send --in-reply-to <message-id>\` only when you need to thread against a raw Message-Id instead of an inbound email stored by Primitive.`;
18656
- static summary = "Reply to an inbound email";
18657
- static examples = [
18658
- "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
18659
- "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
18660
- "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
18661
- "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
18662
- ];
18663
- static flags = {
18664
- "api-key": Flags.string({
18665
- description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18666
- env: "PRIMITIVE_API_KEY"
18667
- }),
18668
- "api-base-url-1": Flags.string({
18669
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18670
- env: "PRIMITIVE_API_BASE_URL_1",
18671
- hidden: true
18672
- }),
18673
- "api-base-url-2": Flags.string({
18674
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18675
- env: "PRIMITIVE_API_BASE_URL_2",
18676
- hidden: true
18677
- }),
18678
- id: Flags.string({
18679
- description: "Inbound email id to reply to.",
18680
- required: true
18681
- }),
18682
- body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
18683
- "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
18684
- "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
18685
- html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
18686
- "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
18687
- "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
18688
- from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
18689
- wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
18690
- "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
18691
- time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18692
- };
18693
- async run() {
18694
- const { flags } = await this.parse(ReplyCommand);
18695
- const bodies = resolveMessageBodies({
18696
- body: flags.body,
18697
- bodyFile: flags["body-file"],
18698
- bodyStdin: flags["body-stdin"],
18699
- html: flags.html,
18700
- htmlFile: flags["html-file"],
18701
- htmlStdin: flags["html-stdin"]
18702
- });
18703
- if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
18704
- await runWithTiming(flags.time, async () => {
18705
- const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18706
- apiKey: flags["api-key"],
18707
- apiBaseUrl1: flags["api-base-url-1"],
18708
- apiBaseUrl2: flags["api-base-url-2"],
18709
- configDir: this.config.configDir
18710
- });
18711
- const result = await replyToEmail({
18712
- body: {
18713
- ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
18714
- ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
18715
- ...flags.from !== void 0 ? { from: flags.from } : {},
18716
- ...flags.wait !== void 0 ? { wait: flags.wait } : {},
18717
- ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
18718
- },
18719
- client: apiClient.client,
18720
- path: { id: flags.id },
18721
- responseStyle: "fields"
18722
- });
18723
- if (result.error) {
18724
- const errorPayload = extractErrorPayload(result.error);
18725
- writeErrorWithHints(errorPayload);
18726
- surfaceUnauthorizedHint({
18727
- auth,
18728
- baseUrlOverridden,
18729
- configDir: this.config.configDir,
18730
- payload: errorPayload
18731
- });
18732
- process.exitCode = 1;
18733
- return;
18734
- }
18735
- const envelope = result.data;
18736
- writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
18737
- process.stderr.write(chunk);
18738
- } });
18739
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
18740
- });
18741
- }
18742
- };
18743
- //#endregion
18744
- //#region src/oclif/attachments.ts
18745
- function readAttachmentBytes(path, readFile) {
18746
- try {
18747
- return Buffer.from(readFile(path));
18748
- } catch (error) {
18749
- const detail = error instanceof Error ? error.message : String(error);
18750
- throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
18751
- }
18752
- }
18753
- function hasControlCharacter(value) {
18754
- return Array.from(value).some((character) => {
18755
- const code = character.charCodeAt(0);
18756
- return code <= 31 || code >= 127 && code <= 159;
18757
- });
18758
- }
18759
- function validateAttachmentFilename(path, filename) {
18760
- if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
18761
- if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
18762
- }
18763
- function readAttachmentFiles(paths, readFile = readFileSync) {
18764
- if (!paths || paths.length === 0) return void 0;
18765
- return paths.map((path) => {
18766
- const filename = basename(path);
18767
- validateAttachmentFilename(path, filename);
18768
- const bytes = readAttachmentBytes(path, readFile);
18769
- if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
18770
- return {
18771
- content_base64: bytes.toString("base64"),
18772
- filename
18773
- };
18774
- });
18775
- }
18776
- //#endregion
18777
- //#region src/oclif/commands/send.ts
18778
- var SendCommand = class SendCommand extends Command {
18779
- static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
18780
-
18781
- --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
18782
- --subject defaults to the first line of the body when omitted.
18783
- --attachment attaches a file; repeat it to attach multiple files.
18784
-
18785
- For the full flag set (custom message-id threading on the wire,
18786
- references arrays, etc.), use \`primitive sending send\`.`;
18787
- static summary = "Send an email (simplified, agent-friendly)";
18788
- static examples = [
18789
- "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
18790
- "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
18791
- "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
18792
- "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
18793
- "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
18794
- "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
18795
- "<%= config.bin %> send --to inbox@your-managed-domain.primitive.email --body 'self-loop smoke test' --wait # any *.primitive.email address routes back to the sending account; useful for proving outbound + inbound work end-to-end"
18796
- ];
18797
- static flags = {
18798
- "api-key": Flags.string({
18799
- description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18800
- env: "PRIMITIVE_API_KEY"
18801
- }),
18802
- "api-base-url-1": Flags.string({
18803
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18804
- env: "PRIMITIVE_API_BASE_URL_1",
18805
- hidden: true
18806
- }),
18807
- "api-base-url-2": Flags.string({
18808
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18809
- env: "PRIMITIVE_API_BASE_URL_2",
18810
- hidden: true
18811
- }),
18812
- to: Flags.string({
18813
- description: "Recipient address (e.g. alice@example.com).",
18814
- required: true
18815
- }),
18816
- from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
18817
- subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
18818
- body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
18819
- "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. This does not attach the file; use --attachment for file attachments. Mutually exclusive with --body and --body-stdin." }),
18820
- "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
18821
- html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
18822
- "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
18823
- "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
18824
- attachment: Flags.string({
18825
- description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
18826
- multiple: true
18827
- }),
18828
- "in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
18829
- wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
18830
- "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
18831
- time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18832
- };
18833
- async run() {
18834
- const { flags } = await this.parse(SendCommand);
18835
- const bodies = resolveMessageBodies({
18836
- body: flags.body,
18837
- bodyFile: flags["body-file"],
18838
- bodyStdin: flags["body-stdin"],
18839
- html: flags.html,
18840
- htmlFile: flags["html-file"],
18841
- htmlStdin: flags["html-stdin"]
18842
- });
18843
- if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
18844
- const attachments = readAttachmentFiles(flags.attachment);
18845
- await runWithTiming(flags.time, async () => {
18846
- const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18847
- apiKey: flags["api-key"],
18848
- apiBaseUrl1: flags["api-base-url-1"],
18849
- apiBaseUrl2: flags["api-base-url-2"],
18850
- configDir: this.config.configDir
18851
- });
18852
- const authFailureContext = {
18853
- auth,
18854
- baseUrlOverridden,
18855
- configDir: this.config.configDir
18856
- };
18857
- const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
18858
- const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
18859
- const result = await sendEmail({
18860
- body: {
18861
- from,
18862
- to: flags.to,
18863
- subject,
18864
- ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
18865
- ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
18866
- ...attachments !== void 0 ? { attachments } : {},
18867
- ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
18868
- ...flags.wait !== void 0 ? { wait: flags.wait } : {},
18869
- ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
18870
- },
18871
- client: apiClient._sendClient,
18872
- responseStyle: "fields"
18873
- });
18874
- if (result.error) {
18875
- const errorPayload = extractErrorPayload(result.error);
18876
- writeErrorWithHints(errorPayload);
18877
- surfaceUnauthorizedHint({
18878
- ...authFailureContext,
18879
- payload: errorPayload
18880
- });
18881
- process.exitCode = 1;
18882
- return;
18883
- }
18884
- const envelope = result.data;
18885
- writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
18886
- process.stderr.write(chunk);
18887
- } });
18888
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
18889
- });
18890
- }
18891
- };
18892
- //#endregion
18893
- //#region src/oclif/commands/signup.ts
18894
- const INVALID_VERIFICATION_CODE = "invalid_verification_code";
18895
- const EXPIRED_TOKEN = "expired_token";
18896
- const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
18897
- const SLOW_DOWN = "slow_down";
18898
- const PENDING_SIGNUP_FILE = "signup.json";
18899
- const DEFAULT_SIGNUP_COMMAND_COPY = {
18900
- actionNoun: "signup",
18901
- actionGerund: "creating a new account",
18902
- confirmCommand: (email) => `signup confirm ${email} <code>`,
18903
- resendCommand: (email) => `signup resend ${email}`,
18904
- startCommand: (email) => `signup ${email}`
18905
- };
18906
- function cliError$1(message) {
18907
- return new Errors.CLIError(message, { exit: 1 });
18908
- }
18909
- function unwrapData(value) {
18910
- return value?.data ?? null;
18911
- }
18912
- function isRecord(value) {
18913
- return value !== null && typeof value === "object" && !Array.isArray(value);
18914
- }
18915
- function normalizeEmail(email) {
18916
- return email.trim().toLowerCase();
18917
- }
18918
- function pendingSignupFromJson(value) {
18919
- if (!isRecord(value)) return null;
18920
- if (typeof value.signup_token !== "string" || typeof value.email !== "string" || typeof value.expires_in !== "number" || typeof value.resend_after !== "number" || typeof value.verification_code_length !== "number" || typeof value.api_base_url_1 !== "string" || typeof value.created_at !== "string" || typeof value.expires_at !== "string") return null;
18921
- return {
18922
- api_base_url_1: value.api_base_url_1,
18923
- created_at: value.created_at,
18924
- email: value.email,
18925
- expires_at: value.expires_at,
18926
- expires_in: value.expires_in,
18927
- resend_after: value.resend_after,
18928
- signup_token: value.signup_token,
18929
- verification_code_length: value.verification_code_length
18930
- };
18931
- }
18932
- function pendingSignupPath(configDir) {
18933
- return join(configDir, PENDING_SIGNUP_FILE);
18934
- }
18935
- function deletePendingAgentSignup(configDir) {
18936
- rmSync(pendingSignupPath(configDir), { force: true });
18937
- }
18938
- function pendingSignupFromStart(start, apiBaseUrl1) {
18939
- return {
18940
- ...start,
18941
- api_base_url_1: apiBaseUrl1,
18942
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
18943
- expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
18944
- };
18945
- }
18946
- function savePendingAgentSignup(configDir, start, apiBaseUrl1) {
18947
- mkdirSync(configDir, {
18948
- mode: 448,
18949
- recursive: true
18950
- });
18951
- const pending = pendingSignupFromStart(start, apiBaseUrl1);
18952
- const path = pendingSignupPath(configDir);
18953
- const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
18954
- try {
18955
- writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
18956
- chmodSync(tempPath, 384);
18957
- renameSync(tempPath, path);
18958
- chmodSync(path, 384);
18959
- return pending;
18960
- } catch (error) {
18961
- rmSync(tempPath, { force: true });
18962
- throw error;
18963
- }
18964
- }
18965
- function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18966
- const path = pendingSignupPath(configDir);
18967
- let contents;
18968
- try {
18969
- contents = readFileSync(path, "utf8");
18970
- } catch (error) {
18971
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
18775
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
18972
18776
  throw error;
18973
18777
  }
18974
18778
  let pending;
@@ -18994,8 +18798,8 @@ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18994
18798
  function requirePendingSignupForEmail(params) {
18995
18799
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
18996
18800
  const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
18997
- if (!pending) throw cliError$1(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
18998
- if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$1(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
18801
+ if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
18802
+ if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
18999
18803
  return pending;
19000
18804
  }
19001
18805
  function retryAfterSeconds(result) {
@@ -19036,7 +18840,7 @@ async function confirmTerms() {
19036
18840
  process$1.stderr.write(" https://primitive.dev/terms\n");
19037
18841
  process$1.stderr.write(" https://primitive.dev/privacy\n");
19038
18842
  const answer = (await promptRequired("Type 'yes' to continue: ")).toLowerCase();
19039
- if (answer !== "yes" && answer !== "y") throw cliError$1("You must accept the terms to create an account.");
18843
+ if (answer !== "yes" && answer !== "y") throw cliError$2("You must accept the terms to create an account.");
19040
18844
  }
19041
18845
  async function checkExistingCredentials(params) {
19042
18846
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
@@ -19067,9 +18871,9 @@ async function checkExistingCredentials(params) {
19067
18871
  }
19068
18872
  if (existingStatus.status === "blocked") {
19069
18873
  writeErrorWithHints(existingStatus.payload);
19070
- throw cliError$1(existingStatus.message);
18874
+ throw cliError$2(existingStatus.message);
19071
18875
  }
19072
- throw cliError$1(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
18876
+ throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
19073
18877
  }
19074
18878
  function saveSignupCredentials(params) {
19075
18879
  saveCliCredentials(params.configDir, {
@@ -19103,7 +18907,7 @@ async function startSignup(params) {
19103
18907
  started: false
19104
18908
  };
19105
18909
  }
19106
- throw cliError$1(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
18910
+ throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19107
18911
  }
19108
18912
  if (params.flags.force) deletePendingAgentSignup(params.configDir);
19109
18913
  const promptRequiredFn = params.deps.promptRequired ?? promptRequired;
@@ -19123,10 +18927,10 @@ async function startSignup(params) {
19123
18927
  });
19124
18928
  if (started.error) {
19125
18929
  writeErrorWithHints(extractErrorPayload(started.error));
19126
- throw cliError$1("Could not start Primitive agent signup.");
18930
+ throw cliError$2("Could not start Primitive agent signup.");
19127
18931
  }
19128
- const startResult = unwrapData(started.data);
19129
- if (!startResult) throw cliError$1("Primitive API returned an empty agent signup response.");
18932
+ const startResult = unwrapData$1(started.data);
18933
+ if (!startResult) throw cliError$2("Primitive API returned an empty agent signup response.");
19130
18934
  return {
19131
18935
  pending: savePendingAgentSignup(params.configDir, startResult, params.apiBaseUrl1),
19132
18936
  started: true
@@ -19139,7 +18943,7 @@ async function resendVerificationCode(params) {
19139
18943
  responseStyle: "fields"
19140
18944
  });
19141
18945
  if (resent.data) {
19142
- const resend = unwrapData(resent.data);
18946
+ const resend = unwrapData$1(resent.data);
19143
18947
  const next = resend ? {
19144
18948
  email: resend.email,
19145
18949
  expires_in: resend.expires_in,
@@ -19164,7 +18968,7 @@ async function resendVerificationCode(params) {
19164
18968
  }
19165
18969
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(params.configDir);
19166
18970
  writeErrorWithHints(payload);
19167
- throw cliError$1("Could not resend Primitive agent signup verification email.");
18971
+ throw cliError$2("Could not resend Primitive agent signup verification email.");
19168
18972
  }
19169
18973
  async function runSignupStartWithCredentialLock(params) {
19170
18974
  const { configDir, flags } = params;
@@ -19224,8 +19028,8 @@ async function runSignupConfirmWithCredentialLock(params) {
19224
19028
  responseStyle: "fields"
19225
19029
  });
19226
19030
  if (verified.data) {
19227
- const signup = unwrapData(verified.data);
19228
- if (!signup) throw cliError$1("Primitive API returned an empty agent signup verification response.");
19031
+ const signup = unwrapData$1(verified.data);
19032
+ if (!signup) throw cliError$2("Primitive API returned an empty agent signup verification response.");
19229
19033
  saveSignupCredentials({
19230
19034
  apiBaseUrl1,
19231
19035
  configDir,
@@ -19239,10 +19043,10 @@ async function runSignupConfirmWithCredentialLock(params) {
19239
19043
  }
19240
19044
  const payload = extractErrorPayload(verified.error);
19241
19045
  const code = extractErrorCode(payload);
19242
- if (code === INVALID_VERIFICATION_CODE) throw cliError$1(`Invalid verification code. Try again or run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}.`);
19046
+ if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again or run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}.`);
19243
19047
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(configDir);
19244
19048
  writeErrorWithHints(payload);
19245
- throw cliError$1("Primitive agent signup failed while verifying the account.");
19049
+ throw cliError$2("Primitive agent signup failed while verifying the account.");
19246
19050
  }
19247
19051
  async function runSignupResendWithCredentialLock(params) {
19248
19052
  const deps = params.deps ?? {};
@@ -19329,40 +19133,268 @@ async function runSignupInteractiveWithCredentialLock(params) {
19329
19133
  }
19330
19134
  }
19331
19135
  }
19332
- function commonStartFlags() {
19333
- return {
19334
- "accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
19136
+ function commonStartFlags() {
19137
+ return {
19138
+ "accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
19139
+ "api-base-url-1": Flags.string({
19140
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19141
+ env: "PRIMITIVE_API_BASE_URL_1",
19142
+ hidden: true
19143
+ }),
19144
+ "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19145
+ force: Flags.boolean({
19146
+ char: "f",
19147
+ description: "Replace saved credentials or pending signup state when needed"
19148
+ }),
19149
+ "signup-code": Flags.string({
19150
+ description: "Signup code required to create an account",
19151
+ env: "PRIMITIVE_SIGNUP_CODE"
19152
+ })
19153
+ };
19154
+ }
19155
+ var SignupCommand = class SignupCommand extends Command {
19156
+ static args = { email: Args.string({
19157
+ description: "Email address to sign up",
19158
+ required: false
19159
+ }) };
19160
+ static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
19161
+ static summary = "Start account signup";
19162
+ static examples = [
19163
+ "<%= config.bin %> signup user@example.com",
19164
+ "<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
19165
+ "<%= config.bin %> signup confirm user@example.com 123456"
19166
+ ];
19167
+ static flags = commonStartFlags();
19168
+ async run() {
19169
+ const { args, flags } = await this.parse(SignupCommand);
19170
+ let releaseCredentialsLock;
19171
+ try {
19172
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19173
+ } catch (error) {
19174
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19175
+ }
19176
+ try {
19177
+ await runSignupStartWithCredentialLock({
19178
+ configDir: this.config.configDir,
19179
+ email: args.email,
19180
+ flags
19181
+ });
19182
+ } finally {
19183
+ releaseCredentialsLock();
19184
+ }
19185
+ }
19186
+ };
19187
+ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
19188
+ static args = {
19189
+ email: Args.string({
19190
+ description: "Email address used to start signup",
19191
+ required: true
19192
+ }),
19193
+ code: Args.string({
19194
+ description: "Verification code from the signup email",
19195
+ required: true
19196
+ })
19197
+ };
19198
+ static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
19199
+ static summary = "Confirm account signup";
19200
+ static examples = ["<%= config.bin %> signup confirm user@example.com 123456", "<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
19201
+ static flags = {
19202
+ "api-base-url-1": Flags.string({
19203
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19204
+ env: "PRIMITIVE_API_BASE_URL_1",
19205
+ hidden: true
19206
+ }),
19207
+ force: Flags.boolean({
19208
+ char: "f",
19209
+ description: "Replace saved credentials after verification"
19210
+ }),
19211
+ "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19212
+ };
19213
+ async run() {
19214
+ const { args, flags } = await this.parse(SignupConfirmCommand);
19215
+ let releaseCredentialsLock;
19216
+ try {
19217
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19218
+ } catch (error) {
19219
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19220
+ }
19221
+ try {
19222
+ await runSignupConfirmWithCredentialLock({
19223
+ code: args.code,
19224
+ configDir: this.config.configDir,
19225
+ email: args.email,
19226
+ flags
19227
+ });
19228
+ } finally {
19229
+ releaseCredentialsLock();
19230
+ }
19231
+ }
19232
+ };
19233
+ var SignupResendCommand = class SignupResendCommand extends Command {
19234
+ static args = { email: Args.string({
19235
+ description: "Email address used to start signup",
19236
+ required: true
19237
+ }) };
19238
+ static description = "Resend the verification code for a pending signup.";
19239
+ static summary = "Resend signup verification code";
19240
+ static examples = ["<%= config.bin %> signup resend user@example.com"];
19241
+ static flags = { "api-base-url-1": Flags.string({
19242
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19243
+ env: "PRIMITIVE_API_BASE_URL_1",
19244
+ hidden: true
19245
+ }) };
19246
+ async run() {
19247
+ const { args, flags } = await this.parse(SignupResendCommand);
19248
+ let releaseCredentialsLock;
19249
+ try {
19250
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19251
+ } catch (error) {
19252
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19253
+ }
19254
+ try {
19255
+ await runSignupResendWithCredentialLock({
19256
+ configDir: this.config.configDir,
19257
+ email: args.email,
19258
+ flags
19259
+ });
19260
+ } finally {
19261
+ releaseCredentialsLock();
19262
+ }
19263
+ }
19264
+ };
19265
+ var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
19266
+ static description = "Run the full signup flow in one interactive terminal session.";
19267
+ static summary = "Run interactive account signup";
19268
+ static examples = ["<%= config.bin %> signup interactive"];
19269
+ static flags = commonStartFlags();
19270
+ async run() {
19271
+ const { flags } = await this.parse(SignupInteractiveCommand);
19272
+ let releaseCredentialsLock;
19273
+ try {
19274
+ releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19275
+ } catch (error) {
19276
+ throw cliError$2(error instanceof Error ? error.message : String(error));
19277
+ }
19278
+ try {
19279
+ await runSignupInteractiveWithCredentialLock({
19280
+ configDir: this.config.configDir,
19281
+ flags
19282
+ });
19283
+ } finally {
19284
+ releaseCredentialsLock();
19285
+ }
19286
+ }
19287
+ };
19288
+ //#endregion
19289
+ //#region src/oclif/commands/logout.ts
19290
+ function cliError$1(message) {
19291
+ return new Errors.CLIError(message, { exit: 1 });
19292
+ }
19293
+ function unwrapData(value) {
19294
+ return value?.data ?? null;
19295
+ }
19296
+ function isSavedOAuthSessionExpiredError(error) {
19297
+ return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
19298
+ }
19299
+ async function runLogoutWithCredentialLock(params) {
19300
+ const deps = {
19301
+ cliLogout,
19302
+ createAuthenticatedCliApiClient,
19303
+ ...params.deps
19304
+ };
19305
+ let credentials;
19306
+ try {
19307
+ credentials = loadCliCredentials(params.configDir);
19308
+ } catch (error) {
19309
+ deleteCliCredentials(params.configDir);
19310
+ const detail = error instanceof Error ? error.message : String(error);
19311
+ process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
19312
+ process.exitCode = 1;
19313
+ return;
19314
+ }
19315
+ if (!credentials) throw cliError$1("Not logged in. Run `primitive signin` to create saved CLI credentials.");
19316
+ let authenticated;
19317
+ try {
19318
+ authenticated = await deps.createAuthenticatedCliApiClient({
19319
+ apiBaseUrl1: params.flags["api-base-url-1"],
19320
+ configDir: params.configDir,
19321
+ credentialsLockHeld: true
19322
+ });
19323
+ } catch (error) {
19324
+ if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
19325
+ process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
19326
+ return;
19327
+ }
19328
+ throw error;
19329
+ }
19330
+ const freshCredentials = authenticated.auth.credentials ?? credentials;
19331
+ const result = await deps.cliLogout({
19332
+ body: { key_id: freshCredentials.oauth_grant_id },
19333
+ client: authenticated.apiClient.client,
19334
+ responseStyle: "fields"
19335
+ });
19336
+ if (result.error) {
19337
+ const payload = extractErrorPayload(result.error);
19338
+ const code = extractErrorCode(payload);
19339
+ if (code === API_ERROR_CODES.unauthorized || code === API_ERROR_CODES.notFound) {
19340
+ deleteCliCredentials(params.configDir);
19341
+ writeErrorWithHints(payload);
19342
+ process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
19343
+ process.exitCode = 1;
19344
+ return;
19345
+ }
19346
+ writeErrorWithHints(payload);
19347
+ throw cliError$1("Could not revoke the saved Primitive CLI OAuth grant.");
19348
+ }
19349
+ const logout = unwrapData(result.data);
19350
+ deleteCliCredentials(params.configDir);
19351
+ const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
19352
+ process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
19353
+ }
19354
+ function runForceLogout(params) {
19355
+ const localCredentialsPath = credentialsPath(params.configDir);
19356
+ const pendingPath = pendingSignupPath(params.configDir);
19357
+ const lockPath = credentialsLockPath(params.configDir);
19358
+ const removed = [
19359
+ existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
19360
+ existsSync(pendingPath) ? "pending email-code auth state" : null,
19361
+ existsSync(lockPath) ? "credential lock" : null
19362
+ ].filter((value) => value !== null);
19363
+ deleteCliCredentials(params.configDir);
19364
+ deletePendingAgentSignup(params.configDir);
19365
+ deleteCliCredentialsLock(params.configDir);
19366
+ if (removed.length === 0) {
19367
+ process.stderr.write("No local Primitive CLI auth state was present. Backing OAuth grant was not revoked.\n");
19368
+ return;
19369
+ }
19370
+ process.stderr.write(`Removed ${formatList(removed)}. Backing OAuth grant was not revoked.\n`);
19371
+ }
19372
+ function formatList(values) {
19373
+ if (values.length <= 1) return values[0] ?? "";
19374
+ if (values.length === 2) return `${values[0]} and ${values[1]}`;
19375
+ return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
19376
+ }
19377
+ var LogoutCommand = class LogoutCommand extends Command {
19378
+ static description = "Log out by revoking the saved Primitive CLI OAuth grant and deleting local credentials. Use --force to remove local credentials, pending email-code auth state, and stale credential locks without contacting Primitive.";
19379
+ static summary = "Log out and revoke the saved CLI OAuth grant";
19380
+ static examples = ["<%= config.bin %> logout", "<%= config.bin %> logout --force"];
19381
+ static flags = {
19335
19382
  "api-base-url-1": Flags.string({
19336
19383
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19337
19384
  env: "PRIMITIVE_API_BASE_URL_1",
19338
19385
  hidden: true
19339
19386
  }),
19340
- "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19341
19387
  force: Flags.boolean({
19342
19388
  char: "f",
19343
- description: "Replace saved credentials or pending signup state when needed"
19344
- }),
19345
- "signup-code": Flags.string({
19346
- description: "Signup code required to create an account",
19347
- env: "PRIMITIVE_SIGNUP_CODE"
19389
+ description: "Remove local CLI credentials, pending email-code auth state, and any credential lock without revoking the server OAuth grant"
19348
19390
  })
19349
19391
  };
19350
- }
19351
- var SignupCommand = class SignupCommand extends Command {
19352
- static args = { email: Args.string({
19353
- description: "Email address to sign up",
19354
- required: false
19355
- }) };
19356
- static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
19357
- static summary = "Start account signup";
19358
- static examples = [
19359
- "<%= config.bin %> signup user@example.com",
19360
- "<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
19361
- "<%= config.bin %> signup confirm user@example.com 123456"
19362
- ];
19363
- static flags = commonStartFlags();
19364
19392
  async run() {
19365
- const { args, flags } = await this.parse(SignupCommand);
19393
+ const { flags } = await this.parse(LogoutCommand);
19394
+ if (flags.force) {
19395
+ runForceLogout({ configDir: this.config.configDir });
19396
+ return;
19397
+ }
19366
19398
  let releaseCredentialsLock;
19367
19399
  try {
19368
19400
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -19370,9 +19402,8 @@ var SignupCommand = class SignupCommand extends Command {
19370
19402
  throw cliError$1(error instanceof Error ? error.message : String(error));
19371
19403
  }
19372
19404
  try {
19373
- await runSignupStartWithCredentialLock({
19405
+ await runLogoutWithCredentialLock({
19374
19406
  configDir: this.config.configDir,
19375
- email: args.email,
19376
19407
  flags
19377
19408
  });
19378
19409
  } finally {
@@ -19380,105 +19411,344 @@ var SignupCommand = class SignupCommand extends Command {
19380
19411
  }
19381
19412
  }
19382
19413
  };
19383
- var SignupConfirmCommand = class SignupConfirmCommand extends Command {
19384
- static args = {
19385
- email: Args.string({
19386
- description: "Email address used to start signup",
19387
- required: true
19414
+ //#endregion
19415
+ //#region src/oclif/message-body-sources.ts
19416
+ function defaultReadFile(path) {
19417
+ return readFileSync(path, "utf8");
19418
+ }
19419
+ function defaultReadStdin() {
19420
+ if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
19421
+ return readFileSync(0, "utf8");
19422
+ }
19423
+ function selectedSources(sources) {
19424
+ return sources.filter(([, selected]) => selected).map(([label]) => label);
19425
+ }
19426
+ function readTextFile(path, label, readFile) {
19427
+ try {
19428
+ return {
19429
+ content: readFile(path),
19430
+ kind: "ok"
19431
+ };
19432
+ } catch (error) {
19433
+ return {
19434
+ kind: "error",
19435
+ message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
19436
+ };
19437
+ }
19438
+ }
19439
+ function readTextStdin(label, readStdin) {
19440
+ try {
19441
+ return {
19442
+ content: readStdin(),
19443
+ kind: "ok"
19444
+ };
19445
+ } catch (error) {
19446
+ return {
19447
+ kind: "error",
19448
+ message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
19449
+ };
19450
+ }
19451
+ }
19452
+ function resolveMessageBodies(input) {
19453
+ const bodySources = selectedSources([
19454
+ ["--body", input.body !== void 0],
19455
+ ["--body-file", input.bodyFile !== void 0],
19456
+ ["--body-stdin", input.bodyStdin === true]
19457
+ ]);
19458
+ if (bodySources.length > 1) return {
19459
+ kind: "error",
19460
+ message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
19461
+ };
19462
+ const htmlSources = selectedSources([
19463
+ ["--html", input.html !== void 0],
19464
+ ["--html-file", input.htmlFile !== void 0],
19465
+ ["--html-stdin", input.htmlStdin === true]
19466
+ ]);
19467
+ if (htmlSources.length > 1) return {
19468
+ kind: "error",
19469
+ message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
19470
+ };
19471
+ const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
19472
+ if (stdinSources.length > 1) return {
19473
+ kind: "error",
19474
+ message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
19475
+ };
19476
+ if (bodySources.length === 0 && htmlSources.length === 0) return {
19477
+ kind: "error",
19478
+ message: "Either a plain-text body source or an HTML body source is required."
19479
+ };
19480
+ const readFile = input.readFile ?? defaultReadFile;
19481
+ const readStdin = input.readStdin ?? defaultReadStdin;
19482
+ let body = input.body;
19483
+ let html = input.html;
19484
+ if (input.bodyFile !== void 0) {
19485
+ const result = readTextFile(input.bodyFile, "--body-file", readFile);
19486
+ if (result.kind === "error") return result;
19487
+ body = result.content;
19488
+ }
19489
+ if (input.bodyStdin === true) {
19490
+ const result = readTextStdin("--body-stdin", readStdin);
19491
+ if (result.kind === "error") return result;
19492
+ body = result.content;
19493
+ }
19494
+ if (input.htmlFile !== void 0) {
19495
+ const result = readTextFile(input.htmlFile, "--html-file", readFile);
19496
+ if (result.kind === "error") return result;
19497
+ html = result.content;
19498
+ }
19499
+ if (input.htmlStdin === true) {
19500
+ const result = readTextStdin("--html-stdin", readStdin);
19501
+ if (result.kind === "error") return result;
19502
+ html = result.content;
19503
+ }
19504
+ if (!body && !html) return {
19505
+ kind: "error",
19506
+ message: "Either a non-empty plain-text body or a non-empty HTML body is required."
19507
+ };
19508
+ return {
19509
+ ...body !== void 0 ? { body } : {},
19510
+ ...html !== void 0 ? { html } : {},
19511
+ kind: "ok"
19512
+ };
19513
+ }
19514
+ //#endregion
19515
+ //#region src/oclif/commands/reply.ts
19516
+ var ReplyCommand = class ReplyCommand extends Command {
19517
+ static description = `Reply to an inbound email.
19518
+
19519
+ The API derives recipients, the Re: subject, and threading headers from the inbound email id. Use \`primitive send --in-reply-to <message-id>\` only when you need to thread against a raw Message-Id instead of an inbound email stored by Primitive.`;
19520
+ static summary = "Reply to an inbound email";
19521
+ static examples = [
19522
+ "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
19523
+ "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
19524
+ "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
19525
+ "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
19526
+ ];
19527
+ static flags = {
19528
+ "api-key": Flags.string({
19529
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
19530
+ env: "PRIMITIVE_API_KEY"
19388
19531
  }),
19389
- code: Args.string({
19390
- description: "Verification code from the signup email",
19532
+ "api-base-url-1": Flags.string({
19533
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19534
+ env: "PRIMITIVE_API_BASE_URL_1",
19535
+ hidden: true
19536
+ }),
19537
+ "api-base-url-2": Flags.string({
19538
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
19539
+ env: "PRIMITIVE_API_BASE_URL_2",
19540
+ hidden: true
19541
+ }),
19542
+ id: Flags.string({
19543
+ description: "Inbound email id to reply to.",
19391
19544
  required: true
19392
- })
19545
+ }),
19546
+ body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
19547
+ "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
19548
+ "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
19549
+ html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
19550
+ "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
19551
+ "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
19552
+ from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
19553
+ wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
19554
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
19393
19555
  };
19394
- static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
19395
- static summary = "Confirm account signup";
19396
- static examples = ["<%= config.bin %> signup confirm user@example.com 123456", "<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
19556
+ async run() {
19557
+ const { flags } = await this.parse(ReplyCommand);
19558
+ const bodies = resolveMessageBodies({
19559
+ body: flags.body,
19560
+ bodyFile: flags["body-file"],
19561
+ bodyStdin: flags["body-stdin"],
19562
+ html: flags.html,
19563
+ htmlFile: flags["html-file"],
19564
+ htmlStdin: flags["html-stdin"]
19565
+ });
19566
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
19567
+ await runWithTiming(flags.time, async () => {
19568
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
19569
+ apiKey: flags["api-key"],
19570
+ apiBaseUrl1: flags["api-base-url-1"],
19571
+ apiBaseUrl2: flags["api-base-url-2"],
19572
+ configDir: this.config.configDir
19573
+ });
19574
+ const result = await replyToEmail({
19575
+ body: {
19576
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
19577
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
19578
+ ...flags.from !== void 0 ? { from: flags.from } : {},
19579
+ ...flags.wait !== void 0 ? { wait: flags.wait } : {}
19580
+ },
19581
+ client: apiClient.client,
19582
+ path: { id: flags.id },
19583
+ responseStyle: "fields"
19584
+ });
19585
+ if (result.error) {
19586
+ const errorPayload = extractErrorPayload(result.error);
19587
+ writeErrorWithHints(errorPayload);
19588
+ surfaceUnauthorizedHint({
19589
+ auth,
19590
+ baseUrlOverridden,
19591
+ configDir: this.config.configDir,
19592
+ payload: errorPayload
19593
+ });
19594
+ process.exitCode = 1;
19595
+ return;
19596
+ }
19597
+ const envelope = result.data;
19598
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
19599
+ process.stderr.write(chunk);
19600
+ } });
19601
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
19602
+ });
19603
+ }
19604
+ };
19605
+ //#endregion
19606
+ //#region src/oclif/attachments.ts
19607
+ function readAttachmentBytes(path, readFile) {
19608
+ try {
19609
+ return Buffer.from(readFile(path));
19610
+ } catch (error) {
19611
+ const detail = error instanceof Error ? error.message : String(error);
19612
+ throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
19613
+ }
19614
+ }
19615
+ function hasControlCharacter(value) {
19616
+ return Array.from(value).some((character) => {
19617
+ const code = character.charCodeAt(0);
19618
+ return code <= 31 || code >= 127 && code <= 159;
19619
+ });
19620
+ }
19621
+ function validateAttachmentFilename(path, filename) {
19622
+ if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
19623
+ if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
19624
+ }
19625
+ function readAttachmentFiles(paths, readFile = readFileSync) {
19626
+ if (!paths || paths.length === 0) return void 0;
19627
+ return paths.map((path) => {
19628
+ const filename = basename(path);
19629
+ validateAttachmentFilename(path, filename);
19630
+ const bytes = readAttachmentBytes(path, readFile);
19631
+ if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
19632
+ return {
19633
+ content_base64: bytes.toString("base64"),
19634
+ filename
19635
+ };
19636
+ });
19637
+ }
19638
+ //#endregion
19639
+ //#region src/oclif/commands/send.ts
19640
+ var SendCommand = class SendCommand extends Command {
19641
+ static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
19642
+
19643
+ --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
19644
+ --subject defaults to the first line of the body when omitted.
19645
+ --attachment attaches a file; repeat it to attach multiple files.
19646
+
19647
+ For the full flag set (custom message-id threading on the wire,
19648
+ references arrays, etc.), use \`primitive sending send\`.`;
19649
+ static summary = "Send an email (simplified, agent-friendly)";
19650
+ static examples = [
19651
+ "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
19652
+ "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
19653
+ "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
19654
+ "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
19655
+ "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
19656
+ "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
19657
+ "<%= config.bin %> send --to inbox@your-managed-domain.primitive.email --body 'self-loop smoke test' --wait # any *.primitive.email address routes back to the sending account; useful for proving outbound + inbound work end-to-end"
19658
+ ];
19397
19659
  static flags = {
19660
+ "api-key": Flags.string({
19661
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
19662
+ env: "PRIMITIVE_API_KEY"
19663
+ }),
19398
19664
  "api-base-url-1": Flags.string({
19399
19665
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19400
19666
  env: "PRIMITIVE_API_BASE_URL_1",
19401
19667
  hidden: true
19402
19668
  }),
19403
- force: Flags.boolean({
19404
- char: "f",
19405
- description: "Replace saved credentials after verification"
19669
+ "api-base-url-2": Flags.string({
19670
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
19671
+ env: "PRIMITIVE_API_BASE_URL_2",
19672
+ hidden: true
19406
19673
  }),
19407
- "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19674
+ to: Flags.string({
19675
+ description: "Recipient address (e.g. alice@example.com).",
19676
+ required: true
19677
+ }),
19678
+ from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
19679
+ subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
19680
+ body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
19681
+ "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. This does not attach the file; use --attachment for file attachments. Mutually exclusive with --body and --body-stdin." }),
19682
+ "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
19683
+ html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
19684
+ "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
19685
+ "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
19686
+ attachment: Flags.string({
19687
+ description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
19688
+ multiple: true
19689
+ }),
19690
+ "in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
19691
+ wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
19692
+ "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
19693
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
19408
19694
  };
19409
19695
  async run() {
19410
- const { args, flags } = await this.parse(SignupConfirmCommand);
19411
- let releaseCredentialsLock;
19412
- try {
19413
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19414
- } catch (error) {
19415
- throw cliError$1(error instanceof Error ? error.message : String(error));
19416
- }
19417
- try {
19418
- await runSignupConfirmWithCredentialLock({
19419
- code: args.code,
19420
- configDir: this.config.configDir,
19421
- email: args.email,
19422
- flags
19423
- });
19424
- } finally {
19425
- releaseCredentialsLock();
19426
- }
19427
- }
19428
- };
19429
- var SignupResendCommand = class SignupResendCommand extends Command {
19430
- static args = { email: Args.string({
19431
- description: "Email address used to start signup",
19432
- required: true
19433
- }) };
19434
- static description = "Resend the verification code for a pending signup.";
19435
- static summary = "Resend signup verification code";
19436
- static examples = ["<%= config.bin %> signup resend user@example.com"];
19437
- static flags = { "api-base-url-1": Flags.string({
19438
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19439
- env: "PRIMITIVE_API_BASE_URL_1",
19440
- hidden: true
19441
- }) };
19442
- async run() {
19443
- const { args, flags } = await this.parse(SignupResendCommand);
19444
- let releaseCredentialsLock;
19445
- try {
19446
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19447
- } catch (error) {
19448
- throw cliError$1(error instanceof Error ? error.message : String(error));
19449
- }
19450
- try {
19451
- await runSignupResendWithCredentialLock({
19452
- configDir: this.config.configDir,
19453
- email: args.email,
19454
- flags
19696
+ const { flags } = await this.parse(SendCommand);
19697
+ const bodies = resolveMessageBodies({
19698
+ body: flags.body,
19699
+ bodyFile: flags["body-file"],
19700
+ bodyStdin: flags["body-stdin"],
19701
+ html: flags.html,
19702
+ htmlFile: flags["html-file"],
19703
+ htmlStdin: flags["html-stdin"]
19704
+ });
19705
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
19706
+ const attachments = readAttachmentFiles(flags.attachment);
19707
+ await runWithTiming(flags.time, async () => {
19708
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
19709
+ apiKey: flags["api-key"],
19710
+ apiBaseUrl1: flags["api-base-url-1"],
19711
+ apiBaseUrl2: flags["api-base-url-2"],
19712
+ configDir: this.config.configDir
19455
19713
  });
19456
- } finally {
19457
- releaseCredentialsLock();
19458
- }
19459
- }
19460
- };
19461
- var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
19462
- static description = "Run the full signup flow in one interactive terminal session.";
19463
- static summary = "Run interactive account signup";
19464
- static examples = ["<%= config.bin %> signup interactive"];
19465
- static flags = commonStartFlags();
19466
- async run() {
19467
- const { flags } = await this.parse(SignupInteractiveCommand);
19468
- let releaseCredentialsLock;
19469
- try {
19470
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19471
- } catch (error) {
19472
- throw cliError$1(error instanceof Error ? error.message : String(error));
19473
- }
19474
- try {
19475
- await runSignupInteractiveWithCredentialLock({
19476
- configDir: this.config.configDir,
19477
- flags
19714
+ const authFailureContext = {
19715
+ auth,
19716
+ baseUrlOverridden,
19717
+ configDir: this.config.configDir
19718
+ };
19719
+ const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
19720
+ const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
19721
+ const result = await sendEmail({
19722
+ body: {
19723
+ from,
19724
+ to: flags.to,
19725
+ subject,
19726
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
19727
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
19728
+ ...attachments !== void 0 ? { attachments } : {},
19729
+ ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
19730
+ ...flags.wait !== void 0 ? { wait: flags.wait } : {},
19731
+ ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
19732
+ },
19733
+ client: apiClient._sendClient,
19734
+ responseStyle: "fields"
19478
19735
  });
19479
- } finally {
19480
- releaseCredentialsLock();
19481
- }
19736
+ if (result.error) {
19737
+ const errorPayload = extractErrorPayload(result.error);
19738
+ writeErrorWithHints(errorPayload);
19739
+ surfaceUnauthorizedHint({
19740
+ ...authFailureContext,
19741
+ payload: errorPayload
19742
+ });
19743
+ process.exitCode = 1;
19744
+ return;
19745
+ }
19746
+ const envelope = result.data;
19747
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
19748
+ process.stderr.write(chunk);
19749
+ } });
19750
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
19751
+ });
19482
19752
  }
19483
19753
  };
19484
19754
  //#endregion
@@ -19493,6 +19763,34 @@ const SIGNIN_OTP_COPY = {
19493
19763
  resendCommand: (email) => `signin otp resend ${email}`,
19494
19764
  startCommand: (email) => `signin otp ${email}`
19495
19765
  };
19766
+ const SIGNIN_EMAIL_COPY = {
19767
+ actionNoun: "sign-in",
19768
+ actionGerund: "signing in",
19769
+ confirmCommand: (email) => `signin confirm ${email} <code>`,
19770
+ resendCommand: (email) => `signin resend ${email}`,
19771
+ startCommand: (email) => `signin ${email}`
19772
+ };
19773
+ const LOGIN_EMAIL_COPY = {
19774
+ actionNoun: "login",
19775
+ actionGerund: "logging in",
19776
+ confirmCommand: (email) => `login confirm ${email} <code>`,
19777
+ resendCommand: (email) => `login resend ${email}`,
19778
+ startCommand: (email) => `login ${email}`
19779
+ };
19780
+ const LOGIN_OTP_COPY = {
19781
+ actionNoun: "login",
19782
+ actionGerund: "logging in",
19783
+ confirmCommand: (email) => `login otp confirm ${email} <code>`,
19784
+ resendCommand: (email) => `login otp resend ${email}`,
19785
+ startCommand: (email) => `login otp ${email}`
19786
+ };
19787
+ const OTP_COPY = {
19788
+ actionNoun: "email-code auth",
19789
+ actionGerund: "authenticating",
19790
+ confirmCommand: (email) => `otp confirm ${email} <code>`,
19791
+ resendCommand: (email) => `otp resend ${email}`,
19792
+ startCommand: (email) => `otp ${email}`
19793
+ };
19496
19794
  function acquireCredentialsLock(configDir) {
19497
19795
  try {
19498
19796
  return acquireCliCredentialsLock(configDir);
@@ -19511,31 +19809,91 @@ function commonOtpStartFlags() {
19511
19809
  "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19512
19810
  force: Flags.boolean({
19513
19811
  char: "f",
19514
- description: "Replace saved credentials or pending sign-in state when needed"
19812
+ description: "Replace saved credentials or pending email-code auth state when needed"
19515
19813
  }),
19516
19814
  "signup-code": Flags.string({
19517
- description: "Signup code required to start OTP sign-in",
19815
+ description: "Signup code required to start email-code sign-in",
19518
19816
  env: "PRIMITIVE_SIGNUP_CODE"
19519
19817
  })
19520
19818
  };
19521
19819
  }
19522
- var SigninCommand = class extends LoginCommand {
19523
- static description = `Sign in to an existing Primitive account with browser approval and save an org-scoped OAuth session locally.
19820
+ var SigninCommand = class extends LoginCommand$1 {
19821
+ static args = { email: Args.string({
19822
+ description: "Email address for email-code sign-in. Omit it to use browser approval.",
19823
+ required: false
19824
+ }) };
19825
+ static description = `Sign in or log in to an existing Primitive account and save an org-scoped OAuth session locally.
19524
19826
 
19525
- This is the canonical sign-in command. It defaults to the same browser approval flow as \`primitive signin browser\`. For email-code sign-in, use \`primitive signin otp <email> --signup-code <code>\`, then \`primitive signin otp confirm <email> <code>\`. For new account creation, use \`primitive signup <email>\`.`;
19827
+ Run \`primitive signin <email> --signup-code <code> --accept-terms\` for email-code sign-in, then \`primitive signin confirm <email> <code>\`. Run \`primitive signin\` with no email to use browser approval; \`primitive signin browser\` is the explicit browser form. \`primitive login\` supports the same flows with login-shaped commands. \`primitive otp <email>\` is the shortest email-code auth form. For new account creation, use \`primitive signup <email>\`.`;
19526
19828
  static summary = "Sign in to an existing account";
19527
19829
  static examples = [
19528
19830
  "<%= config.bin %> signin",
19529
19831
  "<%= config.bin %> signin browser",
19530
19832
  "<%= config.bin %> signin --no-browser",
19833
+ "<%= config.bin %> signin user@example.com --signup-code invite-code --accept-terms",
19834
+ "<%= config.bin %> signin confirm user@example.com 123456",
19531
19835
  "<%= config.bin %> signin otp user@example.com --signup-code invite-code --accept-terms",
19532
19836
  "<%= config.bin %> signin otp confirm user@example.com 123456"
19533
19837
  ];
19838
+ static flags = {
19839
+ ...LoginCommand$1.flags,
19840
+ ...commonOtpStartFlags(),
19841
+ force: Flags.boolean({
19842
+ char: "f",
19843
+ description: "Replace saved credentials or pending email-code auth state when needed, without first verifying the existing session"
19844
+ })
19845
+ };
19846
+ async run() {
19847
+ const commandClass = this.constructor;
19848
+ const { args, flags } = await this.parse(commandClass);
19849
+ if (!args.email) {
19850
+ if (flags["signup-code"] || flags["accept-terms"]) throw cliError(`Email-code auth needs an email address. Run \`primitive ${this.emailCodeCopy().startCommand("<email>")} --signup-code <code> --accept-terms\`.`);
19851
+ await this.runBrowserLogin(flags, this.retryCommand());
19852
+ return;
19853
+ }
19854
+ const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19855
+ try {
19856
+ await runSignupStartWithCredentialLock({
19857
+ configDir: this.config.configDir,
19858
+ copy: this.emailCodeCopy(),
19859
+ email: args.email,
19860
+ flags
19861
+ });
19862
+ } finally {
19863
+ releaseCredentialsLock();
19864
+ }
19865
+ }
19534
19866
  retryCommand() {
19535
19867
  return "signin";
19536
19868
  }
19869
+ emailCodeCopy() {
19870
+ return SIGNIN_EMAIL_COPY;
19871
+ }
19872
+ };
19873
+ var LoginCommand = class extends SigninCommand {
19874
+ static args = SigninCommand.args;
19875
+ static description = `Log in or sign in to an existing Primitive account and save an org-scoped OAuth session locally.
19876
+
19877
+ Run \`primitive login <email> --signup-code <code> --accept-terms\` for email-code login, then \`primitive login confirm <email> <code>\`. Run \`primitive login\` with no email to use browser approval; \`primitive login browser\` is the explicit browser form. \`primitive signin\` supports the same flows with signin-shaped commands. \`primitive otp <email>\` is the shortest email-code auth form. For new account creation, use \`primitive signup <email>\`.`;
19878
+ static summary = "Log in to an existing account";
19879
+ static examples = [
19880
+ "<%= config.bin %> login",
19881
+ "<%= config.bin %> login browser",
19882
+ "<%= config.bin %> login --no-browser",
19883
+ "<%= config.bin %> login user@example.com --signup-code invite-code --accept-terms",
19884
+ "<%= config.bin %> login confirm user@example.com 123456",
19885
+ "<%= config.bin %> login otp user@example.com --signup-code invite-code --accept-terms",
19886
+ "<%= config.bin %> login otp confirm user@example.com 123456"
19887
+ ];
19888
+ static flags = SigninCommand.flags;
19889
+ retryCommand() {
19890
+ return "login";
19891
+ }
19892
+ emailCodeCopy() {
19893
+ return LOGIN_EMAIL_COPY;
19894
+ }
19537
19895
  };
19538
- var SigninBrowserCommand = class extends LoginCommand {
19896
+ var SigninBrowserCommand = class extends LoginCommand$1 {
19539
19897
  static description = "Sign in to an existing Primitive account by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
19540
19898
  static summary = "Sign in with browser approval";
19541
19899
  static examples = [
@@ -19548,7 +19906,20 @@ var SigninBrowserCommand = class extends LoginCommand {
19548
19906
  return "signin browser";
19549
19907
  }
19550
19908
  };
19551
- var SigninOtpCommand = class SigninOtpCommand extends Command {
19909
+ var LoginBrowserCommand = class extends LoginCommand$1 {
19910
+ static description = "Log in to an existing Primitive account by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
19911
+ static summary = "Log in with browser approval";
19912
+ static examples = [
19913
+ "<%= config.bin %> login browser",
19914
+ "<%= config.bin %> login browser --device-name work-laptop",
19915
+ "<%= config.bin %> login browser --no-browser",
19916
+ "<%= config.bin %> login browser --force"
19917
+ ];
19918
+ retryCommand() {
19919
+ return "login browser";
19920
+ }
19921
+ };
19922
+ var SigninOtpCommand = class extends Command {
19552
19923
  static args = { email: Args.string({
19553
19924
  description: "Email address to sign in with",
19554
19925
  required: false
@@ -19558,12 +19929,13 @@ var SigninOtpCommand = class SigninOtpCommand extends Command {
19558
19929
  static examples = ["<%= config.bin %> signin otp user@example.com --signup-code invite-code --accept-terms", "<%= config.bin %> signin otp confirm user@example.com 123456"];
19559
19930
  static flags = commonOtpStartFlags();
19560
19931
  async run() {
19561
- const { args, flags } = await this.parse(SigninOtpCommand);
19932
+ const commandClass = this.constructor;
19933
+ const { args, flags } = await this.parse(commandClass);
19562
19934
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19563
19935
  try {
19564
19936
  await runSignupStartWithCredentialLock({
19565
19937
  configDir: this.config.configDir,
19566
- copy: SIGNIN_OTP_COPY,
19938
+ copy: this.emailCodeCopy(),
19567
19939
  email: args.email,
19568
19940
  flags
19569
19941
  });
@@ -19571,8 +19943,31 @@ var SigninOtpCommand = class SigninOtpCommand extends Command {
19571
19943
  releaseCredentialsLock();
19572
19944
  }
19573
19945
  }
19946
+ emailCodeCopy() {
19947
+ return SIGNIN_OTP_COPY;
19948
+ }
19574
19949
  };
19575
- var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19950
+ var LoginOtpCommand = class extends SigninOtpCommand {
19951
+ static args = SigninOtpCommand.args;
19952
+ static description = "Start email-code login using Primitive's signup/auth OTP flow, send a verification code, and save the pending token locally. Requires a signup code.";
19953
+ static summary = "Start OTP login";
19954
+ static examples = ["<%= config.bin %> login otp user@example.com --signup-code invite-code --accept-terms", "<%= config.bin %> login otp confirm user@example.com 123456"];
19955
+ static flags = SigninOtpCommand.flags;
19956
+ emailCodeCopy() {
19957
+ return LOGIN_OTP_COPY;
19958
+ }
19959
+ };
19960
+ var OtpCommand = class extends SigninOtpCommand {
19961
+ static args = SigninOtpCommand.args;
19962
+ static description = "Start email-code authentication, send a verification code, and save the pending token locally. Requires a signup code.";
19963
+ static summary = "Start email-code auth";
19964
+ static examples = ["<%= config.bin %> otp user@example.com --signup-code invite-code --accept-terms", "<%= config.bin %> otp confirm user@example.com 123456"];
19965
+ static flags = SigninOtpCommand.flags;
19966
+ emailCodeCopy() {
19967
+ return OTP_COPY;
19968
+ }
19969
+ };
19970
+ var SigninOtpConfirmCommand = class extends Command {
19576
19971
  static args = {
19577
19972
  email: Args.string({
19578
19973
  description: "Email address used to start OTP sign-in",
@@ -19599,13 +19994,14 @@ var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19599
19994
  "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19600
19995
  };
19601
19996
  async run() {
19602
- const { args, flags } = await this.parse(SigninOtpConfirmCommand);
19997
+ const commandClass = this.constructor;
19998
+ const { args, flags } = await this.parse(commandClass);
19603
19999
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19604
20000
  try {
19605
20001
  await runSignupConfirmWithCredentialLock({
19606
20002
  code: args.code,
19607
20003
  configDir: this.config.configDir,
19608
- copy: SIGNIN_OTP_COPY,
20004
+ copy: this.emailCodeCopy(),
19609
20005
  email: args.email,
19610
20006
  flags
19611
20007
  });
@@ -19613,8 +20009,51 @@ var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19613
20009
  releaseCredentialsLock();
19614
20010
  }
19615
20011
  }
20012
+ emailCodeCopy() {
20013
+ return SIGNIN_OTP_COPY;
20014
+ }
20015
+ };
20016
+ var SigninConfirmCommand = class extends SigninOtpConfirmCommand {
20017
+ static args = SigninOtpConfirmCommand.args;
20018
+ static description = "Confirm a pending email-code sign-in, create an OAuth session, and save CLI credentials locally.";
20019
+ static summary = "Confirm email-code sign-in";
20020
+ static examples = ["<%= config.bin %> signin confirm user@example.com 123456", "<%= config.bin %> signin confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20021
+ static flags = SigninOtpConfirmCommand.flags;
20022
+ emailCodeCopy() {
20023
+ return SIGNIN_EMAIL_COPY;
20024
+ }
19616
20025
  };
19617
- var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
20026
+ var LoginConfirmCommand = class extends SigninOtpConfirmCommand {
20027
+ static args = SigninOtpConfirmCommand.args;
20028
+ static description = "Confirm a pending email-code login, create an OAuth session, and save CLI credentials locally.";
20029
+ static summary = "Confirm email-code login";
20030
+ static examples = ["<%= config.bin %> login confirm user@example.com 123456", "<%= config.bin %> login confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20031
+ static flags = SigninOtpConfirmCommand.flags;
20032
+ emailCodeCopy() {
20033
+ return LOGIN_EMAIL_COPY;
20034
+ }
20035
+ };
20036
+ var LoginOtpConfirmCommand = class extends SigninOtpConfirmCommand {
20037
+ static args = SigninOtpConfirmCommand.args;
20038
+ static description = "Confirm a pending OTP login, create an OAuth session, and save CLI credentials locally.";
20039
+ static summary = "Confirm OTP login";
20040
+ static examples = ["<%= config.bin %> login otp confirm user@example.com 123456", "<%= config.bin %> login otp confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20041
+ static flags = SigninOtpConfirmCommand.flags;
20042
+ emailCodeCopy() {
20043
+ return LOGIN_OTP_COPY;
20044
+ }
20045
+ };
20046
+ var OtpConfirmCommand = class extends SigninOtpConfirmCommand {
20047
+ static args = SigninOtpConfirmCommand.args;
20048
+ static description = "Confirm pending email-code authentication, create an OAuth session, and save CLI credentials locally.";
20049
+ static summary = "Confirm email-code auth";
20050
+ static examples = ["<%= config.bin %> otp confirm user@example.com 123456", "<%= config.bin %> otp confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000"];
20051
+ static flags = SigninOtpConfirmCommand.flags;
20052
+ emailCodeCopy() {
20053
+ return OTP_COPY;
20054
+ }
20055
+ };
20056
+ var SigninOtpResendCommand = class extends Command {
19618
20057
  static args = { email: Args.string({
19619
20058
  description: "Email address used to start OTP sign-in",
19620
20059
  required: true
@@ -19628,12 +20067,13 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
19628
20067
  hidden: true
19629
20068
  }) };
19630
20069
  async run() {
19631
- const { args, flags } = await this.parse(SigninOtpResendCommand);
20070
+ const commandClass = this.constructor;
20071
+ const { args, flags } = await this.parse(commandClass);
19632
20072
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19633
20073
  try {
19634
20074
  await runSignupResendWithCredentialLock({
19635
20075
  configDir: this.config.configDir,
19636
- copy: SIGNIN_OTP_COPY,
20076
+ copy: this.emailCodeCopy(),
19637
20077
  email: args.email,
19638
20078
  flags
19639
20079
  });
@@ -19641,6 +20081,49 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
19641
20081
  releaseCredentialsLock();
19642
20082
  }
19643
20083
  }
20084
+ emailCodeCopy() {
20085
+ return SIGNIN_OTP_COPY;
20086
+ }
20087
+ };
20088
+ var SigninResendCommand = class extends SigninOtpResendCommand {
20089
+ static args = SigninOtpResendCommand.args;
20090
+ static description = "Resend the verification code for a pending email-code sign-in.";
20091
+ static summary = "Resend email-code sign-in code";
20092
+ static examples = ["<%= config.bin %> signin resend user@example.com"];
20093
+ static flags = SigninOtpResendCommand.flags;
20094
+ emailCodeCopy() {
20095
+ return SIGNIN_EMAIL_COPY;
20096
+ }
20097
+ };
20098
+ var LoginResendCommand = class extends SigninOtpResendCommand {
20099
+ static args = SigninOtpResendCommand.args;
20100
+ static description = "Resend the verification code for a pending email-code login.";
20101
+ static summary = "Resend email-code login code";
20102
+ static examples = ["<%= config.bin %> login resend user@example.com"];
20103
+ static flags = SigninOtpResendCommand.flags;
20104
+ emailCodeCopy() {
20105
+ return LOGIN_EMAIL_COPY;
20106
+ }
20107
+ };
20108
+ var LoginOtpResendCommand = class extends SigninOtpResendCommand {
20109
+ static args = SigninOtpResendCommand.args;
20110
+ static description = "Resend the verification code for a pending OTP login.";
20111
+ static summary = "Resend OTP login code";
20112
+ static examples = ["<%= config.bin %> login otp resend user@example.com"];
20113
+ static flags = SigninOtpResendCommand.flags;
20114
+ emailCodeCopy() {
20115
+ return LOGIN_OTP_COPY;
20116
+ }
20117
+ };
20118
+ var OtpResendCommand = class extends SigninOtpResendCommand {
20119
+ static args = SigninOtpResendCommand.args;
20120
+ static description = "Resend the verification code for pending email-code authentication.";
20121
+ static summary = "Resend email-code auth code";
20122
+ static examples = ["<%= config.bin %> otp resend user@example.com"];
20123
+ static flags = SigninOtpResendCommand.flags;
20124
+ emailCodeCopy() {
20125
+ return OTP_COPY;
20126
+ }
19644
20127
  };
19645
20128
  //#endregion
19646
20129
  //#region src/oclif/commands/whoami.ts
@@ -19955,6 +20438,7 @@ const CANONICAL_OPERATION_ALIASES = {
19955
20438
  "sending:send": "sending:send-email",
19956
20439
  "sent:get": "sending:get-sent-email",
19957
20440
  "sent:list": "sending:list-sent-emails",
20441
+ "threads:get": "threads:get-thread",
19958
20442
  "webhook-deliveries:list": "webhook-deliveries:list-deliveries",
19959
20443
  "webhook-deliveries:replay": "webhook-deliveries:replay-delivery"
19960
20444
  };
@@ -19986,11 +20470,22 @@ const COMMANDS = {
19986
20470
  reply: ReplyCommand,
19987
20471
  chat: ChatCommand,
19988
20472
  login: LoginCommand,
20473
+ "login:browser": LoginBrowserCommand,
20474
+ "login:confirm": LoginConfirmCommand,
20475
+ "login:otp": LoginOtpCommand,
20476
+ "login:otp:confirm": LoginOtpConfirmCommand,
20477
+ "login:otp:resend": LoginOtpResendCommand,
20478
+ "login:resend": LoginResendCommand,
20479
+ otp: OtpCommand,
20480
+ "otp:confirm": OtpConfirmCommand,
20481
+ "otp:resend": OtpResendCommand,
19989
20482
  signin: SigninCommand,
19990
20483
  "signin:browser": SigninBrowserCommand,
20484
+ "signin:confirm": SigninConfirmCommand,
19991
20485
  "signin:otp": SigninOtpCommand,
19992
20486
  "signin:otp:confirm": SigninOtpConfirmCommand,
19993
20487
  "signin:otp:resend": SigninOtpResendCommand,
20488
+ "signin:resend": SigninResendCommand,
19994
20489
  signup: SignupCommand,
19995
20490
  "signup:confirm": SignupConfirmCommand,
19996
20491
  "signup:interactive": SignupInteractiveCommand,