primcli 3.0.0-preview.2 → 3.0.0-preview.4

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,11 +1,13 @@
1
- import { $ as AutomatedFilterUnsupportedError, F as assertAwaitingFilterRows, G as WEBHOOK_EVENT_TYPES, I as awaitingRejectedError, Jt as getThread, L as ensureSearchReportsReplyState, Lt as getEmail, P as ReplyStateUnsupportedError, Pn as sdk_gen_exports, U as replyStateSurfaceForOperation, V as isAwaitingRejectedError, W as SENT_EMAIL_EVENT_TYPES, et as assertAutomatedVerdict, it as expectedAutomatedVerdicts, nt as automatedSurfaceForOperation, ot as isAutomatedRejectedError, rt as ensureSearchReportsAutomated, sn as listEndpoints, tt as automatedRejectedError, v as parseRetryAfter } from "./repeats-Dj_o6c-X-BzXj5ICh.js";
1
+ import { F as ReplyStateUnsupportedError, G as SENT_EMAIL_EVENT_TYPES, H as isAwaitingRejectedError, I as assertAwaitingFilterRows, K as WEBHOOK_EVENT_TYPES, L as awaitingRejectedError, Ln as sdk_gen_exports, R as ensureSearchReportsReplyState, Rt as getEmail, W as replyStateSurfaceForOperation, Yt as getThread, at as expectedAutomatedVerdicts, et as AutomatedFilterUnsupportedError, it as ensureSearchReportsAutomated, ln as listEndpoints, nt as automatedRejectedError, rt as automatedSurfaceForOperation, st as isAutomatedRejectedError, tt as assertAutomatedVerdict, v as parseRetryAfter } from "./repeats-Dj_o6c-X-BwGkVE7e.js";
2
2
  import { C as saveCliCredentials, F as agentProfilesDirectory, H as requireDefaultLoginProfile, N as agentProfileDirectory, R as loadConnectedAgentProfile, S as resolveCliAuth, W as PrimitiveApiClient, _ as deleteCliCredentials, b as loadCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, j as AGENT_PROFILE_ENV, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveSignupCredentials, x as normalizeApiBaseUrl, y as detectPrimitiveKeyEnvMisname } from "./cli-config-Ms_0vNjq.js";
3
3
  import "./oclif/global-org-flag.js";
4
4
  import { d as readMailJson, h as writeMailJson, m as withMailLock, p as syncMailDirectory, u as privateMailPermissions } from "./shared-mail-files-BAQSGxxz.js";
5
- import { Command, Errors, Flags } from "@oclif/core";
5
+ import { Args, Command, Errors, Flags } from "@oclif/core";
6
6
  import { existsSync, lstatSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { createHash } from "node:crypto";
8
- import { join } from "node:path";
8
+ import { join, win32 } from "node:path";
9
+ import { spawn } from "node:child_process";
10
+ import { createInterface } from "node:readline/promises";
9
11
  // src/oclif/user-grant.ts
10
12
  /**
11
13
  * CLI user logins: a login as the person, not bound to one organization,
@@ -51,7 +53,7 @@ var CliOrgRefusedError = class extends Errors.CLIError {
51
53
  this.orgId = params.orgId;
52
54
  }
53
55
  };
54
- function isRecord(value) {
56
+ function isRecord$1(value) {
55
57
  return value !== null && typeof value === "object" && !Array.isArray(value);
56
58
  }
57
59
  function apiOrigin(apiBaseUrl) {
@@ -78,7 +80,7 @@ function isUuid(value) {
78
80
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
79
81
  }
80
82
  function isOrgAccessToken(value) {
81
- return isRecord(value) && typeof value.access_token === "string" && value.access_token.length > 0 && value.token_type === "Bearer" && typeof value.expires_in === "number" && value.expires_in > 0 && typeof value.org_id === "string" && (value.org_name === null || typeof value.org_name === "string") && typeof value.oauth_grant_id === "string";
83
+ return isRecord$1(value) && typeof value.access_token === "string" && value.access_token.length > 0 && value.token_type === "Bearer" && typeof value.expires_in === "number" && value.expires_in > 0 && typeof value.org_id === "string" && (value.org_name === null || typeof value.org_name === "string") && typeof value.oauth_grant_id === "string";
82
84
  }
83
85
  function orgTokenFromResponse(token, now = Date.now) {
84
86
  return {
@@ -125,7 +127,7 @@ async function postTokenEndpoint(params) {
125
127
  throw new Errors.CLIError(`Could not reach the Primitive OAuth token endpoint: ${detail}`, { exit: 1 });
126
128
  }
127
129
  const payload = await response.json().catch(() => null);
128
- if (response.ok && isRecord(payload)) return {
130
+ if (response.ok && isRecord$1(payload)) return {
129
131
  ok: true,
130
132
  payload
131
133
  };
@@ -133,7 +135,7 @@ async function postTokenEndpoint(params) {
133
135
  ok: true,
134
136
  payload: {}
135
137
  };
136
- const record = isRecord(payload) ? payload : {};
138
+ const record = isRecord$1(payload) ? payload : {};
137
139
  return {
138
140
  ok: false,
139
141
  status: response.status,
@@ -336,12 +338,12 @@ async function fetchCliOrgs(params) {
336
338
  }
337
339
  const payload = await response.json().catch(() => null);
338
340
  if (!response.ok) {
339
- const error = isRecord(payload) && isRecord(payload.error) ? payload.error : {};
341
+ const error = isRecord$1(payload) && isRecord$1(payload.error) ? payload.error : {};
340
342
  if (response.status === 404 && error.code === "feature_disabled") throw new Errors.CLIError("This Primitive server does not support switching organizations for CLI logins.", { exit: 1 });
341
343
  throw new Errors.CLIError(`Could not list organizations${typeof error.message === "string" ? `: ${error.message}` : ` (HTTP ${response.status}).`}`, { exit: 1 });
342
344
  }
343
- const data = isRecord(payload) ? payload.data : null;
344
- if (!isRecord(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
345
+ const data = isRecord$1(payload) ? payload.data : null;
346
+ if (!isRecord$1(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
345
347
  return data;
346
348
  }
347
349
  /** How long a command waits for another CLI process to finish with the saved login. */
@@ -914,6 +916,470 @@ async function withAttachmentWait(call, options) {
914
916
  }
915
917
  return result;
916
918
  }
919
+ // src/oclif/email-rule-authoring.ts
920
+ const EMAIL_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
921
+ /** Whether `id` has the shape of an email id (a UUID). */
922
+ function isEmailId(id) {
923
+ return EMAIL_ID.test(id);
924
+ }
925
+ function usageError(message) {
926
+ return new Errors.CLIError(message, { exit: 2 });
927
+ }
928
+ function isRecord(value) {
929
+ return value !== null && typeof value === "object" && !Array.isArray(value);
930
+ }
931
+ function errorDetail(error) {
932
+ return error instanceof Error ? error.message : String(error);
933
+ }
934
+ function checkEmailId(id, label) {
935
+ if (!isEmailId(id)) throw usageError(`${label} takes email ids (UUIDs, from \`primitive emails list\`); "${id}" is not one.`);
936
+ return id;
937
+ }
938
+ /**
939
+ * The email ids in a repeatable, comma-separated flag, in the order given and
940
+ * without repeats.
941
+ */
942
+ function parseEmailIdList(values, flag) {
943
+ const ids = [];
944
+ const seen = /* @__PURE__ */ new Set();
945
+ for (const value of values) for (const raw of value.split(",")) {
946
+ const id = raw.trim();
947
+ if (id === "") continue;
948
+ checkEmailId(id, flag);
949
+ if (seen.has(id.toLowerCase())) continue;
950
+ seen.add(id.toLowerCase());
951
+ ids.push(id);
952
+ }
953
+ if (ids.length === 0) throw usageError(`${flag} needs at least one email id.`);
954
+ return ids;
955
+ }
956
+ function assertDisjoint(expectations, label) {
957
+ const match = new Set((expectations.match ?? []).map((id) => id.toLowerCase()));
958
+ const both = (expectations.no_match ?? []).find((id) => match.has(id.toLowerCase()));
959
+ if (both) throw usageError(`${label}: ${both} is expected both to match and not to match; give each email one way.`);
960
+ }
961
+ /**
962
+ * Check a JSON value in the shape of a rule's expectations or a test's
963
+ * `expect`: an object with `match` and `no_match`, each a list of email ids,
964
+ * at least one id in all, and no id in both. Repeats are removed.
965
+ */
966
+ function checkExpectations(value, label) {
967
+ if (!isRecord(value)) throw usageError(`${label} must hold a JSON object such as {"match": ["<email-id>"], "no_match": ["<email-id>"]}.`);
968
+ const expectations = {};
969
+ for (const [key, list] of Object.entries(value)) {
970
+ if (key !== "match" && key !== "no_match") throw usageError(`${label} takes only "match" and "no_match"; "${key}" is not one of them.`);
971
+ if (!Array.isArray(list) || list.some((id) => typeof id !== "string")) throw usageError(`${label}: "${key}" must be a list of email ids.`);
972
+ const ids = [];
973
+ const seen = /* @__PURE__ */ new Set();
974
+ for (const id of list) {
975
+ checkEmailId(id, label);
976
+ if (seen.has(id.toLowerCase())) continue;
977
+ seen.add(id.toLowerCase());
978
+ ids.push(id);
979
+ }
980
+ expectations[key] = ids;
981
+ }
982
+ if ((expectations.match?.length ?? 0) + (expectations.no_match?.length ?? 0) === 0) throw usageError(`${label} needs at least one email id in match or no_match.`);
983
+ assertDisjoint(expectations, label);
984
+ return expectations;
985
+ }
986
+ /** Read and parse a JSON file a flag names, failing like --body-file does. */
987
+ function readJsonFileFlag(path, flag) {
988
+ let text;
989
+ try {
990
+ text = readFileSync(path, "utf8");
991
+ } catch (error) {
992
+ throw new Errors.CLIError(`Could not read ${flag} ${path}: ${errorDetail(error)}`, { exit: 1 });
993
+ }
994
+ try {
995
+ return JSON.parse(text);
996
+ } catch (error) {
997
+ throw new Errors.CLIError(`${flag} ${path} is not valid JSON: ${errorDetail(error)}`, { exit: 1 });
998
+ }
999
+ }
1000
+ /** `expect` for test-email-rule, from --expect-file or the id flags. */
1001
+ function expectBodyFields(flags) {
1002
+ const file = flags["expect-file"];
1003
+ if (typeof file === "string") {
1004
+ const label = `--expect-file ${file}`;
1005
+ return [{
1006
+ flag: "--expect-file",
1007
+ property: "expect",
1008
+ value: checkExpectations(readJsonFileFlag(file, "--expect-file"), label)
1009
+ }];
1010
+ }
1011
+ const expect = {};
1012
+ const given = [];
1013
+ const match = flags["expect-match"];
1014
+ if (Array.isArray(match)) {
1015
+ expect.match = parseEmailIdList(match, "--expect-match");
1016
+ given.push("--expect-match");
1017
+ }
1018
+ const noMatch = flags["expect-no-match"];
1019
+ if (Array.isArray(noMatch)) {
1020
+ expect.no_match = parseEmailIdList(noMatch, "--expect-no-match");
1021
+ given.push("--expect-no-match");
1022
+ }
1023
+ if (given.length === 0) return [];
1024
+ assertDisjoint(expect, given.join(" and "));
1025
+ return [{
1026
+ flag: given.join(", "),
1027
+ property: "expect",
1028
+ value: expect
1029
+ }];
1030
+ }
1031
+ /**
1032
+ * `expectations` for create-email-rule and update-email-rule, from
1033
+ * --expectations-file. A file holding `null` removes the saved expectations,
1034
+ * which only update accepts.
1035
+ */
1036
+ function expectationsBodyFields(allowNull) {
1037
+ return (flags) => {
1038
+ const file = flags["expectations-file"];
1039
+ if (typeof file !== "string") return [];
1040
+ const value = readJsonFileFlag(file, "--expectations-file");
1041
+ if (value === null && !allowNull) throw usageError(`--expectations-file ${file} holds null, which only update-email-rule takes (to remove a rule's saved expectations). A new rule takes an object such as {"match": ["<email-id>"], "no_match": ["<email-id>"]}, or no --expectations-file.`);
1042
+ return [{
1043
+ flag: "--expectations-file",
1044
+ property: "expectations",
1045
+ value: value === null ? null : checkExpectations(value, `--expectations-file ${file}`)
1046
+ }];
1047
+ };
1048
+ }
1049
+ function confidence(value) {
1050
+ return value.toFixed(2);
1051
+ }
1052
+ /**
1053
+ * A fact or column value as rule conditions read it, with its confidence:
1054
+ * `true at 0.69`, or for a multi fact with per-value confidences
1055
+ * `[billing 0.91, invoice 0.62] at 0.80`.
1056
+ */
1057
+ function formatFactValue(fact) {
1058
+ const value = fact?.value;
1059
+ let text;
1060
+ if (Array.isArray(value)) {
1061
+ const per = isRecord(fact?.value_confidences) ? fact.value_confidences : void 0;
1062
+ text = `[${value.map((item) => {
1063
+ const name = String(item);
1064
+ const score = per?.[name];
1065
+ return typeof score === "number" ? `${name} ${confidence(score)}` : name;
1066
+ }).join(", ")}]`;
1067
+ } else if (value === null || value === void 0) text = "null";
1068
+ else if (typeof value === "object") text = JSON.stringify(value);
1069
+ else text = String(value);
1070
+ return typeof fact?.confidence === "number" ? `${text} at ${confidence(fact.confidence)}` : text;
1071
+ }
1072
+ /** One line for a fact: `is_cold_outreach: true at 0.69`. */
1073
+ function formatFact(name, fact) {
1074
+ return `${name}: ${formatFactValue(fact)}`;
1075
+ }
1076
+ function scopeWarningLine(warning) {
1077
+ const message = warning.message?.trim();
1078
+ if (!message) return `Scope warning: ${warning.name} (${warning.reason})`;
1079
+ return message.includes(warning.name) ? `Scope warning: ${message}` : `Scope warning: ${warning.name}: ${message}`;
1080
+ }
1081
+ function scopeWarningLines(warnings) {
1082
+ return Array.isArray(warnings) ? warnings.filter(isRecord).map(scopeWarningLine) : [];
1083
+ }
1084
+ const UNCLASSIFIED_REASONS = {
1085
+ pending: {
1086
+ why: "not classified yet",
1087
+ resolvable: true
1088
+ },
1089
+ classifier_unavailable: {
1090
+ why: "the classifier was unavailable",
1091
+ resolvable: true
1092
+ },
1093
+ classification_failed: {
1094
+ why: "its classification failed",
1095
+ resolvable: false,
1096
+ may: true
1097
+ },
1098
+ not_classifiable: {
1099
+ why: "it cannot be classified",
1100
+ resolvable: false
1101
+ }
1102
+ };
1103
+ function sameId(a, b) {
1104
+ return a.toLowerCase() === b.toLowerCase();
1105
+ }
1106
+ /**
1107
+ * Why an expectation is undecided, from its email's result, and whether a
1108
+ * later test can decide it: not for an email the rule cannot find or read, or
1109
+ * one that cannot be classified; yes for one not classified yet, or whose
1110
+ * question went unanswered.
1111
+ */
1112
+ function undecidedReason(expectation, results) {
1113
+ const result = results.find((entry) => sameId(entry.email_id, expectation.email_id));
1114
+ const outcome = result?.outcome ?? expectation.outcome;
1115
+ if (outcome === "not_found") return {
1116
+ why: "no such email the rule can read",
1117
+ resolvable: false
1118
+ };
1119
+ if (outcome === "unclassified") {
1120
+ const reason = result?.unclassified_reason;
1121
+ return (reason ? UNCLASSIFIED_REASONS[reason] : void 0) ?? {
1122
+ why: reason ? `unclassified (${reason})` : "unclassified",
1123
+ resolvable: false
1124
+ };
1125
+ }
1126
+ return {
1127
+ why: "a question went unanswered",
1128
+ resolvable: true
1129
+ };
1130
+ }
1131
+ function undecidedNote(reason) {
1132
+ return `${reason.why}; ${reason.resolvable ? "test again later" : reason.may ? "a later test may not decide it" : "testing again will not decide it"}`;
1133
+ }
1134
+ function answerText(answer) {
1135
+ const verdict = answer.answer === true ? "yes" : answer.answer === false ? "no" : "unanswered";
1136
+ const probability = typeof answer.probability === "number" ? ` at ${confidence(answer.probability)}` : "";
1137
+ const error = answer.error_class ? ` (${answer.error_class})` : "";
1138
+ return `"${answer.question}" ${verdict}${probability}${error}`;
1139
+ }
1140
+ function conditionLines(summary) {
1141
+ const conditions = Array.isArray(summary?.conditions) ? summary.conditions : [];
1142
+ if (conditions.length === 0) return [];
1143
+ return [`Conditions (${summary?.decided ?? 0} decided):`, ...conditions.map((condition) => ` ${condition.index}. ${condition.text}: held on ${condition.held}, failed on ${condition.failed} (the only failure on ${condition.only_failure})`)];
1144
+ }
1145
+ function questionLines(summary, results) {
1146
+ const questions = Array.isArray(summary?.questions) ? summary.questions : [];
1147
+ if (questions.length === 0) return [];
1148
+ return ["Questions (on emails whose conditions held):", ...questions.map((question) => {
1149
+ const text = results.find((result) => result.question_answers?.[question.index])?.question_answers?.[question.index]?.question;
1150
+ return ` ${question.index}. ${text ? `"${text}": ` : ""}yes ${question.yes}, no ${question.no}, unanswered ${question.unanswered}`;
1151
+ })];
1152
+ }
1153
+ function expectationLine(result, results) {
1154
+ const note = result.status === "undecided" ? undecidedNote(undecidedReason(result, results)) : void 0;
1155
+ return ` ${result.status}: ${result.email_id} expected ${result.expected}, got ${result.outcome}${note ? ` (${note})` : ""}`;
1156
+ }
1157
+ function expectationsLines(expectations, testResults) {
1158
+ if (!expectations) return [];
1159
+ const results = Array.isArray(expectations.results) ? expectations.results : [];
1160
+ const passed = expectations.counts?.passed ?? 0;
1161
+ const failed = expectations.counts?.failed ?? 0;
1162
+ const undecided = expectations.counts?.undecided ?? 0;
1163
+ if (expectations.passed && failed === 0 && undecided === 0) return [`Expectations: all ${passed} passed.`];
1164
+ return [`Expectations: ${[
1165
+ failed > 0 ? `${failed} failed` : "",
1166
+ undecided > 0 ? `${undecided} undecided` : "",
1167
+ `${passed} passed`
1168
+ ].filter((part) => part !== "").join(", ")}.`, ...results.filter((result) => result.status !== "passed").map((result) => expectationLine(result, testResults))];
1169
+ }
1170
+ const NOT_MATCHED_ORDER = {
1171
+ near_miss: 1,
1172
+ no_match: 2,
1173
+ unclassified: 3,
1174
+ not_found: 4
1175
+ };
1176
+ function notMatchedLine(result) {
1177
+ if (result.outcome === "unclassified") {
1178
+ const reason = result.unclassified_reason;
1179
+ const note = !reason ? "" : UNCLASSIFIED_REASONS[reason]?.resolvable ? `${reason}: test again later` : reason;
1180
+ return [` ${result.email_id} unclassified${note ? ` (${note})` : ""}`];
1181
+ }
1182
+ if (result.outcome === "not_found") return [` ${result.email_id} not_found`];
1183
+ const failedParts = (result.parts ?? []).filter((part) => !part.matched).map((part) => part.index);
1184
+ const values = Object.entries(result.values ?? {}).map(([column, value]) => formatFact(column, value));
1185
+ const missing = result.missing_facts ?? [];
1186
+ if (missing.length > 0) values.push(`missing: ${missing.join(", ")}`);
1187
+ const head = ` ${result.email_id} ${result.outcome}${failedParts.length > 0 ? ` (failed part ${failedParts.join(", ")})` : ""}`;
1188
+ const lines = [values.length > 0 ? `${head}: ${values.join("; ")}` : head];
1189
+ const answers = result.question_answers ?? [];
1190
+ if (answers.length > 0) {
1191
+ const source = result.question_answers_source ? ` (${result.question_answers_source})` : "";
1192
+ lines.push(` questions${source}: ${answers.map(answerText).join("; ")}`);
1193
+ }
1194
+ return lines;
1195
+ }
1196
+ function notMatchedLines(results, expectations) {
1197
+ const flagged = new Set((expectations?.results ?? []).filter((result) => result.status !== "passed").map((result) => result.email_id));
1198
+ const rank = (result) => flagged.has(result.email_id) ? 0 : NOT_MATCHED_ORDER[result.outcome] ?? 5;
1199
+ const notMatched = results.filter((result) => result.outcome !== "matched").map((result, position) => ({
1200
+ result,
1201
+ position
1202
+ })).sort((a, b) => rank(a.result) - rank(b.result) || a.position - b.position).map(({ result }) => result);
1203
+ if (notMatched.length === 0) return [];
1204
+ const shown = notMatched.slice(0, 25);
1205
+ return [shown.length < notMatched.length ? `Not matched (${shown.length} of ${notMatched.length} shown; the JSON has every result):` : `Not matched (${notMatched.length}):`, ...shown.flatMap(notMatchedLine)];
1206
+ }
1207
+ /**
1208
+ * The exit code a test's expectations call for, and the error a --json
1209
+ * document carries with it:
1210
+ * - 1 and `expectations_failed` when one failed;
1211
+ * - else 1 and `expectations_unresolvable` when one is undecided and a later
1212
+ * test cannot decide it (its email cannot be found or read, or cannot be
1213
+ * classified);
1214
+ * - else 3 and `expectations_undecided` when one is undecided and a later
1215
+ * test can decide it;
1216
+ * - else nothing: the exit code stays 0.
1217
+ */
1218
+ function expectationsOutcome(expectations, testResults = []) {
1219
+ if (!expectations) return {};
1220
+ const results = Array.isArray(expectations.results) ? expectations.results : [];
1221
+ const failed = results.filter((result) => result.status === "failed").map((result) => result.email_id);
1222
+ const undecided = results.filter((result) => result.status === "undecided").map((result) => ({
1223
+ id: result.email_id,
1224
+ reason: undecidedReason(result, testResults)
1225
+ }));
1226
+ const unresolvable = undecided.filter(({ reason }) => !reason.resolvable);
1227
+ const failedCount = Math.max(expectations.counts?.failed ?? 0, failed.length);
1228
+ const undecidedCount = Math.max(expectations.counts?.undecided ?? 0, undecided.length);
1229
+ const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
1230
+ const why = (list) => list.map(({ id, reason }) => `${id} (${reason.why})`).join(", ");
1231
+ if (failedCount > 0) return {
1232
+ exitCode: 1,
1233
+ error: {
1234
+ code: "expectations_failed",
1235
+ message: `${plural(failedCount, "expectation", "expectations")} failed${failed.length > 0 ? `: ${failed.join(", ")}` : ""}.`
1236
+ }
1237
+ };
1238
+ if (unresolvable.length > 0) return {
1239
+ exitCode: 1,
1240
+ error: {
1241
+ code: "expectations_unresolvable",
1242
+ message: `${plural(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
1243
+ }
1244
+ };
1245
+ if (undecidedCount > 0) return {
1246
+ exitCode: 3,
1247
+ error: {
1248
+ code: "expectations_undecided",
1249
+ message: `${plural(undecidedCount, "expectation is", "expectations are")} undecided${undecided.length > 0 ? `: ${why(undecided)}` : ""}. Test again later.`
1250
+ }
1251
+ };
1252
+ return {};
1253
+ }
1254
+ /** The stderr report and exit code of a test-email-rule answer. */
1255
+ function testEmailRuleReport(data) {
1256
+ if (!isRecord(data)) return { lines: [] };
1257
+ const result = data;
1258
+ const results = Array.isArray(result.results) ? result.results : [];
1259
+ const summary = isRecord(result.summary) ? result.summary : void 0;
1260
+ return {
1261
+ lines: [
1262
+ ...(Array.isArray(summary?.warnings) ? summary.warnings : []).map((warning) => `Warning: ${warning}`),
1263
+ ...scopeWarningLines(result.scope_warnings),
1264
+ ...conditionLines(summary),
1265
+ ...questionLines(summary, results),
1266
+ ...expectationsLines(result.expectations, results),
1267
+ ...notMatchedLines(results, result.expectations)
1268
+ ],
1269
+ ...expectationsOutcome(result.expectations, results)
1270
+ };
1271
+ }
1272
+ function expectationsCheckLine(check, ruleId) {
1273
+ if (!isRecord(check)) return void 0;
1274
+ const on = typeof check.version === "number" ? `version ${check.version}` : "the current version";
1275
+ const failed = Array.isArray(check.failed) ? check.failed : [];
1276
+ const undecided = Array.isArray(check.undecided) ? check.undecided : [];
1277
+ if (check.passed && failed.length === 0 && undecided.length === 0) return `Expectations on ${on}: all passed.`;
1278
+ const parts = [];
1279
+ if (failed.length > 0) parts.push(`${failed.length} failed (${failed.join(", ")})`);
1280
+ if (undecided.length > 0) parts.push(`${undecided.length} undecided (${undecided.join(", ")})`);
1281
+ const rest = undecided.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
1282
+ return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}`;
1283
+ }
1284
+ /** The stderr report of a rule written by create-email-rule or update-email-rule. */
1285
+ function writtenEmailRuleReport(data, request) {
1286
+ if (!isRecord(data)) return { lines: [] };
1287
+ const rule = data;
1288
+ const ruleId = typeof rule.id === "string" ? rule.id : "<rule-id>";
1289
+ const lines = scopeWarningLines(rule.scope_warnings);
1290
+ const checked = expectationsCheckLine(rule.expectations_check, ruleId);
1291
+ if (checked) lines.push(checked);
1292
+ else if (isRecord(request.body) && isRecord(request.body.expectations)) lines.push(`Expectations were not checked on this write. Test them with \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.`);
1293
+ return { lines };
1294
+ }
1295
+ /** The stderr report of get-email-rule-scope. */
1296
+ function emailRuleScopeReport(data) {
1297
+ if (!isRecord(data)) return { lines: [] };
1298
+ const scope = data;
1299
+ const lines = scopeWarningLines(scope.scope_warnings);
1300
+ if (scope.reads_nothing === true) {
1301
+ const stopped = typeof scope.stopped_reason === "string" && scope.stopped_reason ? ` Stopped: ${scope.stopped_reason}.` : "";
1302
+ lines.push(`This rule reads no mail now: it cannot run as its author, or everything its scope names is at none of the organization's domains or no longer read.${stopped}`);
1303
+ }
1304
+ return { lines };
1305
+ }
1306
+ function errorDetails(payload) {
1307
+ if (!isRecord(payload)) return {};
1308
+ if (isRecord(payload.details)) return payload.details;
1309
+ if (isRecord(payload.error) && isRecord(payload.error.details)) return payload.error.details;
1310
+ return {};
1311
+ }
1312
+ /**
1313
+ * The hint for update-email-rule's 409 `email_rule_conflict` with
1314
+ * `details.reason` `version_moved`: the rule's version is no longer the
1315
+ * if_version that was sent, so nothing was saved.
1316
+ */
1317
+ function versionMovedHint(payload, ruleId) {
1318
+ const details = errorDetails(payload);
1319
+ if (details.reason !== "version_moved") return void 0;
1320
+ const id = typeof ruleId === "string" && ruleId ? ruleId : "<rule-id>";
1321
+ const current = typeof details.current_version === "number" ? details.current_version : void 0;
1322
+ const read = `Read it again with \`primitive email-rules get-email-rule ${id}\` and reapply your change to what it shows`;
1323
+ return current === void 0 ? `Hint: the rule's version is not the if_version you sent, so nothing was saved, and the answer did not give its current version. ${read}.` : `Hint: the rule's current version is ${current}, not the if_version you sent, so nothing was saved. ${read}, then retry with --if-version ${current}.`;
1324
+ }
1325
+ const EXPECTATION_FLAG_NOTE = "Replaces any expect in --body-file or --raw-body. The exit code follows the answer's expectations; see --help-full.";
1326
+ const EMAIL_RULE_OPERATION_EXTENSIONS = {
1327
+ testEmailRule: {
1328
+ flags: {
1329
+ "expect-match": Flags.string({
1330
+ description: `An email the rule must match, by id: repeat the flag or give a comma list. Sets expect.match. ${EXPECTATION_FLAG_NOTE}`,
1331
+ multiple: true,
1332
+ exclusive: ["expect-file"]
1333
+ }),
1334
+ "expect-no-match": Flags.string({
1335
+ description: `An email the rule must not match, by id: repeat the flag or give a comma list. Sets expect.no_match. ${EXPECTATION_FLAG_NOTE}`,
1336
+ multiple: true,
1337
+ exclusive: ["expect-file"]
1338
+ }),
1339
+ "expect-file": Flags.string({
1340
+ description: "A JSON file holding the request's expect, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Not with --expect-match or --expect-no-match.",
1341
+ exclusive: ["expect-match", "expect-no-match"]
1342
+ })
1343
+ },
1344
+ bodyFields: expectBodyFields,
1345
+ bodyProperties: ["expect"],
1346
+ fallbackExitCode: (data) => {
1347
+ const expectations = data?.expectations;
1348
+ return expectations && expectations.passed !== true ? 1 : void 0;
1349
+ },
1350
+ report: testEmailRuleReport
1351
+ },
1352
+ createEmailRule: {
1353
+ flags: { "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check." }) },
1354
+ bodyFields: expectationsBodyFields(false),
1355
+ bodyProperties: ["expectations"],
1356
+ report: writtenEmailRuleReport
1357
+ },
1358
+ updateEmailRule: {
1359
+ flags: { "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}, replacing its saved expectations; a file holding null removes them. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check." }) },
1360
+ bodyFields: expectationsBodyFields(true),
1361
+ bodyProperties: ["expectations"],
1362
+ report: writtenEmailRuleReport
1363
+ },
1364
+ getEmailRuleScope: {
1365
+ flags: {},
1366
+ report: emailRuleScopeReport
1367
+ }
1368
+ };
1369
+ /** The classification in a listEmailRuleResults answer, or null. */
1370
+ function classificationOf(data) {
1371
+ if (!isRecord(data) || !isRecord(data.classification)) return null;
1372
+ return data.classification;
1373
+ }
1374
+ /** One line per fact: name, value(s) and confidence. */
1375
+ function classificationFactLines(classification) {
1376
+ return Object.entries(isRecord(classification.facts) ? classification.facts : {}).map(([name, fact]) => formatFact(name, isRecord(fact) ? fact : {}));
1377
+ }
1378
+ /** Why there are no facts to print for an email. */
1379
+ function noFactsMessage(data) {
1380
+ if (classificationOf(data)) return "This email's classification has no facts.";
1381
+ return (isRecord(data) && Array.isArray(data.rules) ? data.rules : []).some((rule) => isRecord(rule) && isRecord(rule.decision) && rule.decision.detail_withheld === true) ? "No classification to show: this email's content cannot be read (withheld, failed to parse or discarded)." : "No classification for this email yet. It has not been classified (recent mail can take a while, so try again later), it will not be, or its content cannot be read.";
1382
+ }
917
1383
  // src/oclif/endpoint-event-types.ts
918
1384
  const EVENT_TYPES_FLAG_OPERATIONS = /* @__PURE__ */ new Set(["createEndpoint", "updateEndpoint"]);
919
1385
  const EVENT_TYPES_FLAG_DESCRIPTION = "Event types this endpoint subscribes to, comma separated or repeated (sets rules.event_types). Omitting the subscription means every event type except the opt-in sent_email.* events, which an endpoint receives only when it lists them; sent_email.* expands to all four. Keep email.received in the list if the endpoint also handles inbound mail. On update, the endpoint's other rules are kept.";
@@ -2735,7 +3201,7 @@ function savedConnectedProfiles(configDir, exclude) {
2735
3201
  */
2736
3202
  async function otherProfiles(configDir, session, current) {
2737
3203
  if (session) try {
2738
- const { connectedProfilesForSession } = await import("./machine-session-g-xPd3X0.js").then((n) => n.s);
3204
+ const { connectedProfilesForSession } = await import("./machine-session-DD35Pfgd.js").then((n) => n.s);
2739
3205
  return {
2740
3206
  inSession: true,
2741
3207
  profiles: connectedProfilesForSession(configDir, session, current)
@@ -2842,6 +3308,256 @@ function sentListOtherSendersNote(rows, self) {
2842
3308
  const named = senders.length > 3 ? `${senders.slice(0, 3).join(", ")} and ${senders.length - 3} more` : senders.join(", ");
2843
3309
  return `${count} of ${rows.length} rows were sent by another address in this organization (${named}) to ${own}; they are mail ${own} received, not its own sends. Rows with from_address ${own} are this address's sends; pass --from ${own} to list only those.`;
2844
3310
  }
3311
+ // src/oclif/browser-login-prompt.ts
3312
+ const { join: join$1 } = win32;
3313
+ /**
3314
+ * The program and arguments that open `url` in the default browser. On
3315
+ * Windows the link goes to the URL protocol handler directly: through `cmd
3316
+ * /c start` an unquoted `&` would end the command, cutting a link such as an
3317
+ * approval page's `?purpose=...&client=...` short.
3318
+ */
3319
+ function browserCommand(url, platform = process.platform) {
3320
+ if (platform === "darwin") return {
3321
+ command: "open",
3322
+ args: [url]
3323
+ };
3324
+ if (platform === "win32") return {
3325
+ command: join$1(process.env.SystemRoot ?? "C:\\Windows", "System32", "rundll32.exe"),
3326
+ args: ["url.dll,FileProtocolHandler", url]
3327
+ };
3328
+ return {
3329
+ command: "xdg-open",
3330
+ args: [url]
3331
+ };
3332
+ }
3333
+ /** The link as an http or https URL, or null for anything else (a path, file:, a share). */
3334
+ function browsableUrl(url) {
3335
+ try {
3336
+ const parsed = new URL(url);
3337
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.href : null;
3338
+ } catch {
3339
+ return null;
3340
+ }
3341
+ }
3342
+ /** Open an http or https link in the default browser; anything else is left to the printed link. */
3343
+ function openBrowser(url) {
3344
+ const link = browsableUrl(url);
3345
+ if (!link) return;
3346
+ const { command, args } = browserCommand(link);
3347
+ const child = spawn(command, args, {
3348
+ detached: true,
3349
+ stdio: "ignore"
3350
+ });
3351
+ child.on("error", () => void 0);
3352
+ child.unref();
3353
+ }
3354
+ /**
3355
+ * The copy-URL hotkey needs raw keypresses from a terminal and a terminal
3356
+ * to show the hint on. Anything else (CI, agents, piped output) gets the
3357
+ * plain URL and never blocks on input.
3358
+ */
3359
+ function canOfferCopyHotkey(input, output) {
3360
+ return Boolean(input.isTTY && output.isTTY && typeof input.setRawMode === "function");
3361
+ }
3362
+ function formatBrowserLoginInstructions(params) {
3363
+ const { copyHotkey, openedBrowser, start } = params;
3364
+ const copyHint = copyHotkey ? " (c to copy)" : "";
3365
+ const lines = [`Your sign-in code is: ${start.user_code}`];
3366
+ if (openedBrowser) lines.push("Opening Primitive in your browser...", "", `Browser didn't open, or want a different one? Use this URL${copyHint}:`);
3367
+ else lines.push("", `Open this URL in any browser to sign in${copyHint}:`);
3368
+ lines.push(` ${start.verification_uri_complete}`);
3369
+ if (start.verification_uri !== start.verification_uri_complete) lines.push(`On another device, go to ${start.verification_uri} and enter ${start.user_code}.`);
3370
+ lines.push("");
3371
+ return `${lines.join("\n")}\n`;
3372
+ }
3373
+ /**
3374
+ * OSC 52 asks the terminal emulator itself to set the clipboard, so it
3375
+ * works over SSH, where a local clipboard command would copy on the
3376
+ * remote host instead of the machine the user is sitting at.
3377
+ */
3378
+ function osc52Sequence(text) {
3379
+ return `\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`;
3380
+ }
3381
+ function nativeClipboardCommand() {
3382
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return null;
3383
+ if (process.platform === "darwin") return ["pbcopy", []];
3384
+ if (process.platform === "win32") return ["clip", []];
3385
+ if (process.env.WAYLAND_DISPLAY) return ["wl-copy", []];
3386
+ if (process.env.DISPLAY) return ["xclip", ["-selection", "clipboard"]];
3387
+ return null;
3388
+ }
3389
+ /**
3390
+ * Resolves false on any failure, including a tool that exits before
3391
+ * reading its input (EPIPE on stdin), so the caller falls back to OSC 52
3392
+ * instead of the error taking down login.
3393
+ */
3394
+ function runClipboardCommand(command, args, text) {
3395
+ return new Promise((resolve) => {
3396
+ let inputFailed = false;
3397
+ const child = spawn(command, args, { stdio: [
3398
+ "pipe",
3399
+ "ignore",
3400
+ "ignore"
3401
+ ] });
3402
+ child.on("error", () => resolve(false));
3403
+ child.stdin.on("error", () => {
3404
+ inputFailed = true;
3405
+ });
3406
+ child.on("close", (code) => resolve(code === 0 && !inputFailed));
3407
+ child.stdin.end(text);
3408
+ });
3409
+ }
3410
+ function copyWithNativeClipboard(text) {
3411
+ const command = nativeClipboardCommand();
3412
+ if (!command) return Promise.resolve(false);
3413
+ return runClipboardCommand(command[0], command[1], text);
3414
+ }
3415
+ /**
3416
+ * Copies via the native clipboard when running locally and always emits
3417
+ * OSC 52 as well. Terminals without OSC 52 support (macOS Terminal.app)
3418
+ * are covered by the native path; SSH sessions are covered by OSC 52.
3419
+ */
3420
+ async function copyToClipboard(text, output, native = copyWithNativeClipboard) {
3421
+ output.write(osc52Sequence(text));
3422
+ return await native(text) ? "native" : "terminal";
3423
+ }
3424
+ const CTRL_C = "";
3425
+ const CTRL_Z = "";
3426
+ /**
3427
+ * Listens for `c` while the CLI polls for browser approval. Raw mode turns
3428
+ * Ctrl+C and Ctrl+Z into plain input, so they are handled here: Ctrl+C is
3429
+ * re-raised as SIGINT, and Ctrl+Z restores the terminal, suspends with
3430
+ * SIGTSTP, and re-enters raw mode on SIGCONT. Several keys can arrive in
3431
+ * one chunk (pastes, SSH), so every character is checked. Returns a
3432
+ * disposer that restores the terminal.
3433
+ */
3434
+ function listenForCopyHotkey(params) {
3435
+ const { input, output, url } = params;
3436
+ const copy = params.copy ?? copyToClipboard;
3437
+ let disposed = false;
3438
+ const copyUrl = () => {
3439
+ copy(url, output).then((how) => {
3440
+ if (disposed) return;
3441
+ output.write(how === "native" ? "Copied the sign-in URL to your clipboard.\n" : "Sent the sign-in URL to your terminal's clipboard. If nothing was copied, select the URL above instead.\n");
3442
+ });
3443
+ };
3444
+ const onContinue = () => {
3445
+ if (!disposed) input.setRawMode?.(true);
3446
+ };
3447
+ const suspend = () => {
3448
+ input.setRawMode?.(false);
3449
+ process.once("SIGCONT", onContinue);
3450
+ process.kill(process.pid, "SIGTSTP");
3451
+ };
3452
+ const onData = (chunk) => {
3453
+ let wantsCopy = false;
3454
+ for (const key of chunk.toString()) {
3455
+ if (key === CTRL_C) {
3456
+ dispose();
3457
+ process.kill(process.pid, "SIGINT");
3458
+ return;
3459
+ }
3460
+ if (key === CTRL_Z && process.platform !== "win32") {
3461
+ if (wantsCopy) copyUrl();
3462
+ suspend();
3463
+ return;
3464
+ }
3465
+ if (key === "c" || key === "C") wantsCopy = true;
3466
+ }
3467
+ if (wantsCopy) copyUrl();
3468
+ };
3469
+ const dispose = () => {
3470
+ if (disposed) return;
3471
+ disposed = true;
3472
+ input.off("data", onData);
3473
+ process.off("SIGCONT", onContinue);
3474
+ input.setRawMode?.(false);
3475
+ input.pause();
3476
+ };
3477
+ input.setRawMode?.(true);
3478
+ input.on("data", onData);
3479
+ input.resume();
3480
+ return dispose;
3481
+ }
3482
+ // src/oclif/step-up-approval.ts
3483
+ const STEP_UP_HEADER = "Primitive-Step-Up";
3484
+ const CONTROL_CHARACTERS = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]`);
3485
+ const STEP_UP_TOKEN_FLAG_DESCRIPTION = "One-time token from the approval page, for a request you already approved in the browser. Sent in the Primitive-Step-Up header.";
3486
+ const BROWSER_FLAG_DESCRIPTION = "Open the approval page in your browser. Use --no-browser to only print its link.";
3487
+ /** The error code of a refusal, whether or not it is still in its envelope. */
3488
+ function errorCode(payload) {
3489
+ if (!payload || typeof payload !== "object") return void 0;
3490
+ const inner = payload.error;
3491
+ if (inner && typeof inner === "object" && typeof inner.code === "string") return inner.code;
3492
+ const code = payload.code;
3493
+ return typeof code === "string" ? code : void 0;
3494
+ }
3495
+ /** The approval link of a `step_up_required` refusal, if that is what it is. */
3496
+ function stepUpUrlOf(payload) {
3497
+ if (errorCode(payload) !== "step_up_required") return void 0;
3498
+ if (!payload || typeof payload !== "object") return void 0;
3499
+ const details = payload.details;
3500
+ if (!details || typeof details !== "object") return void 0;
3501
+ const url = details.step_up_url;
3502
+ return typeof url === "string" && url.length > 0 ? url : void 0;
3503
+ }
3504
+ /**
3505
+ * Send a request, and when the server asks the person to approve it, show
3506
+ * the approval link, open it where possible, wait for the token the page
3507
+ * shows, and send the request once more with it. Without a terminal to wait
3508
+ * in, the refusal is returned with its link, to rerun with --step-up-token.
3509
+ */
3510
+ async function withApproval(attempt, io) {
3511
+ const first = await attempt(io.stepUpToken);
3512
+ if (first.ok) return first;
3513
+ const stepUpUrl = stepUpUrlOf(first.payload);
3514
+ if (!stepUpUrl || io.stepUpToken || !io.interactive) return {
3515
+ ...first,
3516
+ ...stepUpUrl ? { stepUpUrl } : {}
3517
+ };
3518
+ io.write(`Approve this request in your browser: ${stepUpUrl}\n`);
3519
+ if (io.openBrowser) io.open(stepUpUrl);
3520
+ io.write("After you confirm, the page shows a one-time token. Paste it here to finish.\n");
3521
+ const token = (await io.prompt("Token: ")).trim();
3522
+ if (!token) return {
3523
+ ...first,
3524
+ stepUpUrl
3525
+ };
3526
+ if (CONTROL_CHARACTERS.test(token)) {
3527
+ io.write("That token holds a line break or another control character, so it cannot be sent. Copy it again from the page.\n");
3528
+ return {
3529
+ ...first,
3530
+ stepUpUrl
3531
+ };
3532
+ }
3533
+ return attempt(token);
3534
+ }
3535
+ /** The line to print when a refusal asks for an approval the command could not wait for. */
3536
+ function approveThenRerunHint(stepUpUrl) {
3537
+ return `Approve this request in your browser: ${stepUpUrl}\nThen run the same command again with --step-up-token <token from the page>.\n`;
3538
+ }
3539
+ async function promptLine(question) {
3540
+ const rl = createInterface({
3541
+ input: process.stdin,
3542
+ output: process.stderr
3543
+ });
3544
+ try {
3545
+ return await rl.question(question);
3546
+ } finally {
3547
+ rl.close();
3548
+ }
3549
+ }
3550
+ /** Approval in the terminal the command runs in: never under --json, and only with a terminal on both ends. */
3551
+ function terminalApprovalIo(options) {
3552
+ return {
3553
+ ...options.stepUpToken ? { stepUpToken: options.stepUpToken } : {},
3554
+ interactive: !options.json && Boolean(process.stdin.isTTY && process.stderr.isTTY),
3555
+ openBrowser: options.openBrowser,
3556
+ open: openBrowser,
3557
+ prompt: promptLine,
3558
+ write: (text) => process.stderr.write(text)
3559
+ };
3560
+ }
2845
3561
  // src/oclif/api-command.ts
2846
3562
  const API_ERROR_CODES = {
2847
3563
  accessDenied: "access_denied",
@@ -2896,6 +3612,82 @@ function collectHeaderFlags(parsedFlags, headerFlagToName) {
2896
3612
  function flagDescription(parameter) {
2897
3613
  return parameter.description ?? parameter.name;
2898
3614
  }
3615
+ const STEP_UP_OPERATIONS = /* @__PURE__ */ new Set([]);
3616
+ /** Whether an operation takes the browser approval token. */
3617
+ function takesStepUp(operation) {
3618
+ return STEP_UP_OPERATIONS.has(operation.sdkName) || operation.headerParams.some((p) => p.name === "Primitive-Step-Up");
3619
+ }
3620
+ const EMAIL_ID_NOT_SENT = "The id of the received email (from `primitive emails list`), not a sent email id. --id is accepted too.";
3621
+ const OPERATION_PATH_FLAGS = {
3622
+ listEmailRuleResults: { id: {
3623
+ flag: "email-id",
3624
+ description: "The id of the email (from `primitive emails list`), not a rule id: lists what every rule you can see decided about this email, and why a rule did not run. --id is accepted too."
3625
+ } },
3626
+ replyToEmail: { id: {
3627
+ flag: "email-id",
3628
+ description: EMAIL_ID_NOT_SENT
3629
+ } },
3630
+ signalEmail: { id: {
3631
+ flag: "email-id",
3632
+ description: EMAIL_ID_NOT_SENT
3633
+ } },
3634
+ stopRepeatFromEmail: { id: {
3635
+ flag: "email-id",
3636
+ description: EMAIL_ID_NOT_SENT
3637
+ } }
3638
+ };
3639
+ /** End a description with a full stop when it has no closing punctuation. */
3640
+ function sentence(text) {
3641
+ const trimmed = text.trim();
3642
+ return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
3643
+ }
3644
+ /**
3645
+ * The path parameter a command also takes as its first argument: the only
3646
+ * one, when the operation has exactly one (`emails get <id>`). Its flag stays
3647
+ * and is no longer required on its own.
3648
+ */
3649
+ function positionalPathParameter(operation) {
3650
+ return operation.pathParams.length === 1 ? operation.pathParams[0] : void 0;
3651
+ }
3652
+ /** The most rows a paged --limit fetches, in pages of the server's own cap. */
3653
+ const PAGED_LIMIT_MAX = 1e3;
3654
+ /**
3655
+ * The server's per-request cap on --limit for a list the CLI can page itself
3656
+ * (array data, a cursor in meta.cursor, a limit with a maximum), else
3657
+ * undefined. A larger --limit, up to PAGED_LIMIT_MAX, is then fetched in
3658
+ * pages of that size.
3659
+ */
3660
+ function pagedLimitMaximum(operation) {
3661
+ if (takesStepUp(operation)) return void 0;
3662
+ const limit = operation.queryParams.find((p) => p.name === "limit");
3663
+ if (!limit || typeof limit.maximum !== "number") return void 0;
3664
+ if (!pagingFlagOverride(operation, limit)) return void 0;
3665
+ return limit.maximum < 1e3 ? limit.maximum : void 0;
3666
+ }
3667
+ /** The flag a path or query parameter is read from. */
3668
+ function parameterFlagName(sdkName, parameterName) {
3669
+ return (sdkName ? OPERATION_PATH_FLAGS[sdkName]?.[parameterName]?.flag : void 0) ?? flagName(parameterName);
3670
+ }
3671
+ /**
3672
+ * Paging help for a list operation whose data is an array and whose next
3673
+ * cursor is meta.cursor: the page cap on --limit, and where the next cursor is
3674
+ * printed, since the --json output of such a list is the data array alone.
3675
+ * The spec's own text is kept. A list that returns its cursor inside the data
3676
+ * (next_cursor) prints it already and is left as the spec describes it.
3677
+ */
3678
+ function pagingFlagOverride(operation, parameter) {
3679
+ if (!operation.queryParams.some((p) => p.name === "cursor")) return void 0;
3680
+ if (operation.responseSchema?.type !== "array") return void 0;
3681
+ const described = flagDescription(parameter).trim().replace(/\.$/, "");
3682
+ if (parameter.name === "cursor") return { description: `${described}. The next page's cursor is meta.cursor, null on the last page: without --json it is printed on stderr as \`next cursor: <value>\`; with --json, add --envelope to get it, since the bare --json output is the data array alone.` };
3683
+ if (parameter.name === "limit" && typeof parameter.maximum === "number") {
3684
+ if (parameter.maximum < 1e3) {
3685
+ const single = operation.queryParams.some((p) => p.name === "wait") ? " (not with --wait or --since)" : "";
3686
+ return { description: `${described}. The API returns at most ${parameter.maximum} per request; a larger --limit, up to ${PAGED_LIMIT_MAX}, is fetched in pages of ${parameter.maximum}${single}. For more, page with --cursor.` };
3687
+ }
3688
+ return { description: `${described}.${described.includes(String(parameter.maximum)) ? "" : ` At most ${parameter.maximum} per page;`} for more, page with --cursor.`.replace(". for more", ". For more") };
3689
+ }
3690
+ }
2899
3691
  const numberFlag = Flags.custom({ async parse(input, _context, options) {
2900
3692
  const trimmed = input.trim();
2901
3693
  if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
@@ -2988,10 +3780,10 @@ function extractBodyFields(schema) {
2988
3780
  * the natural reading is "use the flags above; --raw-body for
2989
3781
  * the leftovers below."
2990
3782
  */
2991
- function renderRequestSchemaSummary(schema) {
3783
+ function renderRequestSchemaSummary(schema, setByFlags = []) {
2992
3784
  const fields = extractBodyFields(schema);
2993
3785
  if (fields.length === 0) return null;
2994
- const complex = fields.filter((f) => f.kind === "complex");
3786
+ const complex = fields.filter((f) => f.kind === "complex" && !setByFlags.includes(f.name));
2995
3787
  if (complex.length === 0) return null;
2996
3788
  const nameWidth = Math.min(24, Math.max(...complex.map((f) => f.name.length)));
2997
3789
  const descMax = 78;
@@ -3006,10 +3798,11 @@ function renderRequestSchemaSummary(schema) {
3006
3798
  lines.push("(* = required. Scalar body fields are exposed as individual --flag-name flags; see FLAGS above.)");
3007
3799
  return lines.join("\n");
3008
3800
  }
3009
- function flagForParameter(parameter) {
3801
+ function flagForParameter(parameter, override) {
3010
3802
  const common = {
3011
- description: flagDescription(parameter),
3012
- required: parameter.required
3803
+ description: override?.description ?? flagDescription(parameter),
3804
+ required: parameter.required,
3805
+ ...override?.flag ? { aliases: [flagName(parameter.name)] } : {}
3013
3806
  };
3014
3807
  if (parameter.type === "boolean") return Flags.boolean(common);
3015
3808
  if (parameter.type === "integer") return Flags.integer({
@@ -3278,7 +4071,18 @@ function buildFlags(operation) {
3278
4071
  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." });
3279
4072
  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." });
3280
4073
  }
3281
- for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
4074
+ const positional = positionalPathParameter(operation);
4075
+ const pageMaximum = pagedLimitMaximum(operation);
4076
+ for (const parameter of [...operation.pathParams, ...operation.queryParams]) {
4077
+ const override = OPERATION_PATH_FLAGS[operation.sdkName]?.[parameter.name];
4078
+ flags[parameterFlagName(operation.sdkName, parameter.name)] = flagForParameter(parameter === positional ? {
4079
+ ...parameter,
4080
+ required: false
4081
+ } : pageMaximum !== void 0 && parameter.name === "limit" ? {
4082
+ ...parameter,
4083
+ maximum: PAGED_LIMIT_MAX
4084
+ } : parameter, override ?? pagingFlagOverride(operation, parameter));
4085
+ }
3282
4086
  const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
3283
4087
  if (operation.hasJsonBody) {
3284
4088
  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." });
@@ -3302,7 +4106,21 @@ function buildFlags(operation) {
3302
4106
  }
3303
4107
  }
3304
4108
  const headerFlagToName = /* @__PURE__ */ new Map();
4109
+ if (takesStepUp(operation)) {
4110
+ for (const name of ["step-up-token", "browser"]) if (flags[name] !== void 0) throw new Error(`${operation.sdkName}: --${name} is both a parameter and the approval flag`);
4111
+ flags["step-up-token"] = Flags.string({
4112
+ description: STEP_UP_TOKEN_FLAG_DESCRIPTION,
4113
+ aliases: [headerFlagName(STEP_UP_HEADER)]
4114
+ });
4115
+ flags.browser = Flags.boolean({
4116
+ allowNo: true,
4117
+ default: true,
4118
+ description: BROWSER_FLAG_DESCRIPTION
4119
+ });
4120
+ headerFlagToName.set("step-up-token", STEP_UP_HEADER);
4121
+ }
3305
4122
  for (const parameter of operation.headerParams) {
4123
+ if (parameter.name === "Primitive-Step-Up") continue;
3306
4124
  const name = headerFlagName(parameter.name);
3307
4125
  if (RESERVED_FLAG_NAMES.has(name)) continue;
3308
4126
  if (flags[name] !== void 0) continue;
@@ -3319,6 +4137,11 @@ function buildFlags(operation) {
3319
4137
  default: true,
3320
4138
  description: ATTACHMENT_WAIT_FLAG_DESCRIPTION
3321
4139
  });
4140
+ const extension = OPERATION_EXTENSIONS[operation.sdkName];
4141
+ for (const [name, flag] of Object.entries(extension?.flags ?? {})) {
4142
+ if (flags[name] !== void 0) throw new Error(`${operation.sdkName}: --${name} is both a generated flag and a CLI extension flag`);
4143
+ flags[name] = flag;
4144
+ }
3322
4145
  return {
3323
4146
  flags,
3324
4147
  bodyFieldFlagToProperty,
@@ -3346,10 +4169,10 @@ async function applyEventTypesFlag(params) {
3346
4169
  throw error;
3347
4170
  }
3348
4171
  }
3349
- function collectValues(parameters, flags) {
4172
+ function collectValues(parameters, flags, sdkName) {
3350
4173
  const values = {};
3351
4174
  for (const parameter of parameters) {
3352
- const value = coerceParameterValue(parameter, flags[flagName(parameter.name)]);
4175
+ const value = coerceParameterValue(parameter, flags[parameterFlagName(sdkName, parameter.name)]);
3353
4176
  if (value !== void 0) values[parameter.name] = value;
3354
4177
  }
3355
4178
  return values;
@@ -3403,6 +4226,7 @@ const OPERATION_HINTS = {
3403
4226
  deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
3404
4227
  searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
3405
4228
  startAgentSignup: "Tip: pass --terms-accepted. 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.",
4229
+ testEmailRule: "Tip: --expect-match and --expect-no-match (or --expect-file) set expect, the emails the rule must match and must not. When no expect is sent, by these flags or in the body, a saved rule's saved expectations are judged instead. The exit code follows whichever expectations the answer judged: 1 when one failed; 1 when one is undecided and testing again will not decide it (its email cannot be found or read, or cannot be classified); 3 when the only undecided ones can be decided later (not classified yet, or a question unanswered), so test again later. Exit 1 also means the request failed. With --json, error.code tells these apart: expectations_failed, expectations_unresolvable, expectations_undecided (exit 3), or the API's own code. Without --json a readable report follows the JSON on stderr: warnings, how each condition held, the expectations, and the values each email that did not match was compared on.",
3406
4230
  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."
3407
4231
  };
3408
4232
  const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
@@ -3410,9 +4234,11 @@ const OPERATION_EXTRA_BODY_FLAGS = { addDomain: { "mail-relay": {
3410
4234
  property: "mail_relay",
3411
4235
  description: "Keep your existing mailbox provider: the domain's MX points at a Primitive mail relay, which forwards mail to your mailboxes and stores a copy in Primitive. Requires mail relay to be enabled for your account. Google Workspace only for now."
3412
4236
  } } };
4237
+ const OPERATION_EXTENSIONS = { ...EMAIL_RULE_OPERATION_EXTENSIONS };
3413
4238
  const MAIL_RELAY_NOT_ENABLED_MESSAGE = "Mail relay is not enabled for this account, so the domain was not added. Retry without --mail-relay to point the domain's MX at Primitive directly, or ask Primitive to enable mail relay for your account.";
3414
4239
  const MAIL_RELAY_UNAVAILABLE_MESSAGE = "No mail relay is available on this API server right now, so the domain was not added. Retry later, or retry without --mail-relay to point the domain's MX at Primitive directly.";
3415
4240
  const OPERATION_ERROR_HINTS = {
4241
+ updateEmailRule: (code, _body, payload, path) => code === "email_rule_conflict" ? versionMovedHint(payload, path?.id) : void 0,
3416
4242
  verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
3417
4243
  addDomain: (code, body) => {
3418
4244
  if (code === "mail_relay_unavailable") return MAIL_RELAY_UNAVAILABLE_MESSAGE;
@@ -3433,20 +4259,35 @@ const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, api
3433
4259
  function createOperationCommand(operation) {
3434
4260
  const { flags, bodyFieldFlagToProperty, headerFlagToName } = buildFlags(operation);
3435
4261
  const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
3436
- const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
4262
+ const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema, OPERATION_EXTENSIONS[operation.sdkName]?.bodyProperties) : null;
3437
4263
  const hint = OPERATION_HINTS[operation.sdkName];
3438
4264
  const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
3439
4265
  const fullDescription = hint ? `${descriptionWithSchema}\n\n${hint}` : descriptionWithSchema;
3440
4266
  const flagNames = Object.keys(flags);
4267
+ const extension = OPERATION_EXTENSIONS[operation.sdkName];
4268
+ const positional = positionalPathParameter(operation);
4269
+ const positionalFlag = positional ? parameterFlagName(operation.sdkName, positional.name) : void 0;
3441
4270
  class OperationCommand extends Command {
3442
4271
  static description = fullDescription;
3443
4272
  static flags = flags;
4273
+ static args = positional && positionalFlag ? { [positionalFlag]: Args.string({
4274
+ description: `${sentence(OPERATION_PATH_FLAGS[operation.sdkName]?.[positional.name]?.description ?? positional.description ?? positional.name)} The same as --${positionalFlag}; give one of the two.`,
4275
+ required: false
4276
+ }) } : {};
3444
4277
  static summary = operation.summary ?? `${operation.method} ${operation.path}`;
3445
4278
  async run() {
3446
- const { flags } = await this.parse(OperationCommand).catch((error) => {
4279
+ const { args, flags } = await this.parse(OperationCommand).catch((error) => {
3447
4280
  throw withFlagSuggestion(error, flagNames);
3448
4281
  });
3449
4282
  const parsedFlags = flags;
4283
+ if (positionalFlag) {
4284
+ const fromArgument = args[positionalFlag];
4285
+ const fromFlag = parsedFlags[positionalFlag];
4286
+ if (fromArgument !== void 0 && fromFlag !== void 0 && String(fromArgument) !== String(fromFlag)) throw new Errors.CLIError(`Give the ${positionalFlag} once: as the first argument or with --${positionalFlag}, not both with different values.`, { exit: 2 });
4287
+ const value = fromFlag ?? fromArgument;
4288
+ if (value === void 0 || value === "") throw new Errors.CLIError(`Missing the ${positionalFlag}: pass it as the first argument or with --${positionalFlag}.`, { exit: 2 });
4289
+ parsedFlags[positionalFlag] = value;
4290
+ }
3450
4291
  const headers = collectHeaderFlags(parsedFlags, headerFlagToName);
3451
4292
  if ([
3452
4293
  "startAgentSignup",
@@ -3463,6 +4304,11 @@ function createOperationCommand(operation) {
3463
4304
  if (operation.hasJsonBody) {
3464
4305
  const explicit = readJsonBody(parsedFlags);
3465
4306
  const overrides = collectBodyFieldFlags(parsedFlags, bodyFieldFlagToProperty);
4307
+ const extensionFlagFor = /* @__PURE__ */ new Map();
4308
+ for (const field of extension?.bodyFields?.(parsedFlags) ?? []) {
4309
+ overrides[field.property] = field.value;
4310
+ extensionFlagFor.set(field.property, field.flag);
4311
+ }
3466
4312
  if (Object.keys(overrides).length > 0) {
3467
4313
  if (explicit === void 0) body = overrides;
3468
4314
  else if (explicit !== null && typeof explicit === "object" && !Array.isArray(explicit)) body = {
@@ -3471,7 +4317,7 @@ function createOperationCommand(operation) {
3471
4317
  };
3472
4318
  else {
3473
4319
  const explicitKind = explicit === null ? "null" : Array.isArray(explicit) ? "array" : typeof explicit;
3474
- const overrideFlags = Object.keys(overrides).map((p) => `--${flagName(p)}`).join(", ");
4320
+ const overrideFlags = Object.keys(overrides).map((p) => extensionFlagFor.get(p) ?? `--${flagName(p)}`).join(", ");
3475
4321
  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.`);
3476
4322
  }
3477
4323
  } else body = explicit;
@@ -3488,20 +4334,88 @@ function createOperationCommand(operation) {
3488
4334
  });
3489
4335
  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.`);
3490
4336
  const operationFn = sdk_gen_exports[operation.sdkName];
3491
- const query = collectValues(operation.queryParams, parsedFlags);
4337
+ const query = collectValues(operation.queryParams, parsedFlags, operation.sdkName);
3492
4338
  const replyStateSurface = replyStateSurfaceForOperation(operation.sdkName, query);
3493
4339
  const automatedSurface = automatedSurfaceForOperation(operation.sdkName, query);
3494
4340
  const fromVerified = fromVerifiedExpectation(operation.sdkName, query);
3495
- const callOperation = () => operationFn({
3496
- body,
3497
- client: apiClient.client,
3498
- ...Object.keys(headers).length > 0 ? { headers } : {},
3499
- parseAs: operation.binaryResponse ? "blob" : "auto",
3500
- path: collectValues(operation.pathParams, parsedFlags),
3501
- query,
3502
- responseStyle: "fields"
3503
- });
3504
- const result = ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
4341
+ const callOperation = (stepUpToken, pageQuery = query) => {
4342
+ const sent = stepUpToken ? {
4343
+ ...headers,
4344
+ [STEP_UP_HEADER]: stepUpToken
4345
+ } : headers;
4346
+ return operationFn({
4347
+ body,
4348
+ client: apiClient.client,
4349
+ ...Object.keys(sent).length > 0 ? { headers: sent } : {},
4350
+ parseAs: operation.binaryResponse ? "blob" : "auto",
4351
+ path: collectValues(operation.pathParams, parsedFlags, operation.sdkName),
4352
+ query: pageQuery,
4353
+ responseStyle: "fields"
4354
+ });
4355
+ };
4356
+ let stepUpUrl;
4357
+ const callWithApproval = async () => {
4358
+ const approved = await withApproval(async (token) => {
4359
+ const attempt = await callOperation(token);
4360
+ return {
4361
+ ok: !attempt.error,
4362
+ payload: attempt.error ? extractErrorPayload(attempt.error) : void 0,
4363
+ attempt
4364
+ };
4365
+ }, terminalApprovalIo({
4366
+ stepUpToken: typeof parsedFlags["step-up-token"] === "string" ? parsedFlags["step-up-token"] : void 0,
4367
+ openBrowser: parsedFlags.browser !== false,
4368
+ json: parsedFlags.json === true
4369
+ }));
4370
+ stepUpUrl = approved.stepUpUrl;
4371
+ return approved.attempt;
4372
+ };
4373
+ const pageMaximum = pagedLimitMaximum(operation);
4374
+ const wanted = typeof query.limit === "number" ? query.limit : 0;
4375
+ const paged = pageMaximum !== void 0 && wanted > pageMaximum;
4376
+ if (paged) {
4377
+ const single = typeof query.wait === "number" && query.wait > 0 ? "--wait" : query.since !== void 0 ? "--since" : void 0;
4378
+ if (single) throw new Errors.CLIError(`${single} takes a --limit of at most ${pageMaximum}, since it reads one page; a larger --limit is fetched in several requests.`, { exit: 2 });
4379
+ }
4380
+ let pagingNote;
4381
+ const callPaged = async () => {
4382
+ const max = pageMaximum ?? wanted;
4383
+ const rows = [];
4384
+ let sentCursor = query.cursor;
4385
+ let last = await callOperation(void 0, {
4386
+ ...query,
4387
+ limit: Math.min(max, wanted)
4388
+ });
4389
+ const budget = Math.ceil(wanted / max) + 20;
4390
+ for (let requests = 1;; requests += 1) {
4391
+ if (last.error) return last;
4392
+ const envelope = last.data;
4393
+ const page = Array.isArray(envelope?.data) ? envelope.data : [];
4394
+ rows.push(...page);
4395
+ const cursor = envelope?.meta?.cursor ?? void 0;
4396
+ const exhausted = Boolean(cursor) && cursor !== sentCursor && rows.length < wanted && requests >= budget;
4397
+ if (exhausted) pagingNote = `Stopped after ${requests} requests with ${rows.length} of ${wanted}; more may remain. Continue with --cursor ${cursor}.`;
4398
+ if (!cursor || cursor === sentCursor || rows.length >= wanted || exhausted) return {
4399
+ ...last,
4400
+ data: {
4401
+ ...envelope ?? {},
4402
+ data: rows,
4403
+ meta: {
4404
+ ...envelope?.meta ?? {},
4405
+ limit: wanted,
4406
+ cursor: cursor ?? null
4407
+ }
4408
+ }
4409
+ };
4410
+ sentCursor = cursor;
4411
+ last = await callOperation(void 0, {
4412
+ ...query,
4413
+ cursor,
4414
+ limit: Math.min(max, wanted - rows.length)
4415
+ });
4416
+ }
4417
+ };
4418
+ const result = paged ? await callPaged() : takesStepUp(operation) ? await callWithApproval() : ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
3505
4419
  wait: parsedFlags.wait !== false,
3506
4420
  errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
3507
4421
  onFirstWait: () => {
@@ -3511,8 +4425,9 @@ function createOperationCommand(operation) {
3511
4425
  if (result.error) {
3512
4426
  const errorPayload = extractErrorPayload(result.error);
3513
4427
  writeErrorWithHints(errorPayload, result.response);
3514
- const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload);
4428
+ const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName));
3515
4429
  if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
4430
+ if (stepUpUrl) process.stderr.write(approveThenRerunHint(stepUpUrl));
3516
4431
  if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
3517
4432
  if (automatedSurface && isAutomatedRejectedError(errorPayload)) process.stderr.write(`${automatedRejectedError(automatedSurface).message}\n`);
3518
4433
  surfaceUnauthorizedHint({
@@ -3584,6 +4499,7 @@ function createOperationCommand(operation) {
3584
4499
  const withEnvelope = parsedFlags.envelope === true;
3585
4500
  const cursor = envelope?.meta?.cursor;
3586
4501
  if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
4502
+ if (pagingNote && !jsonOutput) process.stderr.write(`${pagingNote}\n`);
3587
4503
  const summary = [];
3588
4504
  if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
3589
4505
  const hint = EMPTY_RESULT_HINTS[operation.sdkName];
@@ -3614,7 +4530,40 @@ function createOperationCommand(operation) {
3614
4530
  const detail = hookError instanceof Error ? hookError.message : String(hookError);
3615
4531
  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`);
3616
4532
  }
3617
- this.log(JSON.stringify(jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope), null, 2));
4533
+ let report;
4534
+ let reportError;
4535
+ try {
4536
+ report = extension?.report?.(envelope?.data, {
4537
+ body,
4538
+ path: collectValues(operation.pathParams, parsedFlags, operation.sdkName)
4539
+ });
4540
+ } catch (error) {
4541
+ reportError = error instanceof Error ? error.message : String(error);
4542
+ let fallback;
4543
+ try {
4544
+ fallback = extension?.fallbackExitCode?.(envelope?.data);
4545
+ } catch {
4546
+ fallback = 1;
4547
+ }
4548
+ if (fallback) report = {
4549
+ lines: [],
4550
+ exitCode: fallback,
4551
+ error: {
4552
+ code: "expectations_not_passed",
4553
+ message: "The answer's expectations did not all pass; read expectations in the answer."
4554
+ }
4555
+ };
4556
+ }
4557
+ let printed = jsonOutput && withEnvelope ? operationJsonDocument(envelope, summary) : operationOutputPayload(envelope, withEnvelope);
4558
+ if (jsonOutput && report?.exitCode && report.error && printed !== null && typeof printed === "object" && !Array.isArray(printed)) printed = {
4559
+ ...printed,
4560
+ error: report.error
4561
+ };
4562
+ this.log(JSON.stringify(printed, null, 2));
4563
+ if (reportError !== void 0) process.stderr.write(`Warning: ${operation.sdkName} succeeded but its report could not be built (${reportError}). The JSON above is the full answer; read it rather than the exit code.\n`);
4564
+ if (report && !jsonOutput) for (const line of report.lines) process.stderr.write(`${line}\n`);
4565
+ if (reportError !== void 0 && report?.exitCode && !jsonOutput) process.stderr.write(`The answer's expectations did not all pass, so the command exits ${report.exitCode}.\n`);
4566
+ if (report?.exitCode) process.exitCode = report.exitCode;
3618
4567
  });
3619
4568
  }
3620
4569
  }
@@ -3631,4 +4580,4 @@ const EMPTY_RESULT_HINTS = {
3631
4580
  function canonicalizeCliReferences(description) {
3632
4581
  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'`");
3633
4582
  }
3634
- export { followUpCommandPrefix as $, clearConsumedPendingMail as A, ConnectedAgentKeyRequiredError as At, replyInteractionWarning as B, fetchCliOrgs as Bt, serverMuted as C, detectRuntimeSession as Ct, wakeRecipientField as D, ATTACHMENT_WAIT_FLAG_DESCRIPTION as Dt, wakeReadCommand as E, withFlagSuggestion as Et, removePendingMail as F, refuseOrgSelectionForProfile as Ft, apiResultHttpStatus as G, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as H, switchCurrentOrg as Ht, interactionHumanLine as I, resolveCliApiRequestConfig as It, buildThrownSendFailureEnvelope as J, assertValidIdempotencyKey as K, interactionNextActions as L, selectedOrgFromEnv as Lt, pendingMailPath as M, createAuthenticatedCliApiClient as Mt, readPendingMail as N, createCliApiClient as Nt, wakeRelationship as O, ATTACHMENT_WAIT_MAX_MS as Ot, recordPendingMail as P, refreshStoredCliCredentials as Pt, excludeSignalReplies as Q, readEmailInteraction as R, CliOrgRefusedError as Rt, sentInThread as S, detectAgentRuntime as St, skillFirstLine as T, formatAlreadySentNotice as Tt, MAX_SEND_WAIT_TIMEOUT_MS as U, wakeInteractionSentence as V, revokeUserLogin as Vt, SEND_OUTCOME_HELP as W, classifySendError as X, checkPriorReplies as Y, deriveSendIdempotencyKey as Z, describeWake as _, defaultConnectionName as _t, extractErrorCode as a, priorRepliesThatWentOut as at, latestOwnOutbound as b, loadSkillLine as bt, hasControlCharacter as c, sendOutcomeExitCode as ct, readTextFileFlag as d, serializeErrorPayload as dt, formatDeletedEarlierSendNotice as et, runWithTiming as f, successfulSendOutcome as ft, bareAddress$1 as g, connectNextSteps as gt, explainPendingMailNotFound as h, cliInvocation as ht, createOperationCommand as i, formatSendRecordFailureSummary as it, clearReadPendingMail as j, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as jt, claimPendingMail as k, withAttachmentWait as kt, headerFlagName as l, sendWaitClampNotice as lt, writeErrorWithHints as m, DEFAULT_ENROLL_NAME as mt, API_ERROR_CODES as n, formatPriorRepliesWarning as nt, extractErrorPayload as o, reportSendCommandResult as ot, surfaceUnauthorizedHint as p, thrownErrorExitCode as pt, buildFollowUpCommand as q, TIME_FLAG_DESCRIPTION as r, formatSendFailureSummary as rt, formatErrorPayload as s, sendIdentityFields as st, API_BASE_URL_FLAG_DESCRIPTION as t, formatPriorRepliesCheckSkipped as tt, isGrantRefusal as u, sentHistoryWindowStart as ut, displayAddress as v, generatedAgentName as vt, serverRelationship as w, runtimeSessionVariableList as wt, readThreadContext as x, currentMailSessionKey as xt, formatMailWakeLine as y, identitySuggestions as yt, replyExpectation as z, credentialsFromUserLogin as zt };
4583
+ export { replyExpectation as $, refuseOrgSelectionForProfile as $t, bareAddress$1 as A, connectNextSteps as At, wakeReadCommand as B, withFlagSuggestion as Bt, terminalApprovalIo as C, sendWaitClampNotice as Ct, listenForCopyHotkey as D, thrownErrorExitCode as Dt, formatBrowserLoginInstructions as E, successfulSendOutcome as Et, readThreadContext as F, currentMailSessionKey as Ft, clearReadPendingMail as G, ATTACHMENT_WAIT_FLAG_DESCRIPTION as Gt, wakeRelationship as H, classificationOf as Ht, sentInThread as I, detectAgentRuntime as It, recordPendingMail as J, ConnectedAgentKeyRequiredError as Jt, pendingMailPath as K, ATTACHMENT_WAIT_MAX_MS as Kt, serverMuted as L, detectRuntimeSession as Lt, displayAddress as M, generatedAgentName as Mt, formatMailWakeLine as N, identitySuggestions as Nt, openBrowser as O, DEFAULT_ENROLL_NAME as Ot, latestOwnOutbound as P, loadSkillLine as Pt, readEmailInteraction as Q, refreshStoredCliCredentials as Qt, serverRelationship as R, runtimeSessionVariableList as Rt, promptLine as S, sendOutcomeExitCode as St, canOfferCopyHotkey as T, serializeErrorPayload as Tt, claimPendingMail as U, isEmailId as Ut, wakeRecipientField as V, classificationFactLines as Vt, clearConsumedPendingMail as W, noFactsMessage as Wt, interactionHumanLine as X, createAuthenticatedCliApiClient as Xt, removePendingMail as Y, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as Yt, interactionNextActions as Z, createCliApiClient as Zt, writeErrorWithHints as _, formatSendFailureSummary as _t, createOperationCommand as a, revokeUserLogin as an, apiResultHttpStatus as at, STEP_UP_TOKEN_FLAG_DESCRIPTION as b, reportSendCommandResult as bt, formatErrorPayload as c, buildThrownSendFailureEnvelope as ct, isGrantRefusal as d, deriveSendIdempotencyKey as dt, resolveCliApiRequestConfig as en, replyInteractionWarning as et, parameterFlagName as f, excludeSignalReplies as ft, takesStepUp as g, formatPriorRepliesWarning as gt, surfaceUnauthorizedHint as h, formatPriorRepliesCheckSkipped as ht, TIME_FLAG_DESCRIPTION as i, fetchCliOrgs as in, SEND_OUTCOME_HELP as it, describeWake as j, defaultConnectionName as jt, explainPendingMailNotFound as k, cliInvocation as kt, hasControlCharacter as l, checkPriorReplies as lt, runWithTiming as m, formatDeletedEarlierSendNotice as mt, API_ERROR_CODES as n, CliOrgRefusedError as nn, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as nt, extractErrorCode as o, switchCurrentOrg as on, assertValidIdempotencyKey as ot, readTextFileFlag as p, followUpCommandPrefix as pt, readPendingMail as q, withAttachmentWait as qt, OPERATION_PATH_FLAGS as r, credentialsFromUserLogin as rn, MAX_SEND_WAIT_TIMEOUT_MS as rt, extractErrorPayload as s, buildFollowUpCommand as st, API_BASE_URL_FLAG_DESCRIPTION as t, selectedOrgFromEnv as tn, wakeInteractionSentence as tt, headerFlagName as u, classifySendError as ut, BROWSER_FLAG_DESCRIPTION as v, formatSendRecordFailureSummary as vt, withApproval as w, sentHistoryWindowStart as wt, approveThenRerunHint as x, sendIdentityFields as xt, STEP_UP_HEADER as y, priorRepliesThatWentOut as yt, skillFirstLine as z, formatAlreadySentNotice as zt };