@primitivedotdev/cli 0.32.0 → 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) {
@@ -14613,41 +14744,40 @@ function buildChatFollowUpCommands(context) {
14613
14744
  return commands;
14614
14745
  }
14615
14746
  function buildChatRecoveryCommands(context) {
14616
- return [
14617
- buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
14618
- "primitive",
14619
- "emails",
14620
- "wait",
14621
- "--reply-to-sent-email-id",
14622
- context.sent.id,
14623
- "--to",
14624
- context.from,
14625
- "--since",
14626
- context.sentAtIso,
14627
- "--timeout",
14628
- String(context.timeoutSeconds)
14629
- ]),
14630
- buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
14631
- "primitive",
14632
- "emails",
14633
- "wait",
14634
- "--from",
14635
- context.recipient,
14636
- "--to",
14637
- context.from,
14638
- "--since",
14639
- context.sentAtIso,
14640
- "--timeout",
14641
- String(context.timeoutSeconds)
14642
- ]),
14643
- buildCommand("inspect_sent_email", "Inspect the outbound send", [
14644
- "primitive",
14645
- "sent",
14646
- "get",
14647
- "--id",
14648
- context.sent.id
14649
- ])
14650
- ];
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;
14651
14781
  }
14652
14782
  function buildChatJsonEnvelope(context) {
14653
14783
  const responseBody = resolveChatResponseBody(context.reply);
@@ -14794,7 +14924,7 @@ var ChatCommand = class ChatCommand extends Command {
14794
14924
  static summary = "Chat with an agent over email (send and wait for the reply)";
14795
14925
  static examples = [
14796
14926
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14797
- "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
14927
+ "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14798
14928
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14799
14929
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14800
14930
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
@@ -14823,7 +14953,10 @@ var ChatCommand = class ChatCommand extends Command {
14823
14953
  hidden: true
14824
14954
  }),
14825
14955
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
14826
- 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
+ }),
14827
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." }),
14828
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." }),
14829
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." }),
@@ -15130,6 +15263,38 @@ function redactConfig(config) {
15130
15263
  environments: Object.fromEntries(Object.entries(config.environments).map(([name, environment]) => [name, redactCliEnvironment(environment)]))
15131
15264
  };
15132
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
+ }
15133
15298
  function switchCliEnvironment(configDir, environmentName) {
15134
15299
  const environment = normalizeCliEnvironmentName(environmentName);
15135
15300
  const config = loadOrCreateConfig(configDir);
@@ -15179,16 +15344,16 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
15179
15344
  const { flags } = await this.parse(ConfigSetCommand);
15180
15345
  const headers = flags.header ?? [];
15181
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 });
15182
- const config = upsertCliEnvironment({
15347
+ const { environment, removedCredentials } = upsertCliEnvironmentAndClearCredentialsIfSwitched({
15183
15348
  apiBaseUrl1: flags["api-base-url-1"],
15184
15349
  apiBaseUrl2: flags["api-base-url-2"],
15185
- config: loadOrCreateConfig(this.config.configDir),
15350
+ configDir: this.config.configDir,
15186
15351
  environmentName: flags.environment,
15187
15352
  headers,
15188
15353
  unsetHeaders: flags["unset-header"]
15189
15354
  });
15190
- saveCliConfig(this.config.configDir, config);
15191
- 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");
15192
15357
  }
15193
15358
  };
15194
15359
  var ConfigUseCommand = class ConfigUseCommand extends Command {
@@ -15878,7 +16043,9 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
15878
16043
  process.exitCode = 1;
15879
16044
  return;
15880
16045
  }
15881
- cursor = page.cursor ?? cursor;
16046
+ const nextCursor = cursorFromAcceptedRows(page.rows);
16047
+ const cursorAdvanced = Boolean(nextCursor && nextCursor !== cursor);
16048
+ if (nextCursor) cursor = nextCursor;
15882
16049
  for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
15883
16050
  if (flags.table) {
15884
16051
  if (!headerPrinted) {
@@ -15890,7 +16057,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
15890
16057
  matched += 1;
15891
16058
  if (matched >= flags.number) return;
15892
16059
  }
15893
- if (page.rows.length > 0) continue;
16060
+ if (cursorAdvanced) continue;
15894
16061
  if (deadline !== null && Date.now() >= deadline) break;
15895
16062
  await sleep$1(flags.interval * 1e3);
15896
16063
  }
@@ -16000,7 +16167,9 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
16000
16167
  process.exitCode = 1;
16001
16168
  return;
16002
16169
  }
16003
- cursor = page.cursor ?? cursor;
16170
+ const nextCursor = cursorFromAcceptedRows(page.rows);
16171
+ const cursorAdvanced = Boolean(nextCursor && nextCursor !== cursor);
16172
+ if (nextCursor) cursor = nextCursor;
16004
16173
  for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
16005
16174
  if (flags.jsonl) this.log(JSON.stringify(email));
16006
16175
  else {
@@ -16013,7 +16182,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
16013
16182
  printed += 1;
16014
16183
  if (flags.number && printed >= flags.number) return;
16015
16184
  }
16016
- if (page.rows.length > 0) continue;
16185
+ if (cursorAdvanced) continue;
16017
16186
  if (deadline !== null && Date.now() >= deadline) break;
16018
16187
  await sleep$1(flags.interval * 1e3);
16019
16188
  }
@@ -16769,7 +16938,7 @@ const PRIMITIVE_TEAM_AUTHOR = {
16769
16938
  url: "https://primitive.dev"
16770
16939
  };
16771
16940
  const SDK_VERSION_RANGE = "^0.32.0";
16772
- const CLI_VERSION_RANGE = "^0.32.0";
16941
+ const CLI_VERSION_RANGE = "^0.32.1";
16773
16942
  const ESBUILD_VERSION_RANGE = "^0.27.0";
16774
16943
  function renderHandler() {
16775
16944
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -16795,49 +16964,93 @@ interface Env {
16795
16964
  PRIMITIVE_WEBHOOK_SECRET: string;
16796
16965
  }
16797
16966
 
16798
- // Loop-protection knob. Only used by the isLoop helper below; the
16799
- // handler's outbound reply address is server-defaulted (no
16800
- // from-address parameter is passed to client.reply). Update this if
16801
- // you later switch to sending from a non-managed-domain address so
16802
- // the loop guard recognizes mail returning to it as self-traffic.
16803
- 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
+ }
16804
17007
 
16805
17008
  // Loop protection. A newly deployed Function starts as a fallback
16806
- // endpoint for managed *.primitive.email domains that do not have a
16807
- // domain-scoped endpoint. That can include bounces and auto-replies
16808
- // generated by the handler's own outbound traffic. Without this guard
16809
- // 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.
16810
17014
  //
16811
- // The default check returns true when From is on any *.primitive.email
16812
- // address (covers managed-domain fallback mail, the simple
16813
- // self-reply case, and bounces from mailer-daemon@*.primitive.email)
16814
- // or when From contains REPLY_FROM as a case-insensitive substring.
16815
- // Substring matching is deliberate so display-name forms like
16816
- // "Support <support@example.com>" match a bare-address REPLY_FROM,
16817
- // but it also accepts false positives where REPLY_FROM is a suffix
16818
- // of another address (e.g. REPLY_FROM="info@x.com" matches
16819
- // "mr.info@x.com"). For strict equality, parse the address out of the
16820
- // 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.
16821
17019
  //
16822
17020
  // Extend this helper if you need stricter detection. Common additions:
16823
- // - Match the org's signup / account-owner email (not auto-injected
16824
- // into env today; either bake it into a SIGNUP_EMAIL const or read
16825
- // it from a secret you set via \`primitive functions:set-secret\`).
16826
17021
  // - Honor RFC 3834 auto-response headers: skip when
16827
- // \`event.email.headers["auto-submitted"]\` is anything other than
16828
- // "no", or when a \`List-Unsubscribe\` / \`Precedence: bulk\` header
16829
- // is present.
17022
+ // event.email.headers["auto-submitted"] is anything other than "no",
17023
+ // or when a List-Unsubscribe / Precedence: bulk header is present.
16830
17024
  // - Track Message-ID / In-Reply-To chains to break ping-pong loops
16831
17025
  // between two cooperating handlers on different domains.
16832
17026
  export function isLoop(event: EmailReceivedEvent): boolean {
16833
- // event.email.headers.from is the raw RFC 2822 header value, so it
16834
- // may be a bare address ("alice@example.com") or a display-name form
16835
- // ("Alice <alice@example.com>"). Lowercase substring checks match
16836
- // both shapes without needing to parse the bracketed address.
16837
- const from = event.email.headers.from?.toLowerCase() ?? "";
16838
- if (!from) return false;
16839
- if (from.includes(".primitive.email")) return true;
16840
- 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
+
16841
17054
  return false;
16842
17055
  }
16843
17056
 
@@ -16885,11 +17098,16 @@ export default {
16885
17098
  apiBaseUrl1: env.PRIMITIVE_API_BASE_URL,
16886
17099
  });
16887
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
+
16888
17106
  // Recipient gate
16889
17107
  // https://www.primitive.dev/docs/sending#who-you-can-send-to
16890
17108
  // Even via client.reply, sends to the original sender are
16891
17109
  // subject to the recipient gate. New accounts can send to
16892
- // *.primitive.email addresses, verified domains, addresses that
17110
+ // Primitive-managed domains, verified domains, addresses that
16893
17111
  // have authenticated to you, and other org-member signup emails.
16894
17112
  // Sends to arbitrary external addresses return 403
16895
17113
  // recipient_not_allowed with a structured gates[] array until
@@ -16939,8 +17157,10 @@ function renderPackageJson(name) {
16939
17157
  type: "module",
16940
17158
  scripts: {
16941
17159
  build: "node build.mjs",
16942
- deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js`,
16943
- 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"
16944
17164
  },
16945
17165
  dependencies: { "@primitivedotdev/sdk": SDK_VERSION_RANGE },
16946
17166
  devDependencies: {
@@ -17011,13 +17231,47 @@ npm run build
17011
17231
  npm run deploy
17012
17232
  \`\`\`
17013
17233
 
17014
- The deploy step calls \`primitive functions deploy\` (provided by the
17015
- \`@primitivedotdev/cli\` package; install with
17234
+ The deploy step calls \`primitive functions deploy --wait\` (provided
17235
+ by the \`@primitivedotdev/cli\` package; install with
17016
17236
  \`npm install -g @primitivedotdev/cli\` or run via
17017
17237
  \`npx @primitivedotdev/cli@latest <command>\`). It requires
17018
17238
  \`PRIMITIVE_API_KEY\` to be set in your shell (or pass \`--api-key\`).
17019
17239
  Run \`primitive signin\` once to save a key in your CLI config if you
17020
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
+ \`\`\`
17021
17275
  `;
17022
17276
  }
17023
17277
  function renderEmailReplyTemplateFiles(name) {
@@ -17192,8 +17446,9 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
17192
17446
  this.log("Next:");
17193
17447
  this.log(` cd ${outDir}`);
17194
17448
  this.log(" npm install");
17195
- this.log(" npm run build");
17196
- 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");
17197
17452
  }
17198
17453
  };
17199
17454
  //#endregion
@@ -17823,26 +18078,32 @@ var FunctionsTemplatesCommand = class FunctionsTemplatesCommand extends Command
17823
18078
  //#endregion
17824
18079
  //#region src/oclif/commands/functions-test-function.ts
17825
18080
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
17826
- const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
18081
+ const TERMINAL_TEST_TRACE_STATES = new Set([
18082
+ "completed",
18083
+ "failed",
18084
+ "send_failed"
18085
+ ]);
17827
18086
  function buildFunctionTestOutcome(params) {
18087
+ const inbound = params.trace.inbound_email;
17828
18088
  const outcome = {
17829
18089
  elapsed_seconds: params.elapsedSeconds,
17830
18090
  function_id: params.functionId,
17831
18091
  inbound_domain: params.invocation.inbound_domain,
17832
- inbound_id: params.inboundId,
18092
+ inbound_id: inbound?.id ?? null,
17833
18093
  inbound_to: params.invocation.to,
17834
18094
  poll_since: params.invocation.poll_since,
18095
+ state: params.trace.state,
17835
18096
  test_run_id: params.invocation.test_run_id,
17836
18097
  test_send_id: params.invocation.send_id,
17837
18098
  test_subject: params.invocation.subject,
17838
18099
  trace_url: params.invocation.trace_url,
17839
18100
  watch_url: params.invocation.watch_url,
17840
- webhook_attempt_count: params.detail.webhook_attempt_count,
17841
- webhook_last_error: params.detail.webhook_last_error,
17842
- webhook_last_status_code: params.detail.webhook_last_status_code,
17843
- 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
17844
18105
  };
17845
- if (params.showSends) outcome.sent_emails = params.detail.replies;
18106
+ if (params.showSends) outcome.sent_emails = params.trace.replies;
17846
18107
  return outcome;
17847
18108
  }
17848
18109
  function writeFunctionTestProgress(message, writeStderr = (chunk) => {
@@ -17941,7 +18202,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
17941
18202
  required: true
17942
18203
  }),
17943
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>`." }),
17944
- 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." }),
17945
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." }),
17946
18207
  timeout: Flags.integer({
17947
18208
  default: DEFAULT_WAIT_TIMEOUT_SECONDS,
@@ -18001,41 +18262,15 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18001
18262
  const timeoutMs = flags.timeout * 1e3;
18002
18263
  const pollIntervalMs = flags["poll-interval"] * 1e3;
18003
18264
  const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
18004
- writeFunctionTestProgress(`Waiting for test inbound to arrive at ${invocation.to}...`);
18005
- let inboundId;
18006
- while (!isExpired()) {
18007
- const page = await fetchEmailSearchPage({
18008
- apiClient,
18009
- filters: { to: invocation.to },
18010
- pageSize: 25,
18011
- since: invocation.poll_since
18012
- });
18013
- if (!page.ok) {
18014
- const payload = extractErrorPayload(page.error);
18015
- writeErrorWithHints(payload);
18016
- surfaceUnauthorizedHint({
18017
- auth,
18018
- baseUrlOverridden,
18019
- configDir: this.config.configDir,
18020
- payload
18021
- });
18022
- process.exitCode = 1;
18023
- return;
18024
- }
18025
- const found = page.rows[0];
18026
- if (found) {
18027
- inboundId = found.id;
18028
- break;
18029
- }
18030
- await sleep$1(pollIntervalMs);
18031
- }
18032
- 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 });
18033
- writeFunctionTestProgress(`Inbound landed (${inboundId}). Waiting for function to run...`);
18034
- let detail;
18265
+ writeFunctionTestProgress(`Waiting for test run ${invocation.test_run_id} to complete for ${invocation.to}...`);
18266
+ let trace;
18035
18267
  while (!isExpired()) {
18036
- const result = await getEmail({
18268
+ const result = await getFunctionTestRunTrace({
18037
18269
  client: apiClient.client,
18038
- path: { id: inboundId },
18270
+ path: {
18271
+ id: flags.id,
18272
+ run_id: invocation.test_run_id
18273
+ },
18039
18274
  responseStyle: "fields"
18040
18275
  });
18041
18276
  if (result.error) {
@@ -18051,24 +18286,22 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18051
18286
  return;
18052
18287
  }
18053
18288
  const fetched = result.data.data;
18054
- if (fetched.webhook_status && TERMINAL_WEBHOOK_STATUSES.has(fetched.webhook_status)) {
18055
- detail = fetched;
18289
+ if (TERMINAL_TEST_TRACE_STATES.has(fetched.state)) {
18290
+ trace = fetched;
18056
18291
  break;
18057
18292
  }
18058
18293
  await sleep$1(pollIntervalMs);
18059
18294
  }
18060
- 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 });
18061
- 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 });
18062
18296
  const outcome = buildFunctionTestOutcome({
18063
- detail,
18064
- elapsedSeconds,
18297
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1e3),
18065
18298
  functionId: flags.id,
18066
- inboundId,
18067
18299
  invocation,
18068
- showSends: shouldShowSends
18300
+ showSends: shouldShowSends,
18301
+ trace
18069
18302
  });
18070
18303
  this.log(JSON.stringify(outcome, null, 2));
18071
- if (detail.webhook_status === "exhausted") process.exitCode = 1;
18304
+ if (trace.state === "failed" || trace.state === "send_failed") process.exitCode = 1;
18072
18305
  });
18073
18306
  }
18074
18307
  };
@@ -18319,7 +18552,7 @@ async function checkExistingLogin(params) {
18319
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."
18320
18553
  };
18321
18554
  }
18322
- var LoginCommand = class extends Command {
18555
+ var LoginCommand$1 = class extends Command {
18323
18556
  static description = "Log in by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
18324
18557
  static summary = "Log in with browser approval";
18325
18558
  static examples = [
@@ -18343,6 +18576,9 @@ var LoginCommand = class extends Command {
18343
18576
  async run() {
18344
18577
  const commandClass = this.constructor;
18345
18578
  const { flags } = await this.parse(commandClass);
18579
+ await this.runBrowserLogin(flags, this.retryCommand());
18580
+ }
18581
+ async runBrowserLogin(flags, retryCommand = this.retryCommand()) {
18346
18582
  let releaseCredentialsLock;
18347
18583
  try {
18348
18584
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -18350,7 +18586,7 @@ var LoginCommand = class extends Command {
18350
18586
  throw cliError$3(error instanceof Error ? error.message : String(error));
18351
18587
  }
18352
18588
  try {
18353
- await this.runWithCredentialLock(flags, this.retryCommand());
18589
+ await this.runWithCredentialLock(flags, retryCommand);
18354
18590
  } finally {
18355
18591
  releaseCredentialsLock();
18356
18592
  }
@@ -18458,520 +18694,85 @@ var LoginCommand = class extends Command {
18458
18694
  }
18459
18695
  };
18460
18696
  //#endregion
18461
- //#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
+ };
18462
18710
  function cliError$2(message) {
18463
18711
  return new Errors.CLIError(message, { exit: 1 });
18464
18712
  }
18465
18713
  function unwrapData$1(value) {
18466
18714
  return value?.data ?? null;
18467
18715
  }
18468
- function isSavedOAuthSessionExpiredError(error) {
18469
- 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);
18470
18718
  }
18471
- async function runLogoutWithCredentialLock(params) {
18472
- const deps = {
18473
- cliLogout,
18474
- createAuthenticatedCliApiClient,
18475
- ...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
18476
18734
  };
18477
- 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`);
18478
18758
  try {
18479
- 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;
18480
18764
  } catch (error) {
18481
- deleteCliCredentials(params.configDir);
18482
- const detail = error instanceof Error ? error.message : String(error);
18483
- process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
18484
- process.exitCode = 1;
18485
- return;
18765
+ rmSync(tempPath, { force: true });
18766
+ throw error;
18486
18767
  }
18487
- if (!credentials) throw cliError$2("Not logged in. Run `primitive signin` to create saved CLI credentials.");
18488
- let authenticated;
18768
+ }
18769
+ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18770
+ const path = pendingSignupPath(configDir);
18771
+ let contents;
18489
18772
  try {
18490
- authenticated = await deps.createAuthenticatedCliApiClient({
18491
- apiBaseUrl1: params.flags["api-base-url-1"],
18492
- configDir: params.configDir,
18493
- credentialsLockHeld: true
18494
- });
18773
+ contents = readFileSync(path, "utf8");
18495
18774
  } catch (error) {
18496
- if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
18497
- process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
18498
- return;
18499
- }
18500
- throw error;
18501
- }
18502
- const freshCredentials = authenticated.auth.credentials ?? credentials;
18503
- const result = await deps.cliLogout({
18504
- body: { key_id: freshCredentials.oauth_grant_id },
18505
- client: authenticated.apiClient.client,
18506
- responseStyle: "fields"
18507
- });
18508
- if (result.error) {
18509
- const payload = extractErrorPayload(result.error);
18510
- const code = extractErrorCode(payload);
18511
- if (code === API_ERROR_CODES.unauthorized || code === API_ERROR_CODES.notFound) {
18512
- deleteCliCredentials(params.configDir);
18513
- writeErrorWithHints(payload);
18514
- process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
18515
- process.exitCode = 1;
18516
- return;
18517
- }
18518
- writeErrorWithHints(payload);
18519
- throw cliError$2("Could not revoke the saved Primitive CLI OAuth grant.");
18520
- }
18521
- const logout = unwrapData$1(result.data);
18522
- deleteCliCredentials(params.configDir);
18523
- const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
18524
- process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
18525
- }
18526
- var LogoutCommand = class LogoutCommand extends Command {
18527
- static description = "Log out by revoking the saved Primitive CLI OAuth grant and deleting local credentials.";
18528
- static summary = "Log out and revoke the saved CLI OAuth grant";
18529
- static examples = ["<%= config.bin %> logout"];
18530
- static flags = { "api-base-url-1": Flags.string({
18531
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18532
- env: "PRIMITIVE_API_BASE_URL_1",
18533
- hidden: true
18534
- }) };
18535
- async run() {
18536
- const { flags } = await this.parse(LogoutCommand);
18537
- let releaseCredentialsLock;
18538
- try {
18539
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
18540
- } catch (error) {
18541
- throw cliError$2(error instanceof Error ? error.message : String(error));
18542
- }
18543
- try {
18544
- await runLogoutWithCredentialLock({
18545
- configDir: this.config.configDir,
18546
- flags
18547
- });
18548
- } finally {
18549
- releaseCredentialsLock();
18550
- }
18551
- }
18552
- };
18553
- //#endregion
18554
- //#region src/oclif/message-body-sources.ts
18555
- function defaultReadFile(path) {
18556
- return readFileSync(path, "utf8");
18557
- }
18558
- function defaultReadStdin() {
18559
- if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
18560
- return readFileSync(0, "utf8");
18561
- }
18562
- function selectedSources(sources) {
18563
- return sources.filter(([, selected]) => selected).map(([label]) => label);
18564
- }
18565
- function readTextFile(path, label, readFile) {
18566
- try {
18567
- return {
18568
- content: readFile(path),
18569
- kind: "ok"
18570
- };
18571
- } catch (error) {
18572
- return {
18573
- kind: "error",
18574
- message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
18575
- };
18576
- }
18577
- }
18578
- function readTextStdin(label, readStdin) {
18579
- try {
18580
- return {
18581
- content: readStdin(),
18582
- kind: "ok"
18583
- };
18584
- } catch (error) {
18585
- return {
18586
- kind: "error",
18587
- message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
18588
- };
18589
- }
18590
- }
18591
- function resolveMessageBodies(input) {
18592
- const bodySources = selectedSources([
18593
- ["--body", input.body !== void 0],
18594
- ["--body-file", input.bodyFile !== void 0],
18595
- ["--body-stdin", input.bodyStdin === true]
18596
- ]);
18597
- if (bodySources.length > 1) return {
18598
- kind: "error",
18599
- message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
18600
- };
18601
- const htmlSources = selectedSources([
18602
- ["--html", input.html !== void 0],
18603
- ["--html-file", input.htmlFile !== void 0],
18604
- ["--html-stdin", input.htmlStdin === true]
18605
- ]);
18606
- if (htmlSources.length > 1) return {
18607
- kind: "error",
18608
- message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
18609
- };
18610
- const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
18611
- if (stdinSources.length > 1) return {
18612
- kind: "error",
18613
- message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
18614
- };
18615
- if (bodySources.length === 0 && htmlSources.length === 0) return {
18616
- kind: "error",
18617
- message: "Either a plain-text body source or an HTML body source is required."
18618
- };
18619
- const readFile = input.readFile ?? defaultReadFile;
18620
- const readStdin = input.readStdin ?? defaultReadStdin;
18621
- let body = input.body;
18622
- let html = input.html;
18623
- if (input.bodyFile !== void 0) {
18624
- const result = readTextFile(input.bodyFile, "--body-file", readFile);
18625
- if (result.kind === "error") return result;
18626
- body = result.content;
18627
- }
18628
- if (input.bodyStdin === true) {
18629
- const result = readTextStdin("--body-stdin", readStdin);
18630
- if (result.kind === "error") return result;
18631
- body = result.content;
18632
- }
18633
- if (input.htmlFile !== void 0) {
18634
- const result = readTextFile(input.htmlFile, "--html-file", readFile);
18635
- if (result.kind === "error") return result;
18636
- html = result.content;
18637
- }
18638
- if (input.htmlStdin === true) {
18639
- const result = readTextStdin("--html-stdin", readStdin);
18640
- if (result.kind === "error") return result;
18641
- html = result.content;
18642
- }
18643
- if (!body && !html) return {
18644
- kind: "error",
18645
- message: "Either a non-empty plain-text body or a non-empty HTML body is required."
18646
- };
18647
- return {
18648
- ...body !== void 0 ? { body } : {},
18649
- ...html !== void 0 ? { html } : {},
18650
- kind: "ok"
18651
- };
18652
- }
18653
- //#endregion
18654
- //#region src/oclif/commands/reply.ts
18655
- var ReplyCommand = class ReplyCommand extends Command {
18656
- static description = `Reply to an inbound email.
18657
-
18658
- 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.`;
18659
- static summary = "Reply to an inbound email";
18660
- static examples = [
18661
- "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
18662
- "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
18663
- "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
18664
- "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
18665
- ];
18666
- static flags = {
18667
- "api-key": Flags.string({
18668
- description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18669
- env: "PRIMITIVE_API_KEY"
18670
- }),
18671
- "api-base-url-1": Flags.string({
18672
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18673
- env: "PRIMITIVE_API_BASE_URL_1",
18674
- hidden: true
18675
- }),
18676
- "api-base-url-2": Flags.string({
18677
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18678
- env: "PRIMITIVE_API_BASE_URL_2",
18679
- hidden: true
18680
- }),
18681
- id: Flags.string({
18682
- description: "Inbound email id to reply to.",
18683
- required: true
18684
- }),
18685
- body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
18686
- "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
18687
- "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." }),
18688
- html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
18689
- "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
18690
- "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." }),
18691
- from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
18692
- 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." }),
18693
- "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
18694
- time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18695
- };
18696
- async run() {
18697
- const { flags } = await this.parse(ReplyCommand);
18698
- const bodies = resolveMessageBodies({
18699
- body: flags.body,
18700
- bodyFile: flags["body-file"],
18701
- bodyStdin: flags["body-stdin"],
18702
- html: flags.html,
18703
- htmlFile: flags["html-file"],
18704
- htmlStdin: flags["html-stdin"]
18705
- });
18706
- if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
18707
- await runWithTiming(flags.time, async () => {
18708
- const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18709
- apiKey: flags["api-key"],
18710
- apiBaseUrl1: flags["api-base-url-1"],
18711
- apiBaseUrl2: flags["api-base-url-2"],
18712
- configDir: this.config.configDir
18713
- });
18714
- const result = await replyToEmail({
18715
- body: {
18716
- ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
18717
- ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
18718
- ...flags.from !== void 0 ? { from: flags.from } : {},
18719
- ...flags.wait !== void 0 ? { wait: flags.wait } : {},
18720
- ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
18721
- },
18722
- client: apiClient.client,
18723
- path: { id: flags.id },
18724
- responseStyle: "fields"
18725
- });
18726
- if (result.error) {
18727
- const errorPayload = extractErrorPayload(result.error);
18728
- writeErrorWithHints(errorPayload);
18729
- surfaceUnauthorizedHint({
18730
- auth,
18731
- baseUrlOverridden,
18732
- configDir: this.config.configDir,
18733
- payload: errorPayload
18734
- });
18735
- process.exitCode = 1;
18736
- return;
18737
- }
18738
- const envelope = result.data;
18739
- writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
18740
- process.stderr.write(chunk);
18741
- } });
18742
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
18743
- });
18744
- }
18745
- };
18746
- //#endregion
18747
- //#region src/oclif/attachments.ts
18748
- function readAttachmentBytes(path, readFile) {
18749
- try {
18750
- return Buffer.from(readFile(path));
18751
- } catch (error) {
18752
- const detail = error instanceof Error ? error.message : String(error);
18753
- throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
18754
- }
18755
- }
18756
- function hasControlCharacter(value) {
18757
- return Array.from(value).some((character) => {
18758
- const code = character.charCodeAt(0);
18759
- return code <= 31 || code >= 127 && code <= 159;
18760
- });
18761
- }
18762
- function validateAttachmentFilename(path, filename) {
18763
- if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
18764
- if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
18765
- }
18766
- function readAttachmentFiles(paths, readFile = readFileSync) {
18767
- if (!paths || paths.length === 0) return void 0;
18768
- return paths.map((path) => {
18769
- const filename = basename(path);
18770
- validateAttachmentFilename(path, filename);
18771
- const bytes = readAttachmentBytes(path, readFile);
18772
- if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
18773
- return {
18774
- content_base64: bytes.toString("base64"),
18775
- filename
18776
- };
18777
- });
18778
- }
18779
- //#endregion
18780
- //#region src/oclif/commands/send.ts
18781
- var SendCommand = class SendCommand extends Command {
18782
- static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
18783
-
18784
- --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
18785
- --subject defaults to the first line of the body when omitted.
18786
- --attachment attaches a file; repeat it to attach multiple files.
18787
-
18788
- For the full flag set (custom message-id threading on the wire,
18789
- references arrays, etc.), use \`primitive sending send\`.`;
18790
- static summary = "Send an email (simplified, agent-friendly)";
18791
- static examples = [
18792
- "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
18793
- "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
18794
- "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
18795
- "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
18796
- "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
18797
- "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
18798
- "<%= 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"
18799
- ];
18800
- static flags = {
18801
- "api-key": Flags.string({
18802
- description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18803
- env: "PRIMITIVE_API_KEY"
18804
- }),
18805
- "api-base-url-1": Flags.string({
18806
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18807
- env: "PRIMITIVE_API_BASE_URL_1",
18808
- hidden: true
18809
- }),
18810
- "api-base-url-2": Flags.string({
18811
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18812
- env: "PRIMITIVE_API_BASE_URL_2",
18813
- hidden: true
18814
- }),
18815
- to: Flags.string({
18816
- description: "Recipient address (e.g. alice@example.com).",
18817
- required: true
18818
- }),
18819
- from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
18820
- subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
18821
- body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
18822
- "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." }),
18823
- "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." }),
18824
- html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
18825
- "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
18826
- "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." }),
18827
- attachment: Flags.string({
18828
- description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
18829
- multiple: true
18830
- }),
18831
- "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>`." }),
18832
- 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." }),
18833
- "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
18834
- time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18835
- };
18836
- async run() {
18837
- const { flags } = await this.parse(SendCommand);
18838
- const bodies = resolveMessageBodies({
18839
- body: flags.body,
18840
- bodyFile: flags["body-file"],
18841
- bodyStdin: flags["body-stdin"],
18842
- html: flags.html,
18843
- htmlFile: flags["html-file"],
18844
- htmlStdin: flags["html-stdin"]
18845
- });
18846
- if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
18847
- const attachments = readAttachmentFiles(flags.attachment);
18848
- await runWithTiming(flags.time, async () => {
18849
- const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18850
- apiKey: flags["api-key"],
18851
- apiBaseUrl1: flags["api-base-url-1"],
18852
- apiBaseUrl2: flags["api-base-url-2"],
18853
- configDir: this.config.configDir
18854
- });
18855
- const authFailureContext = {
18856
- auth,
18857
- baseUrlOverridden,
18858
- configDir: this.config.configDir
18859
- };
18860
- const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
18861
- const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
18862
- const result = await sendEmail({
18863
- body: {
18864
- from,
18865
- to: flags.to,
18866
- subject,
18867
- ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
18868
- ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
18869
- ...attachments !== void 0 ? { attachments } : {},
18870
- ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
18871
- ...flags.wait !== void 0 ? { wait: flags.wait } : {},
18872
- ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
18873
- },
18874
- client: apiClient._sendClient,
18875
- responseStyle: "fields"
18876
- });
18877
- if (result.error) {
18878
- const errorPayload = extractErrorPayload(result.error);
18879
- writeErrorWithHints(errorPayload);
18880
- surfaceUnauthorizedHint({
18881
- ...authFailureContext,
18882
- payload: errorPayload
18883
- });
18884
- process.exitCode = 1;
18885
- return;
18886
- }
18887
- const envelope = result.data;
18888
- writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
18889
- process.stderr.write(chunk);
18890
- } });
18891
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
18892
- });
18893
- }
18894
- };
18895
- //#endregion
18896
- //#region src/oclif/commands/signup.ts
18897
- const INVALID_VERIFICATION_CODE = "invalid_verification_code";
18898
- const EXPIRED_TOKEN = "expired_token";
18899
- const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
18900
- const SLOW_DOWN = "slow_down";
18901
- const PENDING_SIGNUP_FILE = "signup.json";
18902
- const DEFAULT_SIGNUP_COMMAND_COPY = {
18903
- actionNoun: "signup",
18904
- actionGerund: "creating a new account",
18905
- confirmCommand: (email) => `signup confirm ${email} <code>`,
18906
- resendCommand: (email) => `signup resend ${email}`,
18907
- startCommand: (email) => `signup ${email}`
18908
- };
18909
- function cliError$1(message) {
18910
- return new Errors.CLIError(message, { exit: 1 });
18911
- }
18912
- function unwrapData(value) {
18913
- return value?.data ?? null;
18914
- }
18915
- function isRecord(value) {
18916
- return value !== null && typeof value === "object" && !Array.isArray(value);
18917
- }
18918
- function normalizeEmail(email) {
18919
- return email.trim().toLowerCase();
18920
- }
18921
- function pendingSignupFromJson(value) {
18922
- if (!isRecord(value)) return null;
18923
- 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;
18924
- return {
18925
- api_base_url_1: value.api_base_url_1,
18926
- created_at: value.created_at,
18927
- email: value.email,
18928
- expires_at: value.expires_at,
18929
- expires_in: value.expires_in,
18930
- resend_after: value.resend_after,
18931
- signup_token: value.signup_token,
18932
- verification_code_length: value.verification_code_length
18933
- };
18934
- }
18935
- function pendingSignupPath(configDir) {
18936
- return join(configDir, PENDING_SIGNUP_FILE);
18937
- }
18938
- function deletePendingAgentSignup(configDir) {
18939
- rmSync(pendingSignupPath(configDir), { force: true });
18940
- }
18941
- function pendingSignupFromStart(start, apiBaseUrl1) {
18942
- return {
18943
- ...start,
18944
- api_base_url_1: apiBaseUrl1,
18945
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
18946
- expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
18947
- };
18948
- }
18949
- function savePendingAgentSignup(configDir, start, apiBaseUrl1) {
18950
- mkdirSync(configDir, {
18951
- mode: 448,
18952
- recursive: true
18953
- });
18954
- const pending = pendingSignupFromStart(start, apiBaseUrl1);
18955
- const path = pendingSignupPath(configDir);
18956
- const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
18957
- try {
18958
- writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
18959
- chmodSync(tempPath, 384);
18960
- renameSync(tempPath, path);
18961
- chmodSync(path, 384);
18962
- return pending;
18963
- } catch (error) {
18964
- rmSync(tempPath, { force: true });
18965
- throw error;
18966
- }
18967
- }
18968
- function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18969
- const path = pendingSignupPath(configDir);
18970
- let contents;
18971
- try {
18972
- contents = readFileSync(path, "utf8");
18973
- } catch (error) {
18974
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
18775
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
18975
18776
  throw error;
18976
18777
  }
18977
18778
  let pending;
@@ -18997,8 +18798,8 @@ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18997
18798
  function requirePendingSignupForEmail(params) {
18998
18799
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
18999
18800
  const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
19000
- if (!pending) throw cliError$1(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
19001
- 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.`);
19002
18803
  return pending;
19003
18804
  }
19004
18805
  function retryAfterSeconds(result) {
@@ -19039,7 +18840,7 @@ async function confirmTerms() {
19039
18840
  process$1.stderr.write(" https://primitive.dev/terms\n");
19040
18841
  process$1.stderr.write(" https://primitive.dev/privacy\n");
19041
18842
  const answer = (await promptRequired("Type 'yes' to continue: ")).toLowerCase();
19042
- 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.");
19043
18844
  }
19044
18845
  async function checkExistingCredentials(params) {
19045
18846
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
@@ -19070,9 +18871,9 @@ async function checkExistingCredentials(params) {
19070
18871
  }
19071
18872
  if (existingStatus.status === "blocked") {
19072
18873
  writeErrorWithHints(existingStatus.payload);
19073
- throw cliError$1(existingStatus.message);
18874
+ throw cliError$2(existingStatus.message);
19074
18875
  }
19075
- 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}.`);
19076
18877
  }
19077
18878
  function saveSignupCredentials(params) {
19078
18879
  saveCliCredentials(params.configDir, {
@@ -19106,7 +18907,7 @@ async function startSignup(params) {
19106
18907
  started: false
19107
18908
  };
19108
18909
  }
19109
- 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.`);
19110
18911
  }
19111
18912
  if (params.flags.force) deletePendingAgentSignup(params.configDir);
19112
18913
  const promptRequiredFn = params.deps.promptRequired ?? promptRequired;
@@ -19126,10 +18927,10 @@ async function startSignup(params) {
19126
18927
  });
19127
18928
  if (started.error) {
19128
18929
  writeErrorWithHints(extractErrorPayload(started.error));
19129
- throw cliError$1("Could not start Primitive agent signup.");
18930
+ throw cliError$2("Could not start Primitive agent signup.");
19130
18931
  }
19131
- const startResult = unwrapData(started.data);
19132
- 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.");
19133
18934
  return {
19134
18935
  pending: savePendingAgentSignup(params.configDir, startResult, params.apiBaseUrl1),
19135
18936
  started: true
@@ -19142,7 +18943,7 @@ async function resendVerificationCode(params) {
19142
18943
  responseStyle: "fields"
19143
18944
  });
19144
18945
  if (resent.data) {
19145
- const resend = unwrapData(resent.data);
18946
+ const resend = unwrapData$1(resent.data);
19146
18947
  const next = resend ? {
19147
18948
  email: resend.email,
19148
18949
  expires_in: resend.expires_in,
@@ -19167,7 +18968,7 @@ async function resendVerificationCode(params) {
19167
18968
  }
19168
18969
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(params.configDir);
19169
18970
  writeErrorWithHints(payload);
19170
- throw cliError$1("Could not resend Primitive agent signup verification email.");
18971
+ throw cliError$2("Could not resend Primitive agent signup verification email.");
19171
18972
  }
19172
18973
  async function runSignupStartWithCredentialLock(params) {
19173
18974
  const { configDir, flags } = params;
@@ -19227,8 +19028,8 @@ async function runSignupConfirmWithCredentialLock(params) {
19227
19028
  responseStyle: "fields"
19228
19029
  });
19229
19030
  if (verified.data) {
19230
- const signup = unwrapData(verified.data);
19231
- 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.");
19232
19033
  saveSignupCredentials({
19233
19034
  apiBaseUrl1,
19234
19035
  configDir,
@@ -19242,10 +19043,10 @@ async function runSignupConfirmWithCredentialLock(params) {
19242
19043
  }
19243
19044
  const payload = extractErrorPayload(verified.error);
19244
19045
  const code = extractErrorCode(payload);
19245
- 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)}.`);
19246
19047
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(configDir);
19247
19048
  writeErrorWithHints(payload);
19248
- throw cliError$1("Primitive agent signup failed while verifying the account.");
19049
+ throw cliError$2("Primitive agent signup failed while verifying the account.");
19249
19050
  }
19250
19051
  async function runSignupResendWithCredentialLock(params) {
19251
19052
  const deps = params.deps ?? {};
@@ -19332,40 +19133,268 @@ async function runSignupInteractiveWithCredentialLock(params) {
19332
19133
  }
19333
19134
  }
19334
19135
  }
19335
- function commonStartFlags() {
19336
- return {
19337
- "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 = {
19338
19382
  "api-base-url-1": Flags.string({
19339
19383
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19340
19384
  env: "PRIMITIVE_API_BASE_URL_1",
19341
19385
  hidden: true
19342
19386
  }),
19343
- "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19344
19387
  force: Flags.boolean({
19345
19388
  char: "f",
19346
- description: "Replace saved credentials or pending signup state when needed"
19347
- }),
19348
- "signup-code": Flags.string({
19349
- description: "Signup code required to create an account",
19350
- 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"
19351
19390
  })
19352
19391
  };
19353
- }
19354
- var SignupCommand = class SignupCommand extends Command {
19355
- static args = { email: Args.string({
19356
- description: "Email address to sign up",
19357
- required: false
19358
- }) };
19359
- static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
19360
- static summary = "Start account signup";
19361
- static examples = [
19362
- "<%= config.bin %> signup user@example.com",
19363
- "<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
19364
- "<%= config.bin %> signup confirm user@example.com 123456"
19365
- ];
19366
- static flags = commonStartFlags();
19367
19392
  async run() {
19368
- 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
+ }
19369
19398
  let releaseCredentialsLock;
19370
19399
  try {
19371
19400
  releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
@@ -19373,9 +19402,8 @@ var SignupCommand = class SignupCommand extends Command {
19373
19402
  throw cliError$1(error instanceof Error ? error.message : String(error));
19374
19403
  }
19375
19404
  try {
19376
- await runSignupStartWithCredentialLock({
19405
+ await runLogoutWithCredentialLock({
19377
19406
  configDir: this.config.configDir,
19378
- email: args.email,
19379
19407
  flags
19380
19408
  });
19381
19409
  } finally {
@@ -19383,105 +19411,344 @@ var SignupCommand = class SignupCommand extends Command {
19383
19411
  }
19384
19412
  }
19385
19413
  };
19386
- var SignupConfirmCommand = class SignupConfirmCommand extends Command {
19387
- static args = {
19388
- email: Args.string({
19389
- description: "Email address used to start signup",
19390
- 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"
19391
19531
  }),
19392
- code: Args.string({
19393
- 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.",
19394
19544
  required: true
19395
- })
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 })
19396
19555
  };
19397
- static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
19398
- static summary = "Confirm account signup";
19399
- 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
+ ];
19400
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
+ }),
19401
19664
  "api-base-url-1": Flags.string({
19402
19665
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19403
19666
  env: "PRIMITIVE_API_BASE_URL_1",
19404
19667
  hidden: true
19405
19668
  }),
19406
- force: Flags.boolean({
19407
- char: "f",
19408
- 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
19409
19673
  }),
19410
- "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 })
19411
19694
  };
19412
19695
  async run() {
19413
- const { args, flags } = await this.parse(SignupConfirmCommand);
19414
- let releaseCredentialsLock;
19415
- try {
19416
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19417
- } catch (error) {
19418
- throw cliError$1(error instanceof Error ? error.message : String(error));
19419
- }
19420
- try {
19421
- await runSignupConfirmWithCredentialLock({
19422
- code: args.code,
19423
- configDir: this.config.configDir,
19424
- email: args.email,
19425
- flags
19426
- });
19427
- } finally {
19428
- releaseCredentialsLock();
19429
- }
19430
- }
19431
- };
19432
- var SignupResendCommand = class SignupResendCommand extends Command {
19433
- static args = { email: Args.string({
19434
- description: "Email address used to start signup",
19435
- required: true
19436
- }) };
19437
- static description = "Resend the verification code for a pending signup.";
19438
- static summary = "Resend signup verification code";
19439
- static examples = ["<%= config.bin %> signup resend user@example.com"];
19440
- static flags = { "api-base-url-1": Flags.string({
19441
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19442
- env: "PRIMITIVE_API_BASE_URL_1",
19443
- hidden: true
19444
- }) };
19445
- async run() {
19446
- const { args, flags } = await this.parse(SignupResendCommand);
19447
- let releaseCredentialsLock;
19448
- try {
19449
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19450
- } catch (error) {
19451
- throw cliError$1(error instanceof Error ? error.message : String(error));
19452
- }
19453
- try {
19454
- await runSignupResendWithCredentialLock({
19455
- configDir: this.config.configDir,
19456
- email: args.email,
19457
- 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
19458
19713
  });
19459
- } finally {
19460
- releaseCredentialsLock();
19461
- }
19462
- }
19463
- };
19464
- var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
19465
- static description = "Run the full signup flow in one interactive terminal session.";
19466
- static summary = "Run interactive account signup";
19467
- static examples = ["<%= config.bin %> signup interactive"];
19468
- static flags = commonStartFlags();
19469
- async run() {
19470
- const { flags } = await this.parse(SignupInteractiveCommand);
19471
- let releaseCredentialsLock;
19472
- try {
19473
- releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
19474
- } catch (error) {
19475
- throw cliError$1(error instanceof Error ? error.message : String(error));
19476
- }
19477
- try {
19478
- await runSignupInteractiveWithCredentialLock({
19479
- configDir: this.config.configDir,
19480
- 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"
19481
19735
  });
19482
- } finally {
19483
- releaseCredentialsLock();
19484
- }
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
+ });
19485
19752
  }
19486
19753
  };
19487
19754
  //#endregion
@@ -19496,6 +19763,34 @@ const SIGNIN_OTP_COPY = {
19496
19763
  resendCommand: (email) => `signin otp resend ${email}`,
19497
19764
  startCommand: (email) => `signin otp ${email}`
19498
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
+ };
19499
19794
  function acquireCredentialsLock(configDir) {
19500
19795
  try {
19501
19796
  return acquireCliCredentialsLock(configDir);
@@ -19514,31 +19809,91 @@ function commonOtpStartFlags() {
19514
19809
  "device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
19515
19810
  force: Flags.boolean({
19516
19811
  char: "f",
19517
- description: "Replace saved credentials or pending sign-in state when needed"
19812
+ description: "Replace saved credentials or pending email-code auth state when needed"
19518
19813
  }),
19519
19814
  "signup-code": Flags.string({
19520
- description: "Signup code required to start OTP sign-in",
19815
+ description: "Signup code required to start email-code sign-in",
19521
19816
  env: "PRIMITIVE_SIGNUP_CODE"
19522
19817
  })
19523
19818
  };
19524
19819
  }
19525
- var SigninCommand = class extends LoginCommand {
19526
- 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.
19527
19826
 
19528
- 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>\`.`;
19529
19828
  static summary = "Sign in to an existing account";
19530
19829
  static examples = [
19531
19830
  "<%= config.bin %> signin",
19532
19831
  "<%= config.bin %> signin browser",
19533
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",
19534
19835
  "<%= config.bin %> signin otp user@example.com --signup-code invite-code --accept-terms",
19535
19836
  "<%= config.bin %> signin otp confirm user@example.com 123456"
19536
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
+ }
19537
19866
  retryCommand() {
19538
19867
  return "signin";
19539
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
+ }
19540
19895
  };
19541
- var SigninBrowserCommand = class extends LoginCommand {
19896
+ var SigninBrowserCommand = class extends LoginCommand$1 {
19542
19897
  static description = "Sign in to an existing Primitive account by opening Primitive in your browser and saving an org-scoped OAuth session locally.";
19543
19898
  static summary = "Sign in with browser approval";
19544
19899
  static examples = [
@@ -19551,7 +19906,20 @@ var SigninBrowserCommand = class extends LoginCommand {
19551
19906
  return "signin browser";
19552
19907
  }
19553
19908
  };
19554
- 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 {
19555
19923
  static args = { email: Args.string({
19556
19924
  description: "Email address to sign in with",
19557
19925
  required: false
@@ -19561,12 +19929,13 @@ var SigninOtpCommand = class SigninOtpCommand extends Command {
19561
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"];
19562
19930
  static flags = commonOtpStartFlags();
19563
19931
  async run() {
19564
- const { args, flags } = await this.parse(SigninOtpCommand);
19932
+ const commandClass = this.constructor;
19933
+ const { args, flags } = await this.parse(commandClass);
19565
19934
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19566
19935
  try {
19567
19936
  await runSignupStartWithCredentialLock({
19568
19937
  configDir: this.config.configDir,
19569
- copy: SIGNIN_OTP_COPY,
19938
+ copy: this.emailCodeCopy(),
19570
19939
  email: args.email,
19571
19940
  flags
19572
19941
  });
@@ -19574,8 +19943,31 @@ var SigninOtpCommand = class SigninOtpCommand extends Command {
19574
19943
  releaseCredentialsLock();
19575
19944
  }
19576
19945
  }
19946
+ emailCodeCopy() {
19947
+ return SIGNIN_OTP_COPY;
19948
+ }
19577
19949
  };
19578
- 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 {
19579
19971
  static args = {
19580
19972
  email: Args.string({
19581
19973
  description: "Email address used to start OTP sign-in",
@@ -19602,13 +19994,14 @@ var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19602
19994
  "org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
19603
19995
  };
19604
19996
  async run() {
19605
- const { args, flags } = await this.parse(SigninOtpConfirmCommand);
19997
+ const commandClass = this.constructor;
19998
+ const { args, flags } = await this.parse(commandClass);
19606
19999
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19607
20000
  try {
19608
20001
  await runSignupConfirmWithCredentialLock({
19609
20002
  code: args.code,
19610
20003
  configDir: this.config.configDir,
19611
- copy: SIGNIN_OTP_COPY,
20004
+ copy: this.emailCodeCopy(),
19612
20005
  email: args.email,
19613
20006
  flags
19614
20007
  });
@@ -19616,8 +20009,51 @@ var SigninOtpConfirmCommand = class SigninOtpConfirmCommand extends Command {
19616
20009
  releaseCredentialsLock();
19617
20010
  }
19618
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
+ }
19619
20025
  };
19620
- 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 {
19621
20057
  static args = { email: Args.string({
19622
20058
  description: "Email address used to start OTP sign-in",
19623
20059
  required: true
@@ -19631,12 +20067,13 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
19631
20067
  hidden: true
19632
20068
  }) };
19633
20069
  async run() {
19634
- const { args, flags } = await this.parse(SigninOtpResendCommand);
20070
+ const commandClass = this.constructor;
20071
+ const { args, flags } = await this.parse(commandClass);
19635
20072
  const releaseCredentialsLock = acquireCredentialsLock(this.config.configDir);
19636
20073
  try {
19637
20074
  await runSignupResendWithCredentialLock({
19638
20075
  configDir: this.config.configDir,
19639
- copy: SIGNIN_OTP_COPY,
20076
+ copy: this.emailCodeCopy(),
19640
20077
  email: args.email,
19641
20078
  flags
19642
20079
  });
@@ -19644,6 +20081,49 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
19644
20081
  releaseCredentialsLock();
19645
20082
  }
19646
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
+ }
19647
20127
  };
19648
20128
  //#endregion
19649
20129
  //#region src/oclif/commands/whoami.ts
@@ -19990,11 +20470,22 @@ const COMMANDS = {
19990
20470
  reply: ReplyCommand,
19991
20471
  chat: ChatCommand,
19992
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,
19993
20482
  signin: SigninCommand,
19994
20483
  "signin:browser": SigninBrowserCommand,
20484
+ "signin:confirm": SigninConfirmCommand,
19995
20485
  "signin:otp": SigninOtpCommand,
19996
20486
  "signin:otp:confirm": SigninOtpConfirmCommand,
19997
20487
  "signin:otp:resend": SigninOtpResendCommand,
20488
+ "signin:resend": SigninResendCommand,
19998
20489
  signup: SignupCommand,
19999
20490
  "signup:confirm": SignupConfirmCommand,
20000
20491
  "signup:interactive": SignupInteractiveCommand,