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.
- package/README.md +5 -4
- package/dist/{api-command-B37DPKFA.js → api-command-BFfgKPTp.js} +985 -36
- package/dist/{json-output-BzSBR-KX.js → json-output-DWrJMakZ.js} +7 -1
- package/dist/{machine-session-g-xPd3X0.js → machine-session-DD35Pfgd.js} +4 -4
- package/dist/{notify-session-Bo8WgG3q.js → notify-session-aMGIFmeu.js} +2 -2
- package/dist/oclif/auto-signal-worker.js +3 -3
- package/dist/oclif/auto-signals.js +1 -1
- package/dist/oclif/command-not-found.js +62 -0
- package/dist/oclif/index.js +4849 -3482
- package/dist/oclif/proxy-auto-detect.js +1 -1
- package/dist/oclif/receiver-heal.js +2 -2
- package/dist/{repeats-Dj_o6c-X-BzXj5ICh.js → repeats-Dj_o6c-X-BwGkVE7e.js} +346 -213
- package/dist/{scoped-chat-TJlyUTi7.js → scoped-chat-B-J3sdNS.js} +1 -1
- package/dist/{signal-command-BO93dM2Q.js → signal-command-DEArW9g5.js} +4 -4
- package/man/primitive.1 +4 -3
- package/package.json +10 -4
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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
|
-
|
|
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[
|
|
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 = () =>
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
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
|
-
|
|
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 {
|
|
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 };
|