primcli 1.40.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  import { b as __exportAll } from "./repeats-CoK0tpCK-Bx7nmyRi.js";
2
- import { m as createConfig, p as createClient } from "./connected-agent-profile-8tpOMdq3.js";
2
+ import { C as resolveCliAuth, G as createConfig, S as normalizeApiBaseUrl, T as saveSignupCredentials, U as PrimitiveApiClient, V as requireDefaultLoginProfile, W as createClient, _ as deleteCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveCliCredentials, x as loadCliCredentials, y as detectPrimitiveKeyEnvMisname } from "./cli-config-BoZ1lMUQ.js";
3
+ import { Command, Errors, Flags } from "@oclif/core";
4
+ import { readFileSync, writeFileSync } from "node:fs";
5
+ import { createHash } from "node:crypto";
3
6
  //#region ../packages/api-core/src/api/client.gen.ts
4
7
  const client = createClient(createConfig({ baseUrl: "https://api.primitive.dev/v1" }));
5
8
  //#endregion
@@ -1018,6 +1021,13 @@ const discardEmailContent = (options) => (options.client ?? client).post({
1018
1021
  * merged here; that normalization is model-specific and left to the
1019
1022
  * caller.
1020
1023
  *
1024
+ * Pass `since` for an incremental read. `since=start` returns the
1025
+ * full conversation plus a `cursor`. Sending that `cursor` back as
1026
+ * `since` returns only the messages created or changed after it,
1027
+ * oldest first, with the same per-message shape, and a new
1028
+ * `cursor`. Without `since` the response carries no `cursor` and
1029
+ * no per-message `status`.
1030
+ *
1021
1031
  */
1022
1032
  const getConversation = (options) => (options.client ?? client).get({
1023
1033
  security: [{
@@ -3278,4 +3288,1820 @@ const provisionMemberAddress = (options) => (options.client ?? client).put({
3278
3288
  }
3279
3289
  });
3280
3290
  //#endregion
3281
- export { putAgentContactPolicy as $, getRepeatingSend as A, listEmails as B, getEmail as C, updateFunction as Ct, getInboxStatus as D, updateWakeSchedule as Dt, getFunctionTestRunTrace as E, updateWakeAuthorization as Et, listAgentNetworks as F, listRoutes as G, listFunctionLogs as H, listContacts as I, listWakeDispatches as J, listSentEmails as K, listDefaultNetworkAgents as L, getThread as M, getWakeSchedule as N, getMemory as O, verifyAgentClaim as Ot, listAgentContacts as P, putAgentContact as Q, listDefaultNetworkMembers as R, getDefaultNetworkAgent as S, updateDefaultNetworkMember as St, getFunctionRouting as T, updateRoute as Tt, listFunctions as U, listEndpoints as V, listRepeatingSends as W, pollCliLogin as X, listWakeSchedules as Y, pullWebhookEvent as Z, getAccount as _, startAgentSignup as _t, createEndpoint as a, resendAgentSignupVerification as at, getConversation as b, testFunction as bt, createWakeAuthorization as c, searchEmails as ct, deleteContact as d, sendEmail as dt, putContact as et, deleteMemory as f, setFunctionRoute as ft, downloadEmailAttachmentPart as g, startAgentClaim as gt, deleteWakeSchedule as h, simulateRoute as ht, completeWebhookEvent as i, replyToEmail as it, getSentEmail as j, getOrgRoutingTopology as k, verifyAgentSignup as kt, createWakeSchedule as l, searchMemories as lt, deleteWakeAuthorization as m, setMemory as mt, checkDefaultNetworkContactAdmission as n, removeDefaultNetworkMember as nt, createFunction as o, runWakeSchedule as ot, deleteRoute as p, setFunctionSecret as pt, listWakeAuthorizations as q, cliLogout as r, reorderRoutes as rt, createRoute as s, sdk_gen_exports as st, addDefaultNetworkMember as t, redeemCreditCode as tt, deleteAgentContact as u, semanticSearch as ut, getAgentContactPolicy as v, startCliLogin as vt, getFunction as w, updateRepeatingSend as wt, getCreditBalance as x, unsetFunctionRoute as xt, getContact as y, stopRepeatFromEmail as yt, listDomains as z };
3291
+ //#region src/oclif/api-client.ts
3292
+ const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
3293
+ const OAUTH_REFRESH_SKEW_MS = 60 * 1e3;
3294
+ const SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE = "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
3295
+ function mergeHeaders(...headers) {
3296
+ const merged = {};
3297
+ for (const headerSet of headers) {
3298
+ if (!headerSet) continue;
3299
+ for (const [name, value] of Object.entries(headerSet)) merged[name] = value;
3300
+ }
3301
+ return Object.keys(merged).length > 0 ? merged : void 0;
3302
+ }
3303
+ function parseHeadersJson(raw) {
3304
+ let parsed;
3305
+ try {
3306
+ parsed = JSON.parse(raw);
3307
+ } catch (error) {
3308
+ const detail = error instanceof Error ? error.message : String(error);
3309
+ throw new Errors.CLIError(`${API_HEADERS_ENV} must be valid JSON object syntax: ${detail}`, { exit: 1 });
3310
+ }
3311
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Errors.CLIError(`${API_HEADERS_ENV} must be a JSON object.`, { exit: 1 });
3312
+ const headers = {};
3313
+ for (const [rawName, rawValue] of Object.entries(parsed)) {
3314
+ const name = validateCliHeaderName(rawName);
3315
+ if (typeof rawValue !== "string") throw new Errors.CLIError(`${API_HEADERS_ENV}.${name} must be a string.`, { exit: 1 });
3316
+ headers[name] = validateCliHeaderValue(rawValue, name);
3317
+ }
3318
+ return headers;
3319
+ }
3320
+ function cliApiHeadersFromEnv(env = process.env) {
3321
+ const rawGenericHeaders = env[API_HEADERS_ENV]?.trim();
3322
+ return rawGenericHeaders ? parseHeadersJson(rawGenericHeaders) : void 0;
3323
+ }
3324
+ function resolveCliApiRequestConfig(params) {
3325
+ if ((params.env ?? process.env)["PRIMITIVE_AGENT_PROFILE"]?.trim()) {
3326
+ const auth = resolveCliAuth(params);
3327
+ return {
3328
+ apiBaseUrl: auth.apiBaseUrl,
3329
+ resolvedApiBaseUrl: auth.apiBaseUrl,
3330
+ baseUrlOverridden: params.apiBaseUrl !== void 0,
3331
+ environmentName: null
3332
+ };
3333
+ }
3334
+ const currentEnvironment = resolveConfigEnvironment(loadCliConfig(params.configDir));
3335
+ const configuredApiBaseUrl = currentEnvironment?.config.api_base_url;
3336
+ if (currentEnvironment !== null && currentEnvironment.name !== "default" && params.apiBaseUrl === void 0 && configuredApiBaseUrl === void 0) throw new Errors.CLIError(`The active Primitive CLI environment \`${currentEnvironment.name}\` does not specify an api_base_url. Set one with \`primitive config set --environment ${currentEnvironment.name} --api-base-url https://...\`, or switch to a different environment with \`primitive config use <name>\`. Refusing to fall back to the production default for a non-default environment.`, { exit: 1 });
3337
+ const apiBaseUrl = params.apiBaseUrl !== void 0 ? normalizeApiBaseUrl(params.apiBaseUrl) : configuredApiBaseUrl;
3338
+ return {
3339
+ apiBaseUrl,
3340
+ baseUrlOverridden: apiBaseUrl !== void 0,
3341
+ environmentName: currentEnvironment?.name ?? null,
3342
+ headers: mergeHeaders(currentEnvironment?.config.headers, cliApiHeadersFromEnv(params.env)),
3343
+ resolvedApiBaseUrl: normalizeApiBaseUrl(apiBaseUrl)
3344
+ };
3345
+ }
3346
+ function createCliApiClient(params) {
3347
+ const requestConfig = resolveCliApiRequestConfig(params);
3348
+ return {
3349
+ apiClient: new PrimitiveApiClient({
3350
+ apiKey: params.apiKey,
3351
+ apiBaseUrl: requestConfig.resolvedApiBaseUrl,
3352
+ headers: requestConfig.headers
3353
+ }),
3354
+ requestConfig
3355
+ };
3356
+ }
3357
+ function oauthTokenEndpoint(apiBaseUrl) {
3358
+ const url = new URL(apiBaseUrl);
3359
+ url.pathname = "/oauth/token";
3360
+ url.search = "";
3361
+ url.hash = "";
3362
+ return url.toString();
3363
+ }
3364
+ function shouldRefresh(credentials, now) {
3365
+ const expiresAt = Date.parse(credentials.expires_at);
3366
+ if (!Number.isFinite(expiresAt)) return true;
3367
+ return expiresAt <= now() + OAUTH_REFRESH_SKEW_MS;
3368
+ }
3369
+ function isOAuthRefreshSuccess(value) {
3370
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3371
+ const row = value;
3372
+ return typeof row.access_token === "string" && typeof row.refresh_token === "string" && row.token_type === "Bearer" && typeof row.expires_in === "number" && Number.isFinite(row.expires_in) && row.expires_in > 0;
3373
+ }
3374
+ function oauthErrorCode(value) {
3375
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
3376
+ const raw = value.error;
3377
+ return typeof raw === "string" ? raw : null;
3378
+ }
3379
+ function oauthErrorDescription(value) {
3380
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
3381
+ const raw = value.error_description;
3382
+ return typeof raw === "string" && raw.trim() ? raw : null;
3383
+ }
3384
+ async function refreshStoredCliCredentials(params) {
3385
+ const now = params.now ?? Date.now;
3386
+ if (!shouldRefresh(params.credentials, now)) return params.credentials;
3387
+ let releaseLock;
3388
+ try {
3389
+ if (!params.credentialsLockHeld) try {
3390
+ releaseLock = acquireCliCredentialsLock(params.configDir);
3391
+ } catch (error) {
3392
+ const detail = error instanceof Error ? error.message : String(error);
3393
+ throw new Errors.CLIError(detail, { exit: 1 });
3394
+ }
3395
+ const current = loadCliCredentials(params.configDir);
3396
+ if (!current) throw new Errors.CLIError("Saved Primitive CLI OAuth session is no longer available. Run `primitive signin` to authenticate again.", { exit: 1 });
3397
+ if (!shouldRefresh(current, now)) return current;
3398
+ const fetchImpl = params.fetch ?? fetch;
3399
+ const body = new URLSearchParams({
3400
+ client_id: current.oauth_client_id,
3401
+ grant_type: "refresh_token",
3402
+ refresh_token: current.refresh_token
3403
+ });
3404
+ let response;
3405
+ try {
3406
+ response = await fetchImpl(oauthTokenEndpoint(params.apiBaseUrl), {
3407
+ body,
3408
+ headers: {
3409
+ ...params.headers ?? {},
3410
+ "content-type": "application/x-www-form-urlencoded"
3411
+ },
3412
+ method: "POST"
3413
+ });
3414
+ } catch (error) {
3415
+ const detail = error instanceof Error ? error.message : String(error);
3416
+ throw new Errors.CLIError(`Could not refresh saved Primitive CLI OAuth credentials: ${detail}`, { exit: 1 });
3417
+ }
3418
+ const payload = await response.json().catch(() => null);
3419
+ if (!response.ok) {
3420
+ const code = oauthErrorCode(payload);
3421
+ const description = oauthErrorDescription(payload);
3422
+ if (code === "invalid_grant") {
3423
+ deleteCliCredentials(params.configDir);
3424
+ throw new Errors.CLIError(SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE, { exit: 1 });
3425
+ }
3426
+ throw new Errors.CLIError(`Could not refresh saved Primitive CLI OAuth credentials${description ? `: ${description}` : "."}`, { exit: 1 });
3427
+ }
3428
+ if (!isOAuthRefreshSuccess(payload)) throw new Errors.CLIError("Primitive OAuth token endpoint returned an unexpected refresh response.", { exit: 1 });
3429
+ const next = {
3430
+ ...current,
3431
+ access_token: payload.access_token,
3432
+ expires_at: cliAccessTokenExpiresAt(payload.expires_in, now),
3433
+ refresh_token: payload.refresh_token,
3434
+ token_type: payload.token_type
3435
+ };
3436
+ saveCliCredentials(params.configDir, next);
3437
+ return next;
3438
+ } finally {
3439
+ releaseLock?.();
3440
+ }
3441
+ }
3442
+ async function createAuthenticatedCliApiClient(params) {
3443
+ const requestConfig = resolveCliApiRequestConfig(params);
3444
+ let auth = resolveCliAuth({
3445
+ apiKey: params.apiKey,
3446
+ apiBaseUrl: requestConfig.apiBaseUrl,
3447
+ configDir: params.configDir,
3448
+ env: params.env
3449
+ });
3450
+ if (auth.apiKey === void 0) {
3451
+ const hint = detectPrimitiveKeyEnvMisname(params.env ?? process.env);
3452
+ if (hint) process.stderr.write(`${hint}\n`);
3453
+ }
3454
+ if (auth.source === "stored" && auth.credentials) {
3455
+ const refreshed = await refreshStoredCliCredentials({
3456
+ apiBaseUrl: auth.apiBaseUrl,
3457
+ configDir: params.configDir,
3458
+ credentials: auth.credentials,
3459
+ credentialsLockHeld: params.credentialsLockHeld,
3460
+ fetch: params.fetch,
3461
+ headers: requestConfig.headers,
3462
+ now: params.now
3463
+ });
3464
+ auth = {
3465
+ ...auth,
3466
+ apiKey: refreshed.access_token,
3467
+ credentials: refreshed
3468
+ };
3469
+ }
3470
+ return {
3471
+ apiClient: new PrimitiveApiClient({
3472
+ apiKey: auth.apiKey,
3473
+ apiBaseUrl: auth.apiBaseUrl,
3474
+ headers: auth.connectedAgent ? void 0 : requestConfig.headers
3475
+ }),
3476
+ auth,
3477
+ baseUrlOverridden: requestConfig.baseUrlOverridden,
3478
+ requestConfig
3479
+ };
3480
+ }
3481
+ //#endregion
3482
+ //#region src/oclif/attachment-wait.ts
3483
+ /** Generated download commands that wait for attachment content by default. */
3484
+ const ATTACHMENT_WAIT_OPERATIONS = new Set([
3485
+ "downloadAttachments",
3486
+ "downloadEmailAttachmentPart",
3487
+ "downloadSentAttachmentPart"
3488
+ ]);
3489
+ /** Longest a download command waits for attachment content, in total. */
3490
+ const ATTACHMENT_WAIT_MAX_MS = 6e4;
3491
+ const INITIAL_DELAY_MS = 1e3;
3492
+ const MAX_DELAY_MS = 8e3;
3493
+ const ATTACHMENT_WAIT_FLAG_DESCRIPTION = `Wait and retry while the attachment content is not ready yet (attachment_not_ready), for up to ${ATTACHMENT_WAIT_MAX_MS / 1e3}s. Pass --no-wait to return the error immediately.`;
3494
+ /** Seconds from a Retry-After header, or undefined when absent or not a number of seconds. */
3495
+ function retryAfterMs(response) {
3496
+ const header = response?.headers?.get("retry-after");
3497
+ if (!header || !/^\d+$/.test(header.trim())) return void 0;
3498
+ return Number(header.trim()) * 1e3;
3499
+ }
3500
+ /**
3501
+ * Delay before the next attempt: the server's Retry-After when it gives one,
3502
+ * otherwise exponential backoff from 1s, capped at 8s either way, and never
3503
+ * past the remaining budget.
3504
+ */
3505
+ function nextAttachmentWaitDelay(attempt, remainingMs, serverHintMs) {
3506
+ const backoff = INITIAL_DELAY_MS * 2 ** Math.min(attempt, 10);
3507
+ return Math.max(0, Math.min(Math.min(serverHintMs ?? backoff, MAX_DELAY_MS), remainingMs));
3508
+ }
3509
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3510
+ /**
3511
+ * Run `call` and, while it fails with attachment_not_ready, wait and run it
3512
+ * again until it succeeds, fails differently, or the budget is spent. The last
3513
+ * result is returned either way, so the caller reports it exactly as before.
3514
+ */
3515
+ async function withAttachmentWait(call, options) {
3516
+ const now = options.now ?? Date.now;
3517
+ const sleep = options.sleep ?? defaultSleep;
3518
+ const maxWaitMs = options.maxWaitMs ?? 6e4;
3519
+ const started = now();
3520
+ let result = await call();
3521
+ for (let attempt = 0; options.wait; attempt++) {
3522
+ if (!result.error || options.errorCode(result.error) !== "attachment_not_ready") break;
3523
+ const remaining = maxWaitMs - (now() - started);
3524
+ if (remaining <= 0) break;
3525
+ if (attempt === 0) options.onFirstWait?.();
3526
+ await sleep(nextAttachmentWaitDelay(attempt, remaining, retryAfterMs(result.response)));
3527
+ result = await call();
3528
+ }
3529
+ return result;
3530
+ }
3531
+ //#endregion
3532
+ //#region src/oclif/automated-filter.ts
3533
+ /**
3534
+ * The server-side `automated` verdict on inbound email: `automated`
3535
+ * (boolean) and `automated_reasons` (why, in rule order), decided by
3536
+ * the API when the email arrives, and the `automated=true|false` filter
3537
+ * on GET /emails and GET /emails/search.
3538
+ *
3539
+ * A server that predates it either rejects the `automated` filter (GET
3540
+ * /emails validates its query strictly) or ignores it and returns rows
3541
+ * without the fields (GET /emails/search). Both must fail loudly: a CLI
3542
+ * that read a missing verdict as "not automated" would hand an agent
3543
+ * every bounce and newsletter as mail from a person, and one that fell
3544
+ * back to deciding it locally would re-read all unanswered automated
3545
+ * mail on every call.
3546
+ */
3547
+ const AUTOMATED_FILTER_UNSUPPORTED_CODE = "automated_filter_unsupported";
3548
+ const AUTOMATED_VALUES = ["true", "false"];
3549
+ const AUTOMATED_FLAG_DESCRIPTION = "Only emails whose server-decided `automated` verdict is this value: `false` = sent by a person, `true` = bounces and delivery reports, auto-replies, list and newsletter mail, and mail from the address it was sent to. Fails if the server does not support the automated filter yet.";
3550
+ var AutomatedFilterUnsupportedError = class extends Error {
3551
+ code = AUTOMATED_FILTER_UNSUPPORTED_CODE;
3552
+ constructor(detail) {
3553
+ super(`The server does not support the \`automated\` filter yet: ${detail} Nothing was treated as sent by a person. Upgrade to a server that returns \`automated\` and \`automated_reasons\`, or pass --include-automated.`);
3554
+ this.name = "AutomatedFilterUnsupportedError";
3555
+ }
3556
+ };
3557
+ /** Like AutomatedFilterUnsupportedError, for a probe that failed. */
3558
+ var AutomatedFilterUnverifiedError = class extends AutomatedFilterUnsupportedError {
3559
+ constructor(detail) {
3560
+ super(detail);
3561
+ this.message = `Could not verify that the server supports the \`automated\` filter: ${detail} Nothing was treated as matching. Retry the command.`;
3562
+ this.name = "AutomatedFilterUnverifiedError";
3563
+ this.code = "automated_filter_unverified";
3564
+ }
3565
+ };
3566
+ function hasAutomatedVerdict(row) {
3567
+ if (row === null || typeof row !== "object") return false;
3568
+ const candidate = row;
3569
+ return typeof candidate.automated === "boolean" && Array.isArray(candidate.automated_reasons) && candidate.automated_reasons.every((reason) => typeof reason === "string");
3570
+ }
3571
+ /**
3572
+ * Throw unless every row carries the verdict, and, for each `expected`
3573
+ * value given, unless every row has that value: a server that accepted
3574
+ * the filter but did not apply it must not pass for one that did.
3575
+ */
3576
+ function assertAutomatedVerdict(rows, surface, expected) {
3577
+ const missing = rows.filter((row) => !hasAutomatedVerdict(row)).length;
3578
+ if (missing > 0) throw new AutomatedFilterUnsupportedError(`${surface} returned ${missing} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the \`automated\` and \`automated_reasons\` fields.`);
3579
+ const verdicts = expected === void 0 ? [] : typeof expected === "boolean" ? [expected] : expected;
3580
+ for (const verdict of verdicts) {
3581
+ const wrong = rows.filter((row) => row.automated !== verdict).length;
3582
+ if (wrong > 0) throw new AutomatedFilterUnsupportedError(`${surface} ignored \`automated=${verdict}\` and returned ${wrong} email${wrong === 1 ? "" : "s"} with \`automated=${!verdict}\`.`);
3583
+ }
3584
+ }
3585
+ /**
3586
+ * The verdicts the `automated:` terms in a search DSL query require.
3587
+ * The DSL joins its terms with AND, so every `automated:true|false`
3588
+ * term must hold for every returned row. Quoted text is skipped, so a
3589
+ * phrase that merely contains "automated:false" does not count.
3590
+ */
3591
+ function automatedVerdictsFromQuery(q) {
3592
+ if (typeof q !== "string") return [];
3593
+ const verdicts = [];
3594
+ const tokens = q.match(/(?:[^\s"]|"(?:[^"\\]|\\.)*"?)+/g) ?? [];
3595
+ for (const token of tokens) {
3596
+ const match = /^automated:"?(true|false)"?$/i.exec(token);
3597
+ if (match) verdicts.push(match[1].toLowerCase() === "true");
3598
+ }
3599
+ return verdicts;
3600
+ }
3601
+ /**
3602
+ * Every verdict a list or search call asked for: the `automated`
3603
+ * parameter plus any `automated:` terms in the search query.
3604
+ */
3605
+ function expectedAutomatedVerdicts(automated, q) {
3606
+ return [...automated === void 0 ? [] : [automated], ...automatedVerdictsFromQuery(q)];
3607
+ }
3608
+ /** True when a search DSL query uses the `automated:` term. */
3609
+ function queryUsesAutomated(q) {
3610
+ return typeof q === "string" && /(^|[\s(])-?automated:/i.test(q);
3611
+ }
3612
+ function errorBody$1(payload) {
3613
+ if (payload === null || typeof payload !== "object") return {};
3614
+ const inner = payload.error;
3615
+ return inner !== null && typeof inner === "object" ? inner : payload;
3616
+ }
3617
+ /**
3618
+ * True when an API error is an older server rejecting the `automated`
3619
+ * query parameter or the `automated:` search term as unknown.
3620
+ */
3621
+ function isAutomatedRejectedError(payload) {
3622
+ const body = errorBody$1(payload);
3623
+ if (body.code !== "validation_error") return false;
3624
+ const message = typeof body.message === "string" ? body.message : JSON.stringify(payload);
3625
+ return /automated/i.test(message) && /unrecogni[sz]ed|unknown/i.test(message);
3626
+ }
3627
+ function automatedRejectedError(surface) {
3628
+ return new AutomatedFilterUnsupportedError(`${surface} rejected the \`automated\` filter as unknown.`);
3629
+ }
3630
+ const AUTOMATED_SURFACES = {
3631
+ listEmails: "GET /emails",
3632
+ searchEmails: "GET /emails/search"
3633
+ };
3634
+ /**
3635
+ * For the generated `emails list` / `emails search` commands: the
3636
+ * surface name when this call filtered on `automated` (the parameter or
3637
+ * an `automated:` search term), else null.
3638
+ */
3639
+ function automatedSurfaceForOperation(sdkName, query) {
3640
+ const surface = AUTOMATED_SURFACES[sdkName];
3641
+ if (!surface || !query) return null;
3642
+ const q = typeof query.q === "string" ? query.q : void 0;
3643
+ return query.automated !== void 0 || queryUsesAutomated(q) ? surface : null;
3644
+ }
3645
+ const searchSupportProbes$1 = /* @__PURE__ */ new WeakMap();
3646
+ /**
3647
+ * GET /emails/search ignores unknown parameters, so an empty page for
3648
+ * `automated=...` does not show that the server applied the filter.
3649
+ * Read one unfiltered row: if it lacks the verdict the server does not
3650
+ * support it. No rows at all means the mailbox is empty, so the empty
3651
+ * answer stands. One probe per API client per process.
3652
+ */
3653
+ function ensureSearchReportsAutomated(apiClient) {
3654
+ const key = apiClient;
3655
+ let probe = searchSupportProbes$1.get(key);
3656
+ if (!probe) {
3657
+ probe = (async () => {
3658
+ const result = await searchEmails({
3659
+ client: apiClient.client,
3660
+ query: {
3661
+ limit: 1,
3662
+ include_facets: "false",
3663
+ snippet: "false"
3664
+ },
3665
+ responseStyle: "fields"
3666
+ });
3667
+ if (result.error) throw new AutomatedFilterUnverifiedError("the empty result could not be checked: reading one unfiltered row from GET /emails/search failed.");
3668
+ const rows = result.data?.data;
3669
+ assertAutomatedVerdict(rows ?? [], "GET /emails/search");
3670
+ })();
3671
+ searchSupportProbes$1.set(key, probe);
3672
+ probe.catch(() => {
3673
+ if (searchSupportProbes$1.get(key) === probe) searchSupportProbes$1.delete(key);
3674
+ });
3675
+ }
3676
+ return probe;
3677
+ }
3678
+ //#endregion
3679
+ //#region src/oclif/endpoints-test-redirect.ts
3680
+ async function detectFunctionEndpoint(endpointId, listEndpoints) {
3681
+ let response;
3682
+ try {
3683
+ response = await listEndpoints();
3684
+ } catch {
3685
+ return null;
3686
+ }
3687
+ if (response.error) return null;
3688
+ const rows = response.data?.data;
3689
+ if (!Array.isArray(rows)) return null;
3690
+ for (const row of rows) {
3691
+ if (!row || typeof row !== "object") continue;
3692
+ if (row.id !== endpointId) continue;
3693
+ if (row.kind !== "function") return null;
3694
+ const functionId = row.function_id;
3695
+ if (typeof functionId !== "string" || functionId.length === 0) return null;
3696
+ return {
3697
+ endpointId,
3698
+ functionId
3699
+ };
3700
+ }
3701
+ return null;
3702
+ }
3703
+ function formatFunctionEndpointRedirect(match) {
3704
+ return [
3705
+ "This is a function endpoint. Function endpoints are tested differently. Run:",
3706
+ "",
3707
+ ` primitive functions test --id ${match.functionId}`,
3708
+ "",
3709
+ `(pass the function id, not the endpoint id. endpoint_id=${match.endpointId} function_id=${match.functionId})`
3710
+ ].join("\n");
3711
+ }
3712
+ async function maybeWriteFunctionEndpointRedirect(inputs) {
3713
+ if (inputs.sdkName !== "testEndpoint") return null;
3714
+ if (inputs.errorCode !== "not_found") return null;
3715
+ if (!inputs.endpointId) return null;
3716
+ const match = await detectFunctionEndpoint(inputs.endpointId, inputs.listEndpoints);
3717
+ if (!match) return null;
3718
+ inputs.writeStderr(`${formatFunctionEndpointRedirect(match)}\n`);
3719
+ return match;
3720
+ }
3721
+ //#endregion
3722
+ //#region src/oclif/idempotent-replay-banner.ts
3723
+ /**
3724
+ * If `data` carries `idempotent_replay: true`, write a stderr notice
3725
+ * that the message already went out and nothing new was sent.
3726
+ * Otherwise no-op.
3727
+ *
3728
+ * The notice deliberately gives no advice on how to force a fresh
3729
+ * copy. Whoever sees a replay has already sent this message; telling
3730
+ * them to vary the content is an instruction to double-send.
3731
+ */
3732
+ function writeIdempotentReplayBannerIfReplay(data, options) {
3733
+ if (!isReplay(data)) return;
3734
+ options.write(`${formatAlreadySentNotice(data)}\n`);
3735
+ }
3736
+ function isReplay(data) {
3737
+ if (data === null || typeof data !== "object") return false;
3738
+ return data.idempotent_replay === true;
3739
+ }
3740
+ function formatAlreadySentNotice(sent) {
3741
+ const details = [];
3742
+ if (typeof sent.id === "string" && sent.id) details.push(`sent id ${sent.id}`);
3743
+ const status = typeof sent.status === "string" && sent.status ? sent.status : null;
3744
+ const deliveryStatus = typeof sent.delivery_status === "string" && sent.delivery_status ? sent.delivery_status : null;
3745
+ if (status) details.push(`status ${status}`);
3746
+ if (deliveryStatus && deliveryStatus !== status) details.push(`delivery status ${deliveryStatus}`);
3747
+ return `Already sent: this exact message went out earlier${details.length > 0 ? ` (${details.join(", ")})` : ""}. Nothing new was sent.`;
3748
+ }
3749
+ //#endregion
3750
+ //#region src/oclif/reply-state.ts
3751
+ const AWAITING_VALUES = ["you", "them"];
3752
+ const AWAITING_FLAG_DESCRIPTION = "Only emails whose reply state is this value: `you` = waiting on your reply (the thread's latest message is inbound), `them` = you replied last. Queued and scheduled replies count as replied. Delivered mail only: rejected mail is never returned. Fails if the server does not report reply state yet.";
3753
+ const REPLY_STATE_UNSUPPORTED_CODE = "reply_state_unsupported";
3754
+ /** True when a search DSL query uses the `awaiting:` term. */
3755
+ function queryUsesAwaiting(q) {
3756
+ return typeof q === "string" && /(^|[\s(])-?awaiting:/i.test(q);
3757
+ }
3758
+ var ReplyStateUnsupportedError = class extends Error {
3759
+ code = REPLY_STATE_UNSUPPORTED_CODE;
3760
+ constructor(detail) {
3761
+ super(`The server does not support reply state yet: ${detail} Nothing was treated as awaiting your reply. Upgrade to a server that returns \`awaiting\` and \`reply_count\`, or run without reply-state options.`);
3762
+ this.name = "ReplyStateUnsupportedError";
3763
+ }
3764
+ };
3765
+ /**
3766
+ * The server's reply-state support could not be established (a probe
3767
+ * failed). Reported like ReplyStateUnsupportedError so callers never
3768
+ * read an unverified empty answer as "nothing matched".
3769
+ */
3770
+ var ReplyStateUnverifiedError = class extends ReplyStateUnsupportedError {
3771
+ constructor(detail) {
3772
+ super(detail);
3773
+ this.message = `Could not verify that the server supports reply state: ${detail} Nothing was treated as matching. Retry the command.`;
3774
+ this.name = "ReplyStateUnverifiedError";
3775
+ this.code = "reply_state_unverified";
3776
+ }
3777
+ };
3778
+ function hasReplyState(row) {
3779
+ if (row === null || typeof row !== "object") return false;
3780
+ const candidate = row;
3781
+ return (candidate.awaiting === "you" || candidate.awaiting === "them") && typeof candidate.reply_count === "number" && (candidate.last_replied_at === null || typeof candidate.last_replied_at === "string");
3782
+ }
3783
+ /**
3784
+ * Throw unless every row carries reply state. An empty page proves
3785
+ * nothing either way; callers that asked for reply state follow an
3786
+ * empty search page with ensureSearchReportsReplyState(). `surface` names the
3787
+ * response for the error, e.g. "GET /emails".
3788
+ */
3789
+ function assertReplyState(rows, surface) {
3790
+ const missing = rows.filter((row) => !hasReplyState(row)).length;
3791
+ if (missing > 0) throw new ReplyStateUnsupportedError(`${surface} returned ${missing} of ${rows.length} email${rows.length === 1 ? "" : "s"} without the \`awaiting\` and \`reply_count\` fields.`);
3792
+ }
3793
+ const AWAITING_REJECTED_UNSUPPORTED_CODE = "awaiting_rejected_unsupported";
3794
+ /**
3795
+ * The `awaiting` filter matches delivered mail only: a rejected email
3796
+ * (for example refused over the storage limit) was never delivered, so it
3797
+ * awaits no one. A server that returns one through the filter predates
3798
+ * that, and an agent would be handed mail nobody can answer.
3799
+ */
3800
+ var AwaitingIncludesRejectedError = class extends ReplyStateUnsupportedError {
3801
+ constructor(detail) {
3802
+ super(detail);
3803
+ this.message = `The server's \`awaiting\` filter does not exclude undelivered mail yet: ${detail} Upgrade to a server whose \`awaiting\` filter matches delivered mail only.`;
3804
+ this.name = "AwaitingIncludesRejectedError";
3805
+ this.code = AWAITING_REJECTED_UNSUPPORTED_CODE;
3806
+ }
3807
+ };
3808
+ /**
3809
+ * For rows returned BY an `awaiting` filter (not the unfiltered probe):
3810
+ * every row carries reply state, and none has status `rejected`.
3811
+ */
3812
+ function assertAwaitingFilterRows(rows, surface) {
3813
+ assertReplyState(rows, surface);
3814
+ const rejected = rows.filter((row) => row !== null && typeof row === "object" && row.status === "rejected").length;
3815
+ if (rejected > 0) throw new AwaitingIncludesRejectedError(`${surface} returned ${rejected} email${rejected === 1 ? "" : "s"} with status \`rejected\`.`);
3816
+ }
3817
+ function errorBody(payload) {
3818
+ if (payload === null || typeof payload !== "object") return {};
3819
+ const inner = payload.error;
3820
+ return inner !== null && typeof inner === "object" ? inner : payload;
3821
+ }
3822
+ /**
3823
+ * True when an API error is an older server rejecting the `awaiting`
3824
+ * query parameter or the `awaiting:` search term as unknown. A current
3825
+ * server accepts both, and the CLI only ever sends valid values.
3826
+ */
3827
+ function isAwaitingRejectedError(payload) {
3828
+ const body = errorBody(payload);
3829
+ if (body.code !== "validation_error") return false;
3830
+ const message = typeof body.message === "string" ? body.message : JSON.stringify(payload);
3831
+ return /awaiting/i.test(message) && /unrecogni[sz]ed|unknown/i.test(message);
3832
+ }
3833
+ function awaitingRejectedError(surface) {
3834
+ return new ReplyStateUnsupportedError(`${surface} rejected the \`awaiting\` filter as unknown.`);
3835
+ }
3836
+ /** Short column value for tables: "you", "them", or "-" when absent. */
3837
+ function formatAwaitingCell(row) {
3838
+ if (!hasReplyState(row)) return "-";
3839
+ return row.awaiting;
3840
+ }
3841
+ /** Reply count for tables, or "-" when the server did not report it. */
3842
+ function formatRepliesCell(row) {
3843
+ if (!hasReplyState(row)) return "-";
3844
+ return String(row.reply_count);
3845
+ }
3846
+ const REPLY_STATE_SURFACES = {
3847
+ listEmails: "GET /emails",
3848
+ searchEmails: "GET /emails/search"
3849
+ };
3850
+ /**
3851
+ * For the generated `emails list` / `emails search` commands: the
3852
+ * surface name when this call asked for reply state (an `awaiting`
3853
+ * parameter or an `awaiting:` search term), else null.
3854
+ */
3855
+ function replyStateSurfaceForOperation(sdkName, query) {
3856
+ const surface = REPLY_STATE_SURFACES[sdkName];
3857
+ if (!surface || !query) return null;
3858
+ const q = typeof query.q === "string" ? query.q : void 0;
3859
+ return query.awaiting !== void 0 || queryUsesAwaiting(q) ? surface : null;
3860
+ }
3861
+ const searchSupportProbes = /* @__PURE__ */ new WeakMap();
3862
+ /**
3863
+ * GET /emails/search ignores unknown parameters, so an empty page for
3864
+ * `awaiting=...` does not show that the server applied the filter.
3865
+ * Read one unfiltered row: if it lacks reply state the server does not
3866
+ * support it and this throws ReplyStateUnsupportedError. No rows at all
3867
+ * means the mailbox is empty, so the empty answer stands.
3868
+ */
3869
+ function ensureSearchReportsReplyState(apiClient) {
3870
+ const key = apiClient;
3871
+ let probe = searchSupportProbes.get(key);
3872
+ if (!probe) {
3873
+ probe = (async () => {
3874
+ const result = await searchEmails({
3875
+ client: apiClient.client,
3876
+ query: {
3877
+ limit: 1,
3878
+ include_facets: "false",
3879
+ snippet: "false"
3880
+ },
3881
+ responseStyle: "fields"
3882
+ });
3883
+ if (result.error) throw new ReplyStateUnverifiedError("the empty result could not be checked: reading one unfiltered row from GET /emails/search failed.");
3884
+ const rows = result.data?.data;
3885
+ assertReplyState(rows ?? [], "GET /emails/search");
3886
+ })();
3887
+ searchSupportProbes.set(key, probe);
3888
+ probe.catch(() => {
3889
+ if (searchSupportProbes.get(key) === probe) searchSupportProbes.delete(key);
3890
+ });
3891
+ }
3892
+ return probe;
3893
+ }
3894
+ //#endregion
3895
+ //#region src/oclif/api-command.ts
3896
+ const API_ERROR_CODES = {
3897
+ accessDenied: "access_denied",
3898
+ authorizationPending: "authorization_pending",
3899
+ expiredToken: "expired_token",
3900
+ invalidDeviceCode: "invalid_device_code",
3901
+ notFound: "not_found",
3902
+ slowDown: "slow_down",
3903
+ unauthorized: "unauthorized"
3904
+ };
3905
+ function flagName(parameterName) {
3906
+ return parameterName.replace(/_/g, "-");
3907
+ }
3908
+ function flagDescription(parameter) {
3909
+ return parameter.description ?? parameter.name;
3910
+ }
3911
+ const numberFlag = Flags.custom({ async parse(input, _context, options) {
3912
+ const trimmed = input.trim();
3913
+ if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
3914
+ const value = Number(trimmed);
3915
+ if (!Number.isFinite(value)) throw new Errors.CLIError(`Expected a number but received: ${input}`);
3916
+ if (options.min !== void 0 && value < options.min) throw new Errors.CLIError(`Expected a number greater than or equal to ${options.min} but received: ${input}`);
3917
+ if (options.max !== void 0 && value > options.max) throw new Errors.CLIError(`Expected a number less than or equal to ${options.max} but received: ${input}`);
3918
+ return value;
3919
+ } });
3920
+ function numericFlagOptions(parameter) {
3921
+ return {
3922
+ ...typeof parameter.default === "number" ? { default: parameter.default } : {},
3923
+ ...typeof parameter.maximum === "number" ? { max: parameter.maximum } : {},
3924
+ ...typeof parameter.minimum === "number" ? { min: parameter.minimum } : {}
3925
+ };
3926
+ }
3927
+ function extractBodyFields(schema) {
3928
+ if (!schema || typeof schema !== "object") return [];
3929
+ const properties = schema.properties;
3930
+ if (!properties || typeof properties !== "object") return [];
3931
+ const requiredArr = Array.isArray(schema.required) ? schema.required.filter((k) => typeof k === "string") : [];
3932
+ const required = new Set(requiredArr);
3933
+ const fields = [];
3934
+ for (const [name, raw] of Object.entries(properties)) {
3935
+ const propSchema = raw && typeof raw === "object" ? raw : {};
3936
+ const t = propSchema.type;
3937
+ let displayType = "any";
3938
+ let kind = "complex";
3939
+ if (typeof t === "string") {
3940
+ displayType = t;
3941
+ if (t === "string") kind = "string";
3942
+ else if (t === "integer") kind = "integer";
3943
+ else if (t === "number") kind = "number";
3944
+ else if (t === "boolean") kind = "boolean";
3945
+ else if (t === "array") {
3946
+ const items = propSchema.items;
3947
+ if (items && typeof items === "object") {
3948
+ const itemType = items.type;
3949
+ if (typeof itemType === "string") displayType = `array<${itemType}>`;
3950
+ }
3951
+ kind = "complex";
3952
+ } else kind = "complex";
3953
+ } else if (Array.isArray(t)) {
3954
+ const nonNull = t.filter((s) => s !== "null");
3955
+ if (nonNull.length === 1) {
3956
+ const single = nonNull[0];
3957
+ displayType = `${single}?`;
3958
+ if (single === "string") kind = "string";
3959
+ else if (single === "integer") kind = "integer";
3960
+ else if (single === "number") kind = "number";
3961
+ else if (single === "boolean") kind = "boolean";
3962
+ else kind = "complex";
3963
+ } else {
3964
+ displayType = nonNull.join("|");
3965
+ kind = "complex";
3966
+ }
3967
+ }
3968
+ const description = typeof propSchema.description === "string" ? propSchema.description.split(/\n\s*\n/)[0].replace(/\s*\n\s*/g, " ").trim() : "";
3969
+ const enumRaw = propSchema.enum;
3970
+ const enumValues = kind === "string" && Array.isArray(enumRaw) ? enumRaw.filter((e) => typeof e === "string") : void 0;
3971
+ fields.push({
3972
+ name,
3973
+ description,
3974
+ required: required.has(name),
3975
+ displayType,
3976
+ kind,
3977
+ ...enumValues && enumValues.length > 0 ? { enumValues } : {},
3978
+ ...typeof propSchema.maximum === "number" ? { maximum: propSchema.maximum } : {},
3979
+ ...typeof propSchema.minimum === "number" ? { minimum: propSchema.minimum } : {}
3980
+ });
3981
+ }
3982
+ return fields.sort((a, b) => {
3983
+ if (a.required !== b.required) return a.required ? -1 : 1;
3984
+ return a.name.localeCompare(b.name);
3985
+ });
3986
+ }
3987
+ /**
3988
+ * Render a "Body fields" summary for the per-command help.
3989
+ *
3990
+ * Most scalar fields are exposed as individual `--flag` flags,
3991
+ * which oclif auto-renders in the FLAGS section above. To avoid
3992
+ * duplicating that, the summary here only documents fields that
3993
+ * MUST go through `--raw-body` (complex types: arrays, objects,
3994
+ * mixed-non-nullable). When an operation has only scalars, the
3995
+ * summary is omitted entirely and oclif's FLAGS section is the
3996
+ * full story.
3997
+ *
3998
+ * For operations with mixed scalar and complex fields, we also
3999
+ * include a short header pointing the agent at the flag form so
4000
+ * the natural reading is "use the flags above; --raw-body for
4001
+ * the leftovers below."
4002
+ */
4003
+ function renderRequestSchemaSummary(schema) {
4004
+ const fields = extractBodyFields(schema);
4005
+ if (fields.length === 0) return null;
4006
+ const complex = fields.filter((f) => f.kind === "complex");
4007
+ if (complex.length === 0) return null;
4008
+ const nameWidth = Math.min(24, Math.max(...complex.map((f) => f.name.length)));
4009
+ const descMax = 78;
4010
+ const lines = ["Body fields requiring --raw-body JSON (these are not exposed as flags):"];
4011
+ for (const f of complex) {
4012
+ const marker = f.required ? " *" : " ";
4013
+ const padName = f.name.padEnd(nameWidth);
4014
+ const trimmedDesc = f.description.length > descMax ? `${f.description.slice(0, descMax - 3)}...` : f.description;
4015
+ const desc = trimmedDesc ? ` ${trimmedDesc}` : "";
4016
+ lines.push(`${marker} ${padName} ${f.displayType}${desc}`);
4017
+ }
4018
+ lines.push("(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)");
4019
+ return lines.join("\n");
4020
+ }
4021
+ function flagForParameter(parameter) {
4022
+ const common = {
4023
+ description: flagDescription(parameter),
4024
+ required: parameter.required
4025
+ };
4026
+ if (parameter.type === "boolean") return Flags.boolean(common);
4027
+ if (parameter.type === "integer") return Flags.integer({
4028
+ ...common,
4029
+ ...numericFlagOptions(parameter)
4030
+ });
4031
+ if (parameter.type === "number") return numberFlag({
4032
+ ...common,
4033
+ ...numericFlagOptions(parameter)
4034
+ });
4035
+ if (parameter.enum && parameter.enum.length > 0) return Flags.string({
4036
+ ...common,
4037
+ options: parameter.enum
4038
+ });
4039
+ return Flags.string(common);
4040
+ }
4041
+ function coerceParameterValue(parameter, value) {
4042
+ if (value === void 0) return;
4043
+ if (parameter.type === "number") {
4044
+ if (typeof value === "number") return value;
4045
+ const parsed = Number(value);
4046
+ if (Number.isNaN(parsed)) throw new Errors.CLIError(`Invalid number for --${parameter.name}: ${value}`);
4047
+ return parsed;
4048
+ }
4049
+ if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
4050
+ throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
4051
+ }
4052
+ function cliError(message) {
4053
+ return new Errors.CLIError(message, { exit: 1 });
4054
+ }
4055
+ function parseJson(source, flagLabel) {
4056
+ try {
4057
+ return JSON.parse(source);
4058
+ } catch (error) {
4059
+ throw cliError(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
4060
+ }
4061
+ }
4062
+ function readJsonBody(flags) {
4063
+ const bodyFile = flags["body-file"];
4064
+ const rawBody = flags["raw-body"];
4065
+ if (bodyFile && rawBody) throw cliError("Use either --raw-body or --body-file, not both");
4066
+ if (typeof bodyFile === "string") {
4067
+ let contents;
4068
+ try {
4069
+ contents = readFileSync(bodyFile, "utf8");
4070
+ } catch (error) {
4071
+ throw cliError(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
4072
+ }
4073
+ return parseJson(contents, `--body-file ${bodyFile}`);
4074
+ }
4075
+ if (typeof rawBody === "string") return parseJson(rawBody, "--raw-body");
4076
+ }
4077
+ function readTextFileFlag(path, flagLabel) {
4078
+ try {
4079
+ return readFileSync(path, "utf8");
4080
+ } catch (error) {
4081
+ throw cliError(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
4082
+ }
4083
+ }
4084
+ function extractErrorPayload(raw) {
4085
+ if (raw && typeof raw === "object" && !(raw instanceof Error) && "error" in raw) {
4086
+ const inner = raw.error;
4087
+ if (inner !== null && inner !== void 0) return inner;
4088
+ }
4089
+ return raw;
4090
+ }
4091
+ function extractCauseDetails(cause) {
4092
+ const details = {};
4093
+ let code;
4094
+ if (!cause || typeof cause !== "object") return { details };
4095
+ for (const [key, value] of Object.entries(cause)) if (typeof value === "string" || typeof value === "number") {
4096
+ details[key] = value;
4097
+ if (key === "code" && typeof value === "string") code = value;
4098
+ }
4099
+ return {
4100
+ code,
4101
+ details
4102
+ };
4103
+ }
4104
+ function formatErrorPayload(payload) {
4105
+ if (payload instanceof Error) {
4106
+ const { code, details } = extractCauseDetails(payload.cause);
4107
+ const body = {
4108
+ code: code ?? "client_error",
4109
+ message: payload.message || payload.name || String(payload)
4110
+ };
4111
+ if (Object.keys(details).length > 0) body.cause = details;
4112
+ return JSON.stringify(body, null, 2);
4113
+ }
4114
+ return JSON.stringify(payload, null, 2);
4115
+ }
4116
+ function extractErrorCode(payload) {
4117
+ if (payload instanceof Error) {
4118
+ const { code } = extractCauseDetails(payload.cause);
4119
+ return code;
4120
+ }
4121
+ if (payload && typeof payload === "object") {
4122
+ const inner = payload.error;
4123
+ if (inner && typeof inner === "object" && typeof inner.code === "string") return inner.code;
4124
+ const direct = payload.code;
4125
+ if (typeof direct === "string") return direct;
4126
+ }
4127
+ }
4128
+ const ERROR_CODE_HINTS = { [API_ERROR_CODES.unauthorized]: "Hint: run `primitive signin`, pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify auth is live." };
4129
+ const NETWORK_ERROR_HINTS = {
4130
+ ENETUNREACH: "Hint: the network is unreachable. If you're behind a proxy and set HTTP(S)_PROXY, re-run with NODE_USE_ENV_PROXY=1 (Node 22+ ignores those env vars by default). `primitive doctor` reports the local environment in one shot.",
4131
+ ECONNREFUSED: "Hint: the server refused the connection. Check that your firewall allows egress to *.primitive.dev, that your PRIMITIVE_API_BASE_URL_* overrides (if any) point at a reachable host, and re-run with NODE_USE_ENV_PROXY=1 if you're behind a proxy. `primitive doctor` reports the local environment in one shot.",
4132
+ ETIMEDOUT: "Hint: the connection timed out. Check egress rules and proxy configuration; if you're behind a proxy, re-run with NODE_USE_ENV_PROXY=1 and HTTPS_PROXY set. `primitive doctor` reports the local environment in one shot.",
4133
+ EAI_AGAIN: "Hint: DNS lookup failed. Check /etc/resolv.conf inside containers, and try `curl -v https://api.primitive.dev/v1/account` to confirm the host resolves. `primitive doctor` reports the local environment in one shot."
4134
+ };
4135
+ function writeErrorWithHints(payload) {
4136
+ process.stderr.write(`${formatErrorPayload(payload)}\n`);
4137
+ const code = extractErrorCode(payload);
4138
+ if (!code) return;
4139
+ if (code in ERROR_CODE_HINTS) {
4140
+ const hint = ERROR_CODE_HINTS[code];
4141
+ process.stderr.write(`${hint}\n`);
4142
+ return;
4143
+ }
4144
+ if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
4145
+ }
4146
+ /**
4147
+ * Surface a user-facing hint when a request comes back unauthorized.
4148
+ *
4149
+ * Deliberately does NOT mutate the saved credentials.json. Auto-
4150
+ * deleting on any 401 made transient rejections look like permanent
4151
+ * credential failures and forced unnecessary re-login cycles; we
4152
+ * surface a hint and let the user decide whether to re-authenticate.
4153
+ *
4154
+ * The one legitimate auto-delete case lives in `primitive login`'s
4155
+ * `checkExistingLogin`: the user has explicitly asked to log in,
4156
+ * existing credentials are probed, and if they fail we clean up
4157
+ * before minting a new key. That path calls `deleteCliCredentials`
4158
+ * directly rather than going through this function.
4159
+ */
4160
+ function surfaceUnauthorizedHint(params) {
4161
+ if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return;
4162
+ if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl !== params.auth.credentials.api_base_url) {
4163
+ process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The saved credential is preserved; unset PRIMITIVE_API_BASE_URL, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
4164
+ return;
4165
+ }
4166
+ process.stderr.write("Your saved Primitive CLI OAuth session was rejected. If the command was working a moment ago, please retry; brief retries often clear transient rejections. If it keeps failing, run `primitive logout && primitive signin` to mint a fresh session.\n");
4167
+ }
4168
+ function formatElapsed(ms) {
4169
+ const seconds = ms / 1e3;
4170
+ if (seconds < 60) return `${seconds.toFixed(2)}s`;
4171
+ const minutes = Math.floor(seconds / 60);
4172
+ return `${minutes}m ${(seconds - minutes * 60).toFixed(2)}s`;
4173
+ }
4174
+ async function runWithTiming(enabled, fn) {
4175
+ if (!enabled) return fn();
4176
+ const start = Date.now();
4177
+ try {
4178
+ return await fn();
4179
+ } finally {
4180
+ process.stderr.write(`[time: ${formatElapsed(Date.now() - start)}]\n`);
4181
+ }
4182
+ }
4183
+ const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
4184
+ const API_BASE_URL_FLAG_DESCRIPTION = "Override the API base URL. Internal testing only; not documented to customers.";
4185
+ const RESERVED_FLAG_NAMES = new Set([
4186
+ "api-key",
4187
+ "api-base-url",
4188
+ "raw-body",
4189
+ "body-file",
4190
+ "envelope",
4191
+ "output"
4192
+ ]);
4193
+ function bodyFieldFlag(field, aliases) {
4194
+ const common = {
4195
+ description: field.description || field.name,
4196
+ ...aliases && aliases.length > 0 ? { aliases } : {}
4197
+ };
4198
+ if (field.kind === "boolean") return Flags.boolean({
4199
+ ...common,
4200
+ allowNo: true
4201
+ });
4202
+ if (field.kind === "integer") return Flags.integer({
4203
+ ...common,
4204
+ ...numericFlagOptions(field)
4205
+ });
4206
+ if (field.kind === "number") return numberFlag({
4207
+ ...common,
4208
+ ...numericFlagOptions(field)
4209
+ });
4210
+ if (field.enumValues) return Flags.string({
4211
+ ...common,
4212
+ options: field.enumValues
4213
+ });
4214
+ return Flags.string(common);
4215
+ }
4216
+ function buildFlags(operation) {
4217
+ const flags = {
4218
+ "api-key": Flags.string({
4219
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
4220
+ env: "PRIMITIVE_API_KEY"
4221
+ }),
4222
+ "api-base-url": Flags.string({
4223
+ description: "Override the API base URL. Internal testing only; not documented to customers.",
4224
+ env: "PRIMITIVE_API_BASE_URL",
4225
+ hidden: true
4226
+ }),
4227
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
4228
+ };
4229
+ if (!operation.binaryResponse) {
4230
+ flags.json = Flags.boolean({ description: "Print exactly one JSON document on stdout, on success and on failure, and nothing on stderr. Stdout keeps its usual shape: the data payload, or with --envelope the full response envelope (data plus meta, including meta.cursor, and empty-result hints in summary). The next-cursor line is not printed; use --envelope to page. A failure prints the error with exit_code. Without --json, the cursor and notices go to stderr." });
4231
+ flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
4232
+ }
4233
+ for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
4234
+ const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
4235
+ if (operation.hasJsonBody) {
4236
+ flags["raw-body"] = Flags.string({ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available." });
4237
+ flags["body-file"] = Flags.string({ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload." });
4238
+ const bodyFields = extractBodyFields(operation.requestSchema);
4239
+ const aliasesForOperation = OPERATION_FLAG_ALIASES[operation.sdkName];
4240
+ for (const field of bodyFields) {
4241
+ if (field.kind === "complex") continue;
4242
+ const name = flagName(field.name);
4243
+ if (RESERVED_FLAG_NAMES.has(name)) continue;
4244
+ if (flags[name] !== void 0) continue;
4245
+ flags[name] = bodyFieldFlag(field, aliasesForOperation?.[field.name]);
4246
+ bodyFieldFlagToProperty.set(name, field.name);
4247
+ }
4248
+ }
4249
+ if (operation.binaryResponse) flags.output = Flags.string({ description: "Write binary response bytes to a file" });
4250
+ if (ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName)) flags.wait = Flags.boolean({
4251
+ allowNo: true,
4252
+ default: true,
4253
+ description: ATTACHMENT_WAIT_FLAG_DESCRIPTION
4254
+ });
4255
+ return {
4256
+ flags,
4257
+ bodyFieldFlagToProperty
4258
+ };
4259
+ }
4260
+ function collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty) {
4261
+ const result = {};
4262
+ for (const [flag, property] of bodyFieldFlagToProperty) {
4263
+ const value = parsedFlags[flag];
4264
+ if (value === void 0) continue;
4265
+ result[property] = value;
4266
+ }
4267
+ return result;
4268
+ }
4269
+ function collectValues(parameters, flags) {
4270
+ const values = {};
4271
+ for (const parameter of parameters) {
4272
+ const value = coerceParameterValue(parameter, flags[flagName(parameter.name)]);
4273
+ if (value !== void 0) values[parameter.name] = value;
4274
+ }
4275
+ return values;
4276
+ }
4277
+ function operationOutputPayload(envelope, includeEnvelope) {
4278
+ return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
4279
+ }
4280
+ /**
4281
+ * The `--json` document for a generated command: the full response
4282
+ * envelope, so pagination metadata travels with the data, plus any
4283
+ * notes the command would otherwise print on stderr.
4284
+ */
4285
+ function operationJsonDocument(envelope, summary = []) {
4286
+ const document = envelope !== null && typeof envelope === "object" ? { ...envelope } : { data: null };
4287
+ if (summary.length > 0) document.summary = summary.join("\n");
4288
+ return document;
4289
+ }
4290
+ function isIncompleteDomainVerification(operation, envelope) {
4291
+ if (operation.sdkName !== "verifyDomain") return false;
4292
+ const data = envelope?.data;
4293
+ if (!data || typeof data !== "object") return false;
4294
+ return data.verified === false;
4295
+ }
4296
+ function writeIncompleteDomainVerificationHint() {
4297
+ 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");
4298
+ }
4299
+ const OPERATION_HINTS = {
4300
+ 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.",
4301
+ 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.",
4302
+ downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
4303
+ getInboxStatus: "Tip: prefer `primitive inbox status` for a compact readiness summary and next-step commands.",
4304
+ 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.",
4305
+ sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
4306
+ createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
4307
+ updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
4308
+ createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
4309
+ setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
4310
+ setMemory: "Tip: prefer `primitive memories set <key> <json-value> [--function <function-id>]` for JSON value parsing and scope flags. This raw command exists for callers passing full request JSON.",
4311
+ getMemory: "Tip: prefer `primitive memories get <key> [--function <function-id>]` for the common read path.",
4312
+ deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
4313
+ searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
4314
+ startAgentSignup: "Tip: pass --terms-accepted, and optionally --signup-code <code> if you have one. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
4315
+ verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
4316
+ };
4317
+ const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
4318
+ const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, apiBaseUrl, writeStderr }) => {
4319
+ const data = envelope?.data;
4320
+ if (!data?.access_token || !data?.refresh_token) return;
4321
+ saveSignupCredentials({
4322
+ apiBaseUrl,
4323
+ configDir,
4324
+ signup: data
4325
+ });
4326
+ writeStderr("Credentials saved to the CLI config; `primitive whoami` will work on the next call.\n");
4327
+ } };
4328
+ function createOperationCommand(operation) {
4329
+ const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
4330
+ const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
4331
+ const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
4332
+ const hint = OPERATION_HINTS[operation.sdkName];
4333
+ const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
4334
+ const fullDescription = hint ? `${descriptionWithSchema}\n\n${hint}` : descriptionWithSchema;
4335
+ class OperationCommand extends Command {
4336
+ static description = fullDescription;
4337
+ static flags = flags;
4338
+ static summary = operation.summary ?? `${operation.method} ${operation.path}`;
4339
+ async run() {
4340
+ const { flags } = await this.parse(OperationCommand);
4341
+ const parsedFlags = flags;
4342
+ if ([
4343
+ "startAgentSignup",
4344
+ "verifyAgentSignup",
4345
+ "resendAgentSignupVerification"
4346
+ ].includes(operation.operationId)) requireDefaultLoginProfile();
4347
+ await runWithTiming(parsedFlags.time === true, async () => {
4348
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
4349
+ apiKey: typeof parsedFlags["api-key"] === "string" ? parsedFlags["api-key"] : void 0,
4350
+ apiBaseUrl: typeof parsedFlags["api-base-url"] === "string" ? parsedFlags["api-base-url"] : void 0,
4351
+ configDir: this.config.configDir
4352
+ });
4353
+ let body;
4354
+ if (operation.hasJsonBody) {
4355
+ const explicit = readJsonBody(parsedFlags);
4356
+ const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
4357
+ if (Object.keys(overrides).length > 0) if (explicit === void 0) body = overrides;
4358
+ else if (explicit !== null && typeof explicit === "object" && !Array.isArray(explicit)) body = {
4359
+ ...explicit,
4360
+ ...overrides
4361
+ };
4362
+ else {
4363
+ const explicitKind = explicit === null ? "null" : Array.isArray(explicit) ? "array" : typeof explicit;
4364
+ const overrideFlags = Object.keys(overrides).map((p) => `--${flagName(p)}`).join(", ");
4365
+ throw new Errors.CLIError(`--raw-body must be a JSON object when also passing per-field flags (got ${explicitKind}); supplied per-field flags: ${overrideFlags}. Either drop --raw-body and rely on the per-field flags, or move every field into the JSON --raw-body and drop the flags.`);
4366
+ }
4367
+ else body = explicit;
4368
+ }
4369
+ if (operation.bodyRequired && body === void 0) throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --raw-body / --body-file.`);
4370
+ const operationFn = sdk_gen_exports[operation.sdkName];
4371
+ const query = collectValues(operation.queryParams, parsedFlags);
4372
+ const replyStateSurface = replyStateSurfaceForOperation(operation.sdkName, query);
4373
+ const automatedSurface = automatedSurfaceForOperation(operation.sdkName, query);
4374
+ const callOperation = () => operationFn({
4375
+ body,
4376
+ client: apiClient.client,
4377
+ parseAs: operation.binaryResponse ? "blob" : "auto",
4378
+ path: collectValues(operation.pathParams, parsedFlags),
4379
+ query,
4380
+ responseStyle: "fields"
4381
+ });
4382
+ const result = ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
4383
+ wait: parsedFlags.wait !== false,
4384
+ errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
4385
+ onFirstWait: () => {
4386
+ process.stderr.write(`Attachment content is not ready yet; waiting up to ${ATTACHMENT_WAIT_MAX_MS / 1e3}s. Pass --no-wait to return immediately.\n`);
4387
+ }
4388
+ }) : await callOperation();
4389
+ if (result.error) {
4390
+ const errorPayload = extractErrorPayload(result.error);
4391
+ writeErrorWithHints(errorPayload);
4392
+ if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
4393
+ if (automatedSurface && isAutomatedRejectedError(errorPayload)) process.stderr.write(`${automatedRejectedError(automatedSurface).message}\n`);
4394
+ surfaceUnauthorizedHint({
4395
+ auth,
4396
+ baseUrlOverridden,
4397
+ configDir: this.config.configDir,
4398
+ payload: errorPayload
4399
+ });
4400
+ const listClient = apiClient.client;
4401
+ const listEndpointsFn = () => listEndpoints({
4402
+ client: listClient,
4403
+ responseStyle: "fields"
4404
+ });
4405
+ await maybeWriteFunctionEndpointRedirect({
4406
+ sdkName: operation.sdkName,
4407
+ errorCode: extractErrorCode(errorPayload),
4408
+ endpointId: typeof parsedFlags.id === "string" ? parsedFlags.id : void 0,
4409
+ listEndpoints: listEndpointsFn,
4410
+ writeStderr: (chunk) => {
4411
+ process.stderr.write(chunk);
4412
+ }
4413
+ });
4414
+ process.exitCode = 1;
4415
+ return;
4416
+ }
4417
+ if (operation.binaryResponse) {
4418
+ const blob = result.data;
4419
+ const bytes = Buffer.from(await blob.arrayBuffer());
4420
+ const output = parsedFlags.output;
4421
+ if (typeof output === "string") {
4422
+ writeFileSync(output, bytes);
4423
+ return;
4424
+ }
4425
+ process.stdout.write(bytes);
4426
+ return;
4427
+ }
4428
+ const envelope = result.data;
4429
+ if (replyStateSurface) try {
4430
+ const rows = Array.isArray(envelope?.data) ? envelope.data : [];
4431
+ assertAwaitingFilterRows(rows, replyStateSurface);
4432
+ if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsReplyState(apiClient);
4433
+ } catch (error) {
4434
+ if (!(error instanceof ReplyStateUnsupportedError)) throw error;
4435
+ process.stderr.write(`${error.message}\n`);
4436
+ process.exitCode = 1;
4437
+ return;
4438
+ }
4439
+ if (automatedSurface) try {
4440
+ const rows = Array.isArray(envelope?.data) ? envelope.data : [];
4441
+ assertAutomatedVerdict(rows, automatedSurface, expectedAutomatedVerdicts(query?.automated === "true" ? true : query?.automated === "false" ? false : void 0, typeof query?.q === "string" ? query.q : void 0));
4442
+ if (rows.length === 0 && operation.sdkName === "searchEmails") await ensureSearchReportsAutomated(apiClient);
4443
+ } catch (error) {
4444
+ if (!(error instanceof AutomatedFilterUnsupportedError)) throw error;
4445
+ process.stderr.write(`${error.message}\n`);
4446
+ process.exitCode = 1;
4447
+ return;
4448
+ }
4449
+ const jsonOutput = parsedFlags.json === true;
4450
+ const withEnvelope = parsedFlags.envelope === true;
4451
+ const cursor = envelope?.meta?.cursor;
4452
+ if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
4453
+ const summary = [];
4454
+ if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
4455
+ const hint = EMPTY_RESULT_HINTS[operation.sdkName];
4456
+ if (hint && jsonOutput) {
4457
+ if (withEnvelope) summary.push(hint);
4458
+ } else if (hint) process.stderr.write(`${hint}\n`);
4459
+ }
4460
+ writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
4461
+ process.stderr.write(chunk);
4462
+ } });
4463
+ const successHook = OPERATION_SUCCESS_HOOKS[operation.sdkName];
4464
+ if (successHook) try {
4465
+ successHook({
4466
+ envelope,
4467
+ configDir: this.config.configDir,
4468
+ apiBaseUrl: auth.apiBaseUrl,
4469
+ writeStderr: (chunk) => {
4470
+ process.stderr.write(chunk);
4471
+ }
4472
+ });
4473
+ } catch (hookError) {
4474
+ const detail = hookError instanceof Error ? hookError.message : String(hookError);
4475
+ process.stderr.write(`Warning: ${operation.sdkName} succeeded but its post-success hook threw (${detail}). The response below is still valid; act on it manually.\n`);
4476
+ }
4477
+ this.log(JSON.stringify(jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope), null, 2));
4478
+ if (isIncompleteDomainVerification(operation, envelope)) {
4479
+ writeIncompleteDomainVerificationHint();
4480
+ process.exitCode = 1;
4481
+ }
4482
+ });
4483
+ }
4484
+ }
4485
+ return OperationCommand;
4486
+ }
4487
+ const EMPTY_RESULT_HINTS = {
4488
+ listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints list` to check.",
4489
+ listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
4490
+ listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
4491
+ listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
4492
+ listFilters: "(no results) No filter rules configured.",
4493
+ listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
4494
+ };
4495
+ function canonicalizeCliReferences(description) {
4496
+ return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
4497
+ }
4498
+ //#endregion
4499
+ //#region src/oclif/send-outcome.ts
4500
+ const SEND_OUTCOME_EXIT_CODES = {
4501
+ replied: 0,
4502
+ sent: 0,
4503
+ already_sent: 0,
4504
+ not_sent: 1,
4505
+ sent_awaiting_reply: 3,
4506
+ uncertain: 4
4507
+ };
4508
+ const DEFINITIVE_SEND_REJECTION_STATUSES = [
4509
+ 400,
4510
+ 401,
4511
+ 402,
4512
+ 403,
4513
+ 404,
4514
+ 413,
4515
+ 422,
4516
+ 429
4517
+ ];
4518
+ const SEND_OUTCOME_HELP = `Outcomes and exit codes, shared by chat, chat reply, send and reply (JSON output carries the name as "outcome"):
4519
+ - exit 0 replied: chat only. The message was sent and a reply arrived.
4520
+ - exit 0 sent: accepted for delivery. A queued status counts as sent.
4521
+ - exit 0 already_sent: an identical earlier send exists (or was deleted: HTTP 410 sent_email_deleted). Nothing new went out. Do not resend.
4522
+ - exit 1 not_sent: the API rejected the request (HTTP 400, 401, 402, 403, 404, 413, 422 or 429), the command failed before sending, or the send record has status agent_failed, gate_denied or canceled. Nothing went out.
4523
+ - exit 2: invalid flags or arguments. Nothing went out.
4524
+ - exit 3 sent_awaiting_reply: chat only. Sent, but no reply before the timeout. Wait; do not resend.
4525
+ - exit 4 uncertain: transport error, conflict, server error, or a send record with status unknown. It may or may not have gone out. Reconcile with primitive sent get --idempotency-key <key> (the key is in the --json envelope) before retrying.`;
4526
+ function sendOutcomeExitCode(outcome) {
4527
+ return SEND_OUTCOME_EXIT_CODES[outcome];
4528
+ }
4529
+ function sendFailureOutcome(httpStatus) {
4530
+ return httpStatus !== void 0 && DEFINITIVE_SEND_REJECTION_STATUSES.includes(httpStatus) ? "not_sent" : "uncertain";
4531
+ }
4532
+ const SENT_EMAIL_DELETED_CODE = "sent_email_deleted";
4533
+ function errorPayloadCode(payload) {
4534
+ if (payload === null || typeof payload !== "object") return void 0;
4535
+ const code = payload.code;
4536
+ return typeof code === "string" ? code : void 0;
4537
+ }
4538
+ function isDeletedEarlierSend(httpStatus, errorPayload) {
4539
+ return httpStatus === 410 && errorPayloadCode(errorPayload) === SENT_EMAIL_DELETED_CODE;
4540
+ }
4541
+ /** Classify an API error from a send request. */
4542
+ function classifySendError(httpStatus, errorPayload) {
4543
+ return isDeletedEarlierSend(httpStatus, errorPayload) ? "already_sent" : sendFailureOutcome(httpStatus);
4544
+ }
4545
+ function formatDeletedEarlierSendNotice() {
4546
+ return "Already sent: an earlier send matching this request went out and its record was later deleted, so the API refused this one (HTTP 410 sent_email_deleted). Nothing new was sent. Do not resend or change the idempotency key to get around this.";
4547
+ }
4548
+ /** Read the HTTP status from a `responseStyle: "fields"` API result. */
4549
+ function apiResultHttpStatus(result) {
4550
+ if (result === null || typeof result !== "object") return void 0;
4551
+ if (!("response" in result)) return void 0;
4552
+ const response = result.response;
4553
+ return typeof response?.status === "number" ? response.status : void 0;
4554
+ }
4555
+ const SEND_STATUSES_THAT_DID_NOT_GO_OUT = new Set([
4556
+ "agent_failed",
4557
+ "canceled",
4558
+ "gate_denied"
4559
+ ]);
4560
+ const SEND_STATUSES_WITH_UNKNOWN_OUTCOME = new Set(["unknown"]);
4561
+ /**
4562
+ * Classify a send record the API returned with a 2xx. A 2xx alone does
4563
+ * not mean a message went out: the record's status can show the
4564
+ * attempt was rejected or that its result is indeterminate. The same
4565
+ * holds for an idempotent replay, which returns the earlier attempt's
4566
+ * record as it stands.
4567
+ */
4568
+ function successfulSendOutcome(sent) {
4569
+ if (SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(sent.status)) return "not_sent";
4570
+ if (SEND_STATUSES_WITH_UNKNOWN_OUTCOME.has(sent.status)) return "uncertain";
4571
+ return sent.idempotent_replay === true ? "already_sent" : "sent";
4572
+ }
4573
+ /**
4574
+ * Summary for a 2xx send record whose status shows it did not go out
4575
+ * (`not_sent`) or that its result is indeterminate (`uncertain`).
4576
+ */
4577
+ function formatSendRecordFailureSummary(noun, outcome, sent) {
4578
+ const record = sent.idempotent_replay === true ? `the earlier identical attempt (sent id ${sent.id}) has status ${sent.status}` : `the send record (id ${sent.id}) has status ${sent.status}`;
4579
+ if (outcome === "not_sent") return `${noun} not sent: ${record}. Nothing went out; fix the problem before retrying.`;
4580
+ return `${noun} send outcome uncertain: ${record}. It may or may not have gone out. Do not resend blindly; check sent history first.`;
4581
+ }
4582
+ function describeSentStatus(status) {
4583
+ if (status === "queued") return "queued for delivery";
4584
+ if (status === "delivered") return "delivered";
4585
+ return `status ${status}`;
4586
+ }
4587
+ function formatSentSummary(noun, sent) {
4588
+ return `${noun} sent (${describeSentStatus(sent.status)}, id ${sent.id}). Do not resend.`;
4589
+ }
4590
+ function formatSendFailureSummary(noun, outcome, httpStatus) {
4591
+ if (outcome === "not_sent") return `${noun} not sent: ${httpStatus === void 0 ? "the request failed before sending" : `the API rejected the request (HTTP ${httpStatus})`}. Nothing went out; fix the problem before retrying.`;
4592
+ return `${noun} send outcome uncertain (${httpStatus === void 0 ? "no response from the API" : `HTTP ${httpStatus}`}): it may or may not have gone out. Do not resend blindly; check sent history first.`;
4593
+ }
4594
+ function priorRepliesThatWentOut(replies, options = {}) {
4595
+ return (replies ?? []).filter((reply) => !SEND_STATUSES_THAT_DID_NOT_GO_OUT.has(reply.status) && reply.id !== options.excludeSentId);
4596
+ }
4597
+ function formatPriorRepliesWarning(prior) {
4598
+ const latest = prior.at(-1);
4599
+ if (!latest) return null;
4600
+ return `This email already has ${prior.length === 1 ? "1 outgoing email" : `${prior.length} outgoing emails`}, most recently at ${latest.created_at} (sent id ${latest.id}). These may include activity updates and do not prove a completed answer. Sending this reply.`;
4601
+ }
4602
+ function formatPriorRepliesCheckSkipped(emailId, reason) {
4603
+ return `Could not check whether you already replied to email ${emailId} (${reason}). Prior-reply check skipped; sending the reply anyway.`;
4604
+ }
4605
+ /**
4606
+ * Look up the inbound email and report replies to it that already
4607
+ * went out. Never throws: the check is advisory and must not block a
4608
+ * send, so every failure comes back as `skipped` with a reason the
4609
+ * caller surfaces.
4610
+ */
4611
+ async function checkPriorReplies(params) {
4612
+ try {
4613
+ const result = await getEmail({
4614
+ client: params.client,
4615
+ path: { id: params.emailId },
4616
+ responseStyle: "fields"
4617
+ });
4618
+ if (result.error) {
4619
+ const status = apiResultHttpStatus(result);
4620
+ return {
4621
+ status: "skipped",
4622
+ reason: status === void 0 ? "the email lookup failed" : `the email lookup returned HTTP ${status}`
4623
+ };
4624
+ }
4625
+ const envelope = result.data;
4626
+ const detail = envelope?.data ?? envelope;
4627
+ if (!detail || !Array.isArray(detail.replies)) return {
4628
+ status: "skipped",
4629
+ reason: "the email lookup returned no reply history"
4630
+ };
4631
+ return {
4632
+ status: "checked",
4633
+ prior: priorRepliesThatWentOut(detail.replies),
4634
+ detail
4635
+ };
4636
+ } catch (error) {
4637
+ return {
4638
+ status: "skipped",
4639
+ reason: `the email lookup failed: ${error instanceof Error ? error.message : String(error)}`
4640
+ };
4641
+ }
4642
+ }
4643
+ function shellQuote(value) {
4644
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
4645
+ return `'${value.replaceAll("'", "'\\''")}'`;
4646
+ }
4647
+ function commandFromArgv(argv) {
4648
+ return argv.map(shellQuote).join(" ");
4649
+ }
4650
+ function buildFollowUpCommand(kind, description, argv, options = {}) {
4651
+ const requiresMessage = options.requiresMessage ?? false;
4652
+ return {
4653
+ argv,
4654
+ description,
4655
+ command: commandFromArgv(argv),
4656
+ kind,
4657
+ placeholders: requiresMessage ? [{
4658
+ description: "Replace with the message body before running.",
4659
+ token: "<message>"
4660
+ }] : [],
4661
+ requires_message: requiresMessage
4662
+ };
4663
+ }
4664
+ const SENT_HISTORY_CLOCK_SKEW_MS = 300 * 1e3;
4665
+ function sentHistoryWindowStart(attemptStartedAtIso) {
4666
+ const started = Date.parse(attemptStartedAtIso);
4667
+ if (!Number.isFinite(started)) return attemptStartedAtIso;
4668
+ return new Date(started - SENT_HISTORY_CLOCK_SKEW_MS).toISOString();
4669
+ }
4670
+ /** Header rule for Idempotency-Key: 1 to 255 printable ASCII characters. */
4671
+ const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
4672
+ const IDEMPOTENCY_KEY_FLAG_DESCRIPTION = "Idempotency key for this send (1 to 255 printable ASCII characters). Retrying with the same key returns the original send instead of sending twice. Defaults to a key derived from the request content and the current five-minute window, so an identical retry within that window is deduplicated and a deliberate repeat later still sends. To retry an uncertain send after that, pass the key it reported. Look a send up later with `primitive sent get --idempotency-key <key>`.";
4673
+ function assertValidIdempotencyKey(key) {
4674
+ if (!IDEMPOTENCY_KEY_PATTERN.test(key)) throw new Errors.CLIError("--idempotency-key must be 1 to 255 printable ASCII characters with no spaces.", { exit: 2 });
4675
+ return key;
4676
+ }
4677
+ const NON_CONTENT_SEND_FIELDS = new Set(["wait", "wait_timeout_ms"]);
4678
+ function canonicalJson(value) {
4679
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item ?? null)).join(",")}]`;
4680
+ if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
4681
+ return JSON.stringify(value ?? null);
4682
+ }
4683
+ /**
4684
+ * How long a derived key stays the same. It matches the window the API
4685
+ * uses for the key it derives itself when a request carries none, so an
4686
+ * identical send repeated inside this window is deduplicated and a
4687
+ * deliberate repeat later (a digest, a heartbeat, the same short reply
4688
+ * on another day) still goes out, exactly as without a key. An explicit
4689
+ * --idempotency-key never expires.
4690
+ */
4691
+ const DERIVED_IDEMPOTENCY_WINDOW_MS = 300 * 1e3;
4692
+ /**
4693
+ * The idempotency key a send uses when the caller passes none. It is a
4694
+ * hash of what is sent (and, for a reply, of the email replied to) and
4695
+ * of the current five-minute window, so dedup matches what the API did
4696
+ * before the CLI sent a key, while the CLI knows the key before the
4697
+ * request starts. That lets an uncertain outcome still report the key to
4698
+ * reconcile with, and to retry under with --idempotency-key.
4699
+ */
4700
+ function deriveSendIdempotencyKey(kind, request, now = Date.now()) {
4701
+ const content = Object.fromEntries(Object.entries(request).filter(([key]) => !NON_CONTENT_SEND_FIELDS.has(key)));
4702
+ return `primitive-${kind}-${createHash("sha256").update(canonicalJson(content)).update(`:t${Math.floor(now / DERIVED_IDEMPOTENCY_WINDOW_MS)}`).digest("hex")}`;
4703
+ }
4704
+ /**
4705
+ * Identity fields every send, reply and chat `--json` envelope carries,
4706
+ * whatever the outcome. `sent_email_id` is null until a send record is
4707
+ * known; `idempotency_key` is known before the request starts, so an
4708
+ * uncertain outcome can be reconciled with
4709
+ * `primitive sent get --idempotency-key <key>`.
4710
+ */
4711
+ function sendIdentityFields(params) {
4712
+ return {
4713
+ sent_email_id: params.sent?.id ?? null,
4714
+ idempotency_key: params.idempotencyKey ?? params.sent?.client_idempotency_key ?? null
4715
+ };
4716
+ }
4717
+ /**
4718
+ * Commands that inspect an existing send. None of them send again:
4719
+ * a caller that follows them after a failure cannot double-send.
4720
+ */
4721
+ function buildSendCommandFollowUps(params) {
4722
+ const commands = [];
4723
+ if (params.sentId === null && params.outcome === "uncertain" && params.idempotencyKey) commands.push(buildFollowUpCommand("find_sent_email_by_idempotency_key", "Look up this attempt by its idempotency key before retrying", [
4724
+ "primitive",
4725
+ "sent",
4726
+ "get",
4727
+ "--idempotency-key",
4728
+ params.idempotencyKey
4729
+ ]));
4730
+ if (params.sentId !== null) commands.push(buildFollowUpCommand("inspect_sent_email", "Inspect the send and its delivery status", [
4731
+ "primitive",
4732
+ "sent",
4733
+ "get",
4734
+ "--id",
4735
+ params.sentId
4736
+ ]));
4737
+ if (params.outcome === "uncertain") commands.push(buildFollowUpCommand("list_recent_sent_emails", "Check sent history for this attempt before retrying", [
4738
+ "primitive",
4739
+ "sent",
4740
+ "list",
4741
+ "--date-from",
4742
+ sentHistoryWindowStart(params.attemptStartedAtIso)
4743
+ ]));
4744
+ return commands;
4745
+ }
4746
+ /**
4747
+ * Classify a `send` / `reply` API result, write the stderr summary,
4748
+ * and print stdout. Without `--json` stdout stays exactly what these
4749
+ * commands always printed (the SendMailResult, or `null`), so
4750
+ * existing `| jq` pipelines keep working; the outcome is on stderr
4751
+ * and in the exit code. With `--json` stdout is always an envelope,
4752
+ * whatever the outcome, and the summary is only its outcome_message.
4753
+ */
4754
+ function reportSendCommandResult(params) {
4755
+ const httpStatus = apiResultHttpStatus(params.result);
4756
+ let outcome;
4757
+ let outcomeMessage;
4758
+ let sent = null;
4759
+ let errorPayload = null;
4760
+ if (params.result.error) {
4761
+ errorPayload = extractErrorPayload(params.result.error);
4762
+ params.onApiError(errorPayload);
4763
+ const failure = classifySendError(httpStatus, errorPayload);
4764
+ outcome = failure;
4765
+ outcomeMessage = failure === "already_sent" ? formatDeletedEarlierSendNotice() : formatSendFailureSummary(params.noun, failure, httpStatus);
4766
+ if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
4767
+ } else {
4768
+ sent = params.result.data?.data ?? null;
4769
+ if (sent === null) {
4770
+ outcome = "uncertain";
4771
+ outcomeMessage = `${params.noun} send outcome uncertain: the API accepted the request but returned no send record. Do not resend blindly; check sent history first.`;
4772
+ errorPayload = { message: "The API returned no send record." };
4773
+ } else {
4774
+ outcome = successfulSendOutcome(sent);
4775
+ outcomeMessage = outcome === "not_sent" || outcome === "uncertain" ? formatSendRecordFailureSummary(params.noun, outcome, sent) : outcome === "already_sent" ? formatAlreadySentNotice(sent) : formatSentSummary(params.noun, sent);
4776
+ }
4777
+ if (!params.json) params.writeStderr(`${outcomeMessage}\n`);
4778
+ }
4779
+ if (params.json) {
4780
+ const envelope = {
4781
+ outcome,
4782
+ exit_code: sendOutcomeExitCode(outcome),
4783
+ outcome_message: outcomeMessage,
4784
+ ...sendIdentityFields({
4785
+ idempotencyKey: params.idempotencyKey,
4786
+ sent
4787
+ }),
4788
+ sent,
4789
+ http_status: httpStatus ?? null,
4790
+ error: serializeErrorPayload(errorPayload),
4791
+ follow_up_commands: buildSendCommandFollowUps({
4792
+ attemptStartedAtIso: params.attemptStartedAtIso,
4793
+ idempotencyKey: params.idempotencyKey,
4794
+ outcome,
4795
+ sentId: sent?.id ?? null
4796
+ }),
4797
+ ...params.extraEnvelopeFields
4798
+ };
4799
+ params.log(JSON.stringify(envelope, null, 2));
4800
+ } else if (!params.result.error) params.log(JSON.stringify(sent, null, 2));
4801
+ return outcome;
4802
+ }
4803
+ /** JSON-safe form of an API error payload; `Error` objects stringify to `{}`. */
4804
+ function serializeErrorPayload(payload) {
4805
+ if (!(payload instanceof Error)) return payload ?? null;
4806
+ const serialized = {
4807
+ name: payload.name,
4808
+ message: payload.message
4809
+ };
4810
+ const cause = payload.cause;
4811
+ if (cause && typeof cause.code === "string") serialized.code = cause.code;
4812
+ return serialized;
4813
+ }
4814
+ /**
4815
+ * Envelope for a `send` / `reply` that threw instead of returning an
4816
+ * API result. Printed in --json mode so stdout carries an envelope for
4817
+ * every outcome. Before the request starts (bad input, missing auth,
4818
+ * unreadable attachment) nothing went out; after it starts the
4819
+ * outcome is unknown.
4820
+ */
4821
+ function buildThrownSendFailureEnvelope(params) {
4822
+ const message = params.error instanceof Error ? params.error.message : String(params.error);
4823
+ const outcome = params.requestStarted ? "uncertain" : "not_sent";
4824
+ return {
4825
+ outcome,
4826
+ exit_code: params.requestStarted ? sendOutcomeExitCode("uncertain") : thrownErrorExitCode(params.error),
4827
+ outcome_message: formatSendFailureSummary(params.noun, outcome, void 0),
4828
+ ...sendIdentityFields({
4829
+ idempotencyKey: params.idempotencyKey,
4830
+ sent: null
4831
+ }),
4832
+ sent: null,
4833
+ http_status: null,
4834
+ error: { message },
4835
+ follow_up_commands: params.requestStarted && params.attemptStartedAtIso ? buildSendCommandFollowUps({
4836
+ attemptStartedAtIso: params.attemptStartedAtIso,
4837
+ idempotencyKey: params.idempotencyKey,
4838
+ outcome,
4839
+ sentId: null
4840
+ }) : [],
4841
+ ...params.extraEnvelopeFields
4842
+ };
4843
+ }
4844
+ /** Exit code oclif will use for a thrown error (2 for usage errors). */
4845
+ function thrownErrorExitCode(error) {
4846
+ const exit = error?.oclif?.exit;
4847
+ return typeof exit === "number" ? exit : 1;
4848
+ }
4849
+ //#endregion
4850
+ //#region src/oclif/interaction-actions.ts
4851
+ const HINTS = new Set([
4852
+ "status",
4853
+ "card",
4854
+ "none",
4855
+ "pending"
4856
+ ]);
4857
+ /** A printable `<protocol>/<version>`; anything else is treated as unknown. */
4858
+ const KIND = /^[a-z][a-z0-9._-]{0,63}\/[1-9][0-9]{0,3}$/;
4859
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4860
+ function record(value) {
4861
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
4862
+ }
4863
+ function categoryOf(hint, kind, fyi, candidate) {
4864
+ if (hint === "pending") return candidate ? "pending" : "ordinary";
4865
+ if (hint === "card") switch (kind) {
4866
+ case "x402.payment/1": return "payment";
4867
+ case "primitive.contact/1": return "contact";
4868
+ case "repeat.tick/1": return "repeat";
4869
+ case "repeat.stop/1": return "repeat_stopped";
4870
+ default: return "unsupported";
4871
+ }
4872
+ if (fyi) return "fyi";
4873
+ if (hint === "status") return "signal";
4874
+ return "ordinary";
4875
+ }
4876
+ function replyExpectation(category) {
4877
+ switch (category) {
4878
+ case "fyi":
4879
+ case "signal":
4880
+ case "repeat_stopped": return "no_reply";
4881
+ case "payment":
4882
+ case "contact": return "answer_with_command";
4883
+ case "repeat": return "repeat";
4884
+ case "unsupported": return "unsupported";
4885
+ case "pending": return "read_again";
4886
+ default: return "reply";
4887
+ }
4888
+ }
4889
+ /**
4890
+ * The server's interaction facts for an email read, or null when the server
4891
+ * did not report `interaction_hint` (an older server). An unfamiliar hint is
4892
+ * treated as `none`, as the API documents.
4893
+ */
4894
+ function readEmailInteraction(detail) {
4895
+ const row = record(detail);
4896
+ if (!row || typeof row.interaction_hint !== "string") return null;
4897
+ const hint = HINTS.has(row.interaction_hint) ? row.interaction_hint : "none";
4898
+ const rawKind = row.interaction_kind;
4899
+ const kind = (hint === "card" || hint === "status") && typeof rawKind === "string" && KIND.test(rawKind) ? rawKind : null;
4900
+ const fyi = row.fyi === true || record(row.collaboration)?.fyi === true;
4901
+ const category = categoryOf(hint, kind, fyi, row.interaction_candidate === true);
4902
+ const expectation = replyExpectation(category);
4903
+ return {
4904
+ hint,
4905
+ kind,
4906
+ fyi,
4907
+ category,
4908
+ plain_reply_completes: expectation === "reply" || expectation === "repeat",
4909
+ no_reply_needed: expectation === "no_reply"
4910
+ };
4911
+ }
4912
+ function replyAction(bin, id) {
4913
+ return buildFollowUpCommand("reply", "Reply to the sender", [
4914
+ bin,
4915
+ "reply",
4916
+ "--id",
4917
+ id,
4918
+ "--body",
4919
+ "<message>"
4920
+ ], { requiresMessage: true });
4921
+ }
4922
+ /**
4923
+ * The commands that answer an email, best first. Empty when the email needs
4924
+ * no reply or this CLI cannot answer it. `repeatStoppable: false` (the
4925
+ * repeat's own part says only the sender can stop it) leaves out the stop.
4926
+ */
4927
+ function interactionNextActions(interaction, emailId, options = {}) {
4928
+ const bin = options.bin ?? "primitive";
4929
+ if (!UUID.test(emailId)) return [];
4930
+ const id = emailId.toLowerCase();
4931
+ switch (interaction?.category ?? "ordinary") {
4932
+ case "ordinary": return [replyAction(bin, id)];
4933
+ case "payment": return [buildFollowUpCommand("inspect_payment", "Show the payment request's terms before paying", [
4934
+ bin,
4935
+ "payments",
4936
+ "challenge-from-email",
4937
+ "--id",
4938
+ id
4939
+ ]), buildFollowUpCommand("pay", "Pay the payment request (signs with your wallet key and sends the payment)", [
4940
+ bin,
4941
+ "payments",
4942
+ "pay-email",
4943
+ "--in-reply-to",
4944
+ id
4945
+ ])];
4946
+ case "contact": return [buildFollowUpCommand("accept_contact", "Accept the contact request, if the owner's policy allows it", [
4947
+ bin,
4948
+ "contacts",
4949
+ "accept",
4950
+ "--id",
4951
+ id
4952
+ ])];
4953
+ case "repeat": {
4954
+ const actions = [replyAction(bin, id)];
4955
+ if (options.repeatStoppable !== false) actions.push(buildFollowUpCommand("stop_repeat", "Stop the repeating message once its goal is met", [
4956
+ bin,
4957
+ "repeat",
4958
+ "stop",
4959
+ "--id",
4960
+ id
4961
+ ]));
4962
+ return actions;
4963
+ }
4964
+ case "pending": return [buildFollowUpCommand("read_again", "Read the email again once the server has finished checking it", [
4965
+ bin,
4966
+ "emails",
4967
+ "get",
4968
+ "--id",
4969
+ id,
4970
+ "--brief"
4971
+ ])];
4972
+ default: return [];
4973
+ }
4974
+ }
4975
+ /**
4976
+ * One human line naming how to answer the email, or null for ordinary mail
4977
+ * (and older servers), where `primitive reply` is the answer. Contains only
4978
+ * server-decided values and fixed text, never anything the sender wrote.
4979
+ */
4980
+ function interactionHumanLine(interaction, actions) {
4981
+ if (!interaction) return null;
4982
+ const command = (kind) => actions.find((action) => action.kind === kind)?.command;
4983
+ const kind = interaction.kind ?? "unknown kind";
4984
+ switch (interaction.category) {
4985
+ case "fyi": return "Informational (fyi): no reply needed, not even another fyi.";
4986
+ case "signal": return `Status signal (${kind}): no reply needed.`;
4987
+ case "repeat_stopped": return `Notice that a repeating message stopped (${kind}): no reply needed.`;
4988
+ case "payment": {
4989
+ const inspect = command("inspect_payment");
4990
+ const pay = command("pay");
4991
+ return inspect && pay ? `Payment interaction (${kind}). If it requests payment, review it with ${inspect} and pay with ${pay}. A plain reply does not pay or decline it.` : null;
4992
+ }
4993
+ case "contact": {
4994
+ const accept = command("accept_contact");
4995
+ return accept ? `Contact interaction (${kind}). If it is a contact request, accept it under the owner's policy with ${accept}. A plain reply does not accept it.` : null;
4996
+ }
4997
+ case "repeat": {
4998
+ const stop = command("stop_repeat");
4999
+ return stop ? `Repeating message (${kind}). Reply if it asks for an answer; stop it once its goal is met with ${stop}.` : `Repeating message (${kind}). Reply if it asks for an answer; only the sender can stop it.`;
5000
+ }
5001
+ case "unsupported": return `Interaction (${kind}) that this CLI cannot answer. A plain reply does not complete it.`;
5002
+ case "pending": {
5003
+ const again = command("read_again");
5004
+ return again ? `Primitive has not finished checking this email for an interaction. Read it again before answering: ${again}` : null;
5005
+ }
5006
+ default: return null;
5007
+ }
5008
+ }
5009
+ /** The label a wake notice carries: the interaction kind, `fyi`, or null. */
5010
+ function wakeInteractionLabel(detail) {
5011
+ const interaction = readEmailInteraction(detail);
5012
+ if (!interaction) return null;
5013
+ if (interaction.hint === "card") return interaction.kind ?? "unknown";
5014
+ if (interaction.fyi) return "fyi";
5015
+ return null;
5016
+ }
5017
+ /** The category a wake label stands for, or null for an invalid label. */
5018
+ function wakeLabelCategory(label) {
5019
+ if (!isWakeInteractionLabel(label)) return null;
5020
+ if (label === "fyi") return "fyi";
5021
+ return categoryOf("card", label === "unknown" ? null : label, false, false);
5022
+ }
5023
+ /**
5024
+ * The fixed wake sentences, keyed by reply expectation. The Claude hook
5025
+ * scripts in bin/ carry copies (they run standalone); a test keeps them equal.
5026
+ */
5027
+ const WAKE_SENTENCES = {
5028
+ reply: "",
5029
+ read_again: "",
5030
+ no_reply: " It needs no reply.",
5031
+ answer_with_command: " It is an interaction a plain reply does not complete; the brief names the command that answers it.",
5032
+ repeat: " It is a repeating message; the brief says how to answer it and whether you can stop it.",
5033
+ unsupported: " It is an interaction this CLI cannot answer; a plain reply does not complete it."
5034
+ };
5035
+ /**
5036
+ * The sentence a wake line adds after its read command for an interaction
5037
+ * label: empty for ordinary mail and for anything that is not a valid label.
5038
+ */
5039
+ function wakeInteractionSentence(label) {
5040
+ const category = wakeLabelCategory(label);
5041
+ return category ? WAKE_SENTENCES[replyExpectation(category)] : "";
5042
+ }
5043
+ /** Accepts only labels `wakeInteractionLabel` can produce. */
5044
+ function isWakeInteractionLabel(value) {
5045
+ return typeof value === "string" && (value === "fyi" || value === "unknown" || KIND.test(value));
5046
+ }
5047
+ /**
5048
+ * Warning for a plain (prose) reply to `detail`, or null when a plain reply
5049
+ * is a fine answer. Never refuses: the caller still sends.
5050
+ */
5051
+ function replyInteractionWarning(detail, emailId) {
5052
+ const interaction = readEmailInteraction(detail);
5053
+ if (!interaction || !UUID.test(emailId)) return null;
5054
+ const id = emailId.toLowerCase();
5055
+ const kind = interaction.kind;
5056
+ const label = kind ?? "unknown kind";
5057
+ const base = {
5058
+ email_id: id,
5059
+ kind,
5060
+ category: interaction.category
5061
+ };
5062
+ const expected = interactionNextActions(interaction, id);
5063
+ const commandFor = (actionKind) => expected.find((action) => action.kind === actionKind)?.command;
5064
+ const expectation = replyExpectation(interaction.category);
5065
+ if (expectation === "reply" || expectation === "repeat") return null;
5066
+ switch (interaction.category) {
5067
+ case "payment": return {
5068
+ ...base,
5069
+ code: "interaction_not_answered",
5070
+ message: `Warning: email ${id} is a payment interaction (${label}). A plain reply does not pay or decline it; sending anyway. To pay: ${commandFor("pay")}`,
5071
+ expected
5072
+ };
5073
+ case "contact": return {
5074
+ ...base,
5075
+ code: "interaction_not_answered",
5076
+ message: `Warning: email ${id} is a contact interaction (${label}). A plain reply does not accept it; sending anyway. To accept: ${commandFor("accept_contact")}`,
5077
+ expected
5078
+ };
5079
+ case "unsupported": return {
5080
+ ...base,
5081
+ code: "interaction_not_answered",
5082
+ message: `Warning: email ${id} is an interaction (${label}) that this CLI cannot answer. A plain reply does not complete it; sending anyway.`,
5083
+ expected
5084
+ };
5085
+ case "fyi": return {
5086
+ ...base,
5087
+ code: "reply_not_needed",
5088
+ message: `Warning: email ${id} is informational (fyi) and needs no reply; sending anyway.`,
5089
+ expected
5090
+ };
5091
+ case "signal": return {
5092
+ ...base,
5093
+ code: "reply_not_needed",
5094
+ message: `Warning: email ${id} is a status signal (${label}) and needs no reply; sending anyway.`,
5095
+ expected
5096
+ };
5097
+ case "repeat_stopped": return {
5098
+ ...base,
5099
+ code: "reply_not_needed",
5100
+ message: `Warning: email ${id} is a notice that a repeating message stopped (${label}) and needs no reply; sending anyway.`,
5101
+ expected
5102
+ };
5103
+ default: return null;
5104
+ }
5105
+ }
5106
+ //#endregion
5107
+ export { queryUsesAwaiting as $, listContacts as $t, thrownErrorExitCode as A, setMemory as An, deleteRoute as At, surfaceUnauthorizedHint as B, updateRepeatingSend as Bn, getEmail as Bt, priorRepliesThatWentOut as C, runWakeSchedule as Cn, createFunction as Ct, sentHistoryWindowStart as D, sendEmail as Dn, deleteAgentContact as Dt, sendOutcomeExitCode as E, semanticSearch as En, createWakeSchedule as Et, extractErrorCode as F, stopRepeatFromEmail as Fn, getAgentContactPolicy as Ft, ReplyStateUnsupportedError as G, verifyAgentSignup as Gn, getMemory as Gt, AWAITING_FLAG_DESCRIPTION as H, updateWakeAuthorization as Hn, getFunctionRouting as Ht, extractErrorPayload as I, testFunction as In, getContact as It, ensureSearchReportsReplyState as J, getSentEmail as Jt, assertAwaitingFilterRows as K, getOrgRoutingTopology as Kt, formatErrorPayload as L, unsetFunctionRoute as Ln, getConversation as Lt, API_ERROR_CODES as M, startAgentClaim as Mn, deleteWakeSchedule as Mt, TIME_FLAG_DESCRIPTION as N, startAgentSignup as Nn, downloadEmailAttachmentPart as Nt, serializeErrorPayload as O, setFunctionRoute as On, deleteContact as Ot, createOperationCommand as P, startCliLogin as Pn, getAccount as Pt, isAwaitingRejectedError as Q, listAgentNetworks as Qt, readTextFileFlag as R, updateDefaultNetworkMember as Rn, getCreditBalance as Rt, formatSendRecordFailureSummary as S, resendAgentSignupVerification as Sn, createEndpoint as St, sendIdentityFields as T, searchMemories as Tn, createWakeAuthorization as Tt, AWAITING_VALUES as U, updateWakeSchedule as Un, getFunctionTestRunTrace as Ut, writeErrorWithHints as V, updateRoute as Vn, getFunction as Vt, REPLY_STATE_UNSUPPORTED_CODE as W, verifyAgentClaim as Wn, getInboxStatus as Wt, formatRepliesCell as X, getWakeSchedule as Xt, formatAwaitingCell as Y, getThread as Yt, hasReplyState as Z, listAgentContacts as Zt, deriveSendIdempotencyKey as _, putContact as _n, resolveCliApiRequestConfig as _t, replyExpectation as a, listFunctionLogs as an, AutomatedFilterUnsupportedError as at, formatPriorRepliesWarning as b, reorderRoutes as bn, cliLogout as bt, wakeInteractionSentence as c, listRoutes as cn, ensureSearchReportsAutomated as ct, apiResultHttpStatus as d, listWakeDispatches as dn, isAutomatedRejectedError as dt, listDefaultNetworkAgents as en, formatAlreadySentNotice as et, assertValidIdempotencyKey as f, listWakeSchedules as fn, queryUsesAutomated as ft, classifySendError as g, putAgentContactPolicy as gn, refreshStoredCliCredentials as gt, checkPriorReplies as h, putAgentContact as hn, createCliApiClient as ht, readEmailInteraction as i, listEndpoints as in, AUTOMATED_VALUES as it, API_BASE_URL_FLAG_DESCRIPTION as j, simulateRoute as jn, deleteWakeAuthorization as jt, successfulSendOutcome as k, setFunctionSecret as kn, deleteMemory as kt, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as l, listSentEmails as ln, expectedAutomatedVerdicts as lt, buildThrownSendFailureEnvelope as m, pullWebhookEvent as mn, createAuthenticatedCliApiClient as mt, interactionNextActions as n, listDomains as nn, AUTOMATED_FILTER_UNSUPPORTED_CODE as nt, replyInteractionWarning as o, listFunctions as on, assertAutomatedVerdict as ot, buildFollowUpCommand as p, pollCliLogin as pn, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as pt, awaitingRejectedError as q, getRepeatingSend as qt, isWakeInteractionLabel as r, listEmails as rn, AUTOMATED_FLAG_DESCRIPTION as rt, wakeInteractionLabel as s, listRepeatingSends as sn, automatedRejectedError as st, interactionHumanLine as t, listDefaultNetworkMembers as tn, writeIdempotentReplayBannerIfReplay as tt, SEND_OUTCOME_HELP as u, listWakeAuthorizations as un, hasAutomatedVerdict as ut, formatDeletedEarlierSendNotice as v, redeemCreditCode as vn, addDefaultNetworkMember as vt, reportSendCommandResult as w, searchEmails as wn, createRoute as wt, formatSendFailureSummary as x, replyToEmail as xn, completeWebhookEvent as xt, formatPriorRepliesCheckSkipped as y, removeDefaultNetworkMember as yn, checkDefaultNetworkContactAdmission as yt, runWithTiming as z, updateFunction as zn, getDefaultNetworkAgent as zt };