@primitivedotdev/cli 0.31.5 → 0.31.6
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/dist/oclif/index.js +410 -38
- package/package.json +1 -1
package/dist/oclif/index.js
CHANGED
|
@@ -13379,7 +13379,7 @@ function surfaceUnauthorizedHint(params) {
|
|
|
13379
13379
|
}
|
|
13380
13380
|
process.stderr.write("Your saved Primitive CLI OAuth session was rejected. If the command was working a moment ago, please retry; brief retries often clear transient rejections. If it keeps failing, run `primitive logout && primitive signin` to mint a fresh session.\n");
|
|
13381
13381
|
}
|
|
13382
|
-
function formatElapsed(ms) {
|
|
13382
|
+
function formatElapsed$1(ms) {
|
|
13383
13383
|
const seconds = ms / 1e3;
|
|
13384
13384
|
if (seconds < 60) return `${seconds.toFixed(2)}s`;
|
|
13385
13385
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -13391,7 +13391,7 @@ async function runWithTiming(enabled, fn) {
|
|
|
13391
13391
|
try {
|
|
13392
13392
|
return await fn();
|
|
13393
13393
|
} finally {
|
|
13394
|
-
process.stderr.write(`[time: ${formatElapsed(Date.now() - start)}]\n`);
|
|
13394
|
+
process.stderr.write(`[time: ${formatElapsed$1(Date.now() - start)}]\n`);
|
|
13395
13395
|
}
|
|
13396
13396
|
}
|
|
13397
13397
|
const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
|
|
@@ -13755,6 +13755,300 @@ async function readStdinToString() {
|
|
|
13755
13755
|
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
13756
13756
|
return Buffer.concat(chunks).toString("utf8");
|
|
13757
13757
|
}
|
|
13758
|
+
var ChatProgressIndicator = class {
|
|
13759
|
+
currentMessage = null;
|
|
13760
|
+
frameIndex = 0;
|
|
13761
|
+
lastLineLength = 0;
|
|
13762
|
+
startedAt;
|
|
13763
|
+
timer = null;
|
|
13764
|
+
constructor(stream = process.stderr, now = Date.now) {
|
|
13765
|
+
this.stream = stream;
|
|
13766
|
+
this.now = now;
|
|
13767
|
+
this.startedAt = this.now();
|
|
13768
|
+
}
|
|
13769
|
+
start(message) {
|
|
13770
|
+
this.stopTimer();
|
|
13771
|
+
this.currentMessage = message;
|
|
13772
|
+
if (this.stream.isTTY) {
|
|
13773
|
+
this.render(message);
|
|
13774
|
+
this.timer = setInterval(() => this.render(message), 120);
|
|
13775
|
+
this.timer.unref?.();
|
|
13776
|
+
return;
|
|
13777
|
+
}
|
|
13778
|
+
this.stream.write(`${message}\n`);
|
|
13779
|
+
}
|
|
13780
|
+
update(message, options = {}) {
|
|
13781
|
+
this.currentMessage = message;
|
|
13782
|
+
if (this.stream.isTTY) {
|
|
13783
|
+
this.stopTimer();
|
|
13784
|
+
this.clearLine();
|
|
13785
|
+
this.render(message);
|
|
13786
|
+
this.timer = setInterval(() => this.render(message), 120);
|
|
13787
|
+
this.timer.unref?.();
|
|
13788
|
+
return;
|
|
13789
|
+
}
|
|
13790
|
+
this.stopTimer();
|
|
13791
|
+
this.stream.write(`${message}\n`);
|
|
13792
|
+
if (options.heartbeatMs !== void 0) {
|
|
13793
|
+
this.timer = setInterval(() => {
|
|
13794
|
+
this.stream.write(`${formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds)}\n`);
|
|
13795
|
+
}, options.heartbeatMs);
|
|
13796
|
+
this.timer.unref?.();
|
|
13797
|
+
}
|
|
13798
|
+
}
|
|
13799
|
+
notice(message) {
|
|
13800
|
+
if (this.stream.isTTY) {
|
|
13801
|
+
const currentMessage = this.currentMessage;
|
|
13802
|
+
this.clearLine();
|
|
13803
|
+
this.stream.write(`${message}\n`);
|
|
13804
|
+
if (currentMessage !== null && this.timer !== null) this.render(currentMessage);
|
|
13805
|
+
return;
|
|
13806
|
+
}
|
|
13807
|
+
this.stream.write(`${message}\n`);
|
|
13808
|
+
}
|
|
13809
|
+
succeed(message) {
|
|
13810
|
+
this.finish(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`);
|
|
13811
|
+
}
|
|
13812
|
+
fail(message) {
|
|
13813
|
+
this.finish(message);
|
|
13814
|
+
}
|
|
13815
|
+
finish(message) {
|
|
13816
|
+
this.stopTimer();
|
|
13817
|
+
this.currentMessage = null;
|
|
13818
|
+
if (this.stream.isTTY) this.clearLine();
|
|
13819
|
+
this.stream.write(`${message}\n`);
|
|
13820
|
+
}
|
|
13821
|
+
render(message) {
|
|
13822
|
+
const frames = [
|
|
13823
|
+
"-",
|
|
13824
|
+
"\\",
|
|
13825
|
+
"|",
|
|
13826
|
+
"/"
|
|
13827
|
+
];
|
|
13828
|
+
const frame = frames[this.frameIndex % frames.length];
|
|
13829
|
+
this.frameIndex += 1;
|
|
13830
|
+
const line = `${frame} ${message} (${formatElapsed(this.now() - this.startedAt)})`;
|
|
13831
|
+
this.lastLineLength = Math.max(this.lastLineLength, line.length);
|
|
13832
|
+
this.stream.write(`\r${line}`);
|
|
13833
|
+
}
|
|
13834
|
+
clearLine() {
|
|
13835
|
+
if (this.lastLineLength > 0) {
|
|
13836
|
+
this.stream.write(`\r${" ".repeat(this.lastLineLength)}\r`);
|
|
13837
|
+
this.lastLineLength = 0;
|
|
13838
|
+
}
|
|
13839
|
+
}
|
|
13840
|
+
stopTimer() {
|
|
13841
|
+
if (this.timer !== null) {
|
|
13842
|
+
clearInterval(this.timer);
|
|
13843
|
+
this.timer = null;
|
|
13844
|
+
}
|
|
13845
|
+
}
|
|
13846
|
+
};
|
|
13847
|
+
function formatElapsed(ms) {
|
|
13848
|
+
const seconds = Math.max(0, Math.round(ms / 1e3));
|
|
13849
|
+
if (seconds < 60) return `${seconds}s`;
|
|
13850
|
+
const minutes = Math.floor(seconds / 60);
|
|
13851
|
+
const remainder = seconds % 60;
|
|
13852
|
+
return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
|
|
13853
|
+
}
|
|
13854
|
+
function formatWaitingHeartbeat(message, elapsedMs, timeoutSeconds) {
|
|
13855
|
+
const timeout = timeoutSeconds === void 0 ? "" : timeoutSeconds === 0 ? ", no timeout" : `, timeout ${formatElapsed(timeoutSeconds * 1e3)}`;
|
|
13856
|
+
return `${message} (${formatElapsed(elapsedMs)} elapsed${timeout})`;
|
|
13857
|
+
}
|
|
13858
|
+
function shellQuote(value) {
|
|
13859
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
13860
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
13861
|
+
}
|
|
13862
|
+
function commandFromArgv(argv) {
|
|
13863
|
+
return argv.map(shellQuote).join(" ");
|
|
13864
|
+
}
|
|
13865
|
+
function resolveChatResponseBody(reply) {
|
|
13866
|
+
if (reply.body_text && reply.body_text.length > 0) return {
|
|
13867
|
+
body: reply.body_text,
|
|
13868
|
+
format: "text"
|
|
13869
|
+
};
|
|
13870
|
+
if (reply.body_html && reply.body_html.length > 0) return {
|
|
13871
|
+
body: reply.body_html,
|
|
13872
|
+
format: "html"
|
|
13873
|
+
};
|
|
13874
|
+
if (reply.body_text !== null && reply.body_text !== void 0) return {
|
|
13875
|
+
body: reply.body_text,
|
|
13876
|
+
format: "text"
|
|
13877
|
+
};
|
|
13878
|
+
if (reply.body_html !== null && reply.body_html !== void 0) return {
|
|
13879
|
+
body: reply.body_html,
|
|
13880
|
+
format: "html"
|
|
13881
|
+
};
|
|
13882
|
+
return {
|
|
13883
|
+
body: "",
|
|
13884
|
+
format: "empty"
|
|
13885
|
+
};
|
|
13886
|
+
}
|
|
13887
|
+
function matchDescription(strategy) {
|
|
13888
|
+
return strategy === "strict" ? "strict, matched by reply_to_sent_email_id" : "fallback, matched by sender/time window";
|
|
13889
|
+
}
|
|
13890
|
+
function buildCommand(kind, description, argv, options = {}) {
|
|
13891
|
+
const requiresMessage = options.requiresMessage ?? false;
|
|
13892
|
+
return {
|
|
13893
|
+
argv,
|
|
13894
|
+
description,
|
|
13895
|
+
command: commandFromArgv(argv),
|
|
13896
|
+
kind,
|
|
13897
|
+
placeholders: requiresMessage ? [{
|
|
13898
|
+
description: "Replace with the message body before running.",
|
|
13899
|
+
token: "<message>"
|
|
13900
|
+
}] : [],
|
|
13901
|
+
requires_message: requiresMessage
|
|
13902
|
+
};
|
|
13903
|
+
}
|
|
13904
|
+
function buildChatFollowUpCommands(context) {
|
|
13905
|
+
const commands = [];
|
|
13906
|
+
const continueParts = [
|
|
13907
|
+
"primitive",
|
|
13908
|
+
"chat",
|
|
13909
|
+
context.recipient,
|
|
13910
|
+
"<message>",
|
|
13911
|
+
"--from",
|
|
13912
|
+
context.from,
|
|
13913
|
+
"--subject",
|
|
13914
|
+
context.subject,
|
|
13915
|
+
"--timeout",
|
|
13916
|
+
String(context.timeoutSeconds)
|
|
13917
|
+
];
|
|
13918
|
+
if (context.reply.message_id) continueParts.push("--in-reply-to", context.reply.message_id);
|
|
13919
|
+
if (context.json) continueParts.push("--json");
|
|
13920
|
+
if (context.quiet) continueParts.push("--quiet");
|
|
13921
|
+
if (context.strictOnly) continueParts.push("--strict-only");
|
|
13922
|
+
else if (context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
|
|
13923
|
+
commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
|
|
13924
|
+
commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
|
|
13925
|
+
"primitive",
|
|
13926
|
+
"reply",
|
|
13927
|
+
"--id",
|
|
13928
|
+
context.reply.id,
|
|
13929
|
+
"--from",
|
|
13930
|
+
context.from,
|
|
13931
|
+
"--body",
|
|
13932
|
+
"<message>"
|
|
13933
|
+
], { requiresMessage: true }));
|
|
13934
|
+
commands.push(buildCommand("inspect_reply", "Inspect the full inbound email", [
|
|
13935
|
+
"primitive",
|
|
13936
|
+
"emails",
|
|
13937
|
+
"get",
|
|
13938
|
+
"--id",
|
|
13939
|
+
context.reply.id
|
|
13940
|
+
]));
|
|
13941
|
+
commands.push(buildCommand("wait_for_more", "Wait for future replies to this send", [
|
|
13942
|
+
"primitive",
|
|
13943
|
+
"emails",
|
|
13944
|
+
"wait",
|
|
13945
|
+
"--reply-to-sent-email-id",
|
|
13946
|
+
context.sent.id,
|
|
13947
|
+
"--to",
|
|
13948
|
+
context.from,
|
|
13949
|
+
"--since",
|
|
13950
|
+
context.reply.received_at,
|
|
13951
|
+
"--timeout",
|
|
13952
|
+
String(context.timeoutSeconds)
|
|
13953
|
+
]));
|
|
13954
|
+
return commands;
|
|
13955
|
+
}
|
|
13956
|
+
function buildChatRecoveryCommands(context) {
|
|
13957
|
+
return [
|
|
13958
|
+
buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
|
|
13959
|
+
"primitive",
|
|
13960
|
+
"emails",
|
|
13961
|
+
"wait",
|
|
13962
|
+
"--reply-to-sent-email-id",
|
|
13963
|
+
context.sent.id,
|
|
13964
|
+
"--to",
|
|
13965
|
+
context.from,
|
|
13966
|
+
"--since",
|
|
13967
|
+
context.sentAtIso,
|
|
13968
|
+
"--timeout",
|
|
13969
|
+
String(context.timeoutSeconds)
|
|
13970
|
+
]),
|
|
13971
|
+
buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
|
|
13972
|
+
"primitive",
|
|
13973
|
+
"emails",
|
|
13974
|
+
"wait",
|
|
13975
|
+
"--from",
|
|
13976
|
+
context.recipient,
|
|
13977
|
+
"--to",
|
|
13978
|
+
context.from,
|
|
13979
|
+
"--since",
|
|
13980
|
+
context.sentAtIso,
|
|
13981
|
+
"--timeout",
|
|
13982
|
+
String(context.timeoutSeconds)
|
|
13983
|
+
]),
|
|
13984
|
+
buildCommand("inspect_sent_email", "Inspect the outbound send", [
|
|
13985
|
+
"primitive",
|
|
13986
|
+
"sent",
|
|
13987
|
+
"get",
|
|
13988
|
+
"--id",
|
|
13989
|
+
context.sent.id
|
|
13990
|
+
])
|
|
13991
|
+
];
|
|
13992
|
+
}
|
|
13993
|
+
function buildChatJsonEnvelope(context) {
|
|
13994
|
+
const responseBody = resolveChatResponseBody(context.reply);
|
|
13995
|
+
return {
|
|
13996
|
+
sent: context.sent,
|
|
13997
|
+
reply: context.reply,
|
|
13998
|
+
response_body: responseBody.body,
|
|
13999
|
+
response_body_format: responseBody.format,
|
|
14000
|
+
match: {
|
|
14001
|
+
description: matchDescription(context.matchStrategy),
|
|
14002
|
+
reply_to_sent_email_id: context.reply.reply_to_sent_email_id ?? null,
|
|
14003
|
+
strategy: context.matchStrategy
|
|
14004
|
+
},
|
|
14005
|
+
follow_up_commands: buildChatFollowUpCommands(context)
|
|
14006
|
+
};
|
|
14007
|
+
}
|
|
14008
|
+
function formatChatResponse(context) {
|
|
14009
|
+
const accepted = context.sent.accepted.join(", ") || context.recipient;
|
|
14010
|
+
const responseBody = resolveChatResponseBody(context.reply);
|
|
14011
|
+
const lines = [
|
|
14012
|
+
"Reply received",
|
|
14013
|
+
"",
|
|
14014
|
+
"Sent",
|
|
14015
|
+
` To: ${accepted}`,
|
|
14016
|
+
` From: ${context.sent.from || context.from}`,
|
|
14017
|
+
` Subject: ${context.subject}`,
|
|
14018
|
+
` Sent email id: ${context.sent.id}`,
|
|
14019
|
+
` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
|
|
14020
|
+
"",
|
|
14021
|
+
"Reply",
|
|
14022
|
+
` Email id: ${context.reply.id}`,
|
|
14023
|
+
` From: ${context.reply.from_email}`,
|
|
14024
|
+
` To: ${context.reply.to_email}`,
|
|
14025
|
+
` Subject: ${context.reply.subject ?? "(no subject)"}`,
|
|
14026
|
+
` Received: ${context.reply.received_at}`,
|
|
14027
|
+
` Match: ${matchDescription(context.matchStrategy)}`
|
|
14028
|
+
];
|
|
14029
|
+
if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
|
|
14030
|
+
if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
|
|
14031
|
+
lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.");
|
|
14032
|
+
for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
|
|
14033
|
+
lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
|
|
14034
|
+
return lines.join("\n");
|
|
14035
|
+
}
|
|
14036
|
+
function formatChatRecoveryContext(context) {
|
|
14037
|
+
const lines = [
|
|
14038
|
+
"",
|
|
14039
|
+
"Sent message context",
|
|
14040
|
+
` To: ${context.sent.accepted.join(", ") || context.recipient}`,
|
|
14041
|
+
` From: ${context.sent.from || context.from}`,
|
|
14042
|
+
` Subject: ${context.subject}`,
|
|
14043
|
+
` Sent email id: ${context.sent.id}`,
|
|
14044
|
+
` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
|
|
14045
|
+
` Poll since: ${context.sentAtIso}`,
|
|
14046
|
+
"",
|
|
14047
|
+
"Helpful recovery commands"
|
|
14048
|
+
];
|
|
14049
|
+
for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}:`, ` ${command}`);
|
|
14050
|
+
return lines.join("\n");
|
|
14051
|
+
}
|
|
13758
14052
|
var ChatCommand = class ChatCommand extends Command {
|
|
13759
14053
|
static description = `Send a message to an address and wait for the reply.
|
|
13760
14054
|
|
|
@@ -13763,13 +14057,24 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13763
14057
|
\`primitive chat\` is semantic (send + wait for the threaded reply).
|
|
13764
14058
|
|
|
13765
14059
|
The message body can be given as the second positional argument or
|
|
13766
|
-
piped via stdin. The
|
|
13767
|
-
|
|
14060
|
+
piped via stdin. The default output confirms the reply was received,
|
|
14061
|
+
prints exchange metadata, shows the response body, and lists helpful
|
|
14062
|
+
follow-up commands as templates. The default transcript is for humans;
|
|
14063
|
+
agents and scripts should pass --json for parse-safe output.
|
|
14064
|
+
|
|
14065
|
+
--json emits a structured envelope with both sides of the exchange,
|
|
14066
|
+
a direct response_body field, match details, and follow-up command
|
|
14067
|
+
metadata such as kind, argv, placeholders, and requires_message.
|
|
13768
14068
|
|
|
13769
|
-
Matching the reply:
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
14069
|
+
Matching the reply: chat first waits in strict threading mode by
|
|
14070
|
+
filtering inbound mail with reply_to_sent_email_id=<sent id>. If
|
|
14071
|
+
no strict match arrives before the strict phase ends, and
|
|
14072
|
+
--strict-only is not set, it falls back to a weaker sender/time
|
|
14073
|
+
window match: from=<recipient>, to=<sender>, and since=<send time>.
|
|
14074
|
+
The fallback can catch clients that strip threading headers, but it
|
|
14075
|
+
is less exact than strict matching. Progress is written to stderr
|
|
14076
|
+
while the CLI waits. Exits non-zero on timeout and prints recovery
|
|
14077
|
+
commands when the send succeeded but no reply was returned.`;
|
|
13773
14078
|
static summary = "Chat with an agent over email (send and wait for the reply)";
|
|
13774
14079
|
static examples = [
|
|
13775
14080
|
"<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
|
|
@@ -13802,7 +14107,8 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13802
14107
|
from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
|
|
13803
14108
|
subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
|
|
13804
14109
|
"in-reply-to": Flags.string({ description: "Message-Id of the parent email to thread this against. Use when continuing a prior conversation from outside the CLI; for an inbound you received via Primitive, prefer `primitive reply --id <inbound-id>`." }),
|
|
13805
|
-
json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply } on stdout instead of
|
|
14110
|
+
json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
|
|
14111
|
+
quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
|
|
13806
14112
|
timeout: Flags.integer({
|
|
13807
14113
|
default: DEFAULT_CHAT_TIMEOUT_SECONDS,
|
|
13808
14114
|
description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
|
|
@@ -13847,6 +14153,8 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13847
14153
|
const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
|
|
13848
14154
|
const subject = flags.subject ?? deriveSubject(message);
|
|
13849
14155
|
const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
14156
|
+
const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
|
|
14157
|
+
progress?.start(`Sending message to ${args.recipient}`);
|
|
13850
14158
|
const sendResult = await sendEmail({
|
|
13851
14159
|
body: {
|
|
13852
14160
|
from,
|
|
@@ -13859,6 +14167,7 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13859
14167
|
responseStyle: "fields"
|
|
13860
14168
|
});
|
|
13861
14169
|
if (sendResult.error) {
|
|
14170
|
+
progress?.fail("Message send failed.");
|
|
13862
14171
|
const errorPayload = extractErrorPayload(sendResult.error);
|
|
13863
14172
|
writeErrorWithHints(errorPayload);
|
|
13864
14173
|
surfaceUnauthorizedHint({
|
|
@@ -13869,39 +14178,76 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13869
14178
|
return;
|
|
13870
14179
|
}
|
|
13871
14180
|
const sent = sendResult.data?.data;
|
|
13872
|
-
if (!sent)
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
14181
|
+
if (!sent) {
|
|
14182
|
+
progress?.fail("Send succeeded but the API returned no data.");
|
|
14183
|
+
throw cliError$6("Send succeeded but the API returned no data.");
|
|
14184
|
+
}
|
|
14185
|
+
progress?.update(`Message sent; waiting for reply from ${args.recipient}`, {
|
|
14186
|
+
heartbeatMs: 15e3,
|
|
14187
|
+
timeoutSeconds: flags.timeout
|
|
14188
|
+
});
|
|
14189
|
+
const baseContext = {
|
|
13876
14190
|
from,
|
|
13877
|
-
|
|
13878
|
-
|
|
14191
|
+
json: flags.json,
|
|
14192
|
+
quiet: flags.quiet,
|
|
13879
14193
|
recipient: args.recipient,
|
|
14194
|
+
sent,
|
|
13880
14195
|
sentAtIso,
|
|
13881
|
-
sentId: sent.id,
|
|
13882
14196
|
strictOnly: flags["strict-only"],
|
|
13883
14197
|
strictPhaseSeconds: flags["strict-phase-seconds"],
|
|
14198
|
+
subject,
|
|
13884
14199
|
timeoutSeconds: flags.timeout
|
|
13885
|
-
}
|
|
13886
|
-
|
|
13887
|
-
|
|
14200
|
+
};
|
|
14201
|
+
let replyResult;
|
|
14202
|
+
try {
|
|
14203
|
+
replyResult = await waitForReply({
|
|
14204
|
+
apiClient,
|
|
14205
|
+
authFailureContext,
|
|
14206
|
+
from,
|
|
14207
|
+
interval: flags.interval,
|
|
14208
|
+
notice: (message) => {
|
|
14209
|
+
if (progress) {
|
|
14210
|
+
progress.notice(message);
|
|
14211
|
+
return;
|
|
14212
|
+
}
|
|
14213
|
+
process.stderr.write(`${message}\n`);
|
|
14214
|
+
},
|
|
14215
|
+
pageSize: flags["page-size"],
|
|
14216
|
+
recipient: args.recipient,
|
|
14217
|
+
sentAtIso,
|
|
14218
|
+
sentId: sent.id,
|
|
14219
|
+
strictOnly: flags["strict-only"],
|
|
14220
|
+
strictPhaseSeconds: flags["strict-phase-seconds"],
|
|
14221
|
+
timeoutSeconds: flags.timeout
|
|
14222
|
+
});
|
|
14223
|
+
} catch (error) {
|
|
14224
|
+
progress?.fail("Reply polling failed.");
|
|
14225
|
+
process.stderr.write(`${formatChatRecoveryContext(baseContext)}\n`);
|
|
14226
|
+
throw error;
|
|
14227
|
+
}
|
|
14228
|
+
if (replyResult === null) {
|
|
14229
|
+
const timeoutMessage = `Timed out after ${flags.timeout}s waiting for a reply from ${args.recipient}.`;
|
|
14230
|
+
progress?.fail(timeoutMessage);
|
|
14231
|
+
if (progress === null) process.stderr.write(`${timeoutMessage}\n`);
|
|
14232
|
+
process.stderr.write(`${formatChatRecoveryContext(baseContext)}\n`);
|
|
13888
14233
|
process.exitCode = 1;
|
|
13889
14234
|
return;
|
|
13890
14235
|
}
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
this.log(body);
|
|
13900
|
-
}
|
|
14236
|
+
progress?.succeed(`Reply received from ${replyResult.reply.from_email}`);
|
|
14237
|
+
const outputContext = {
|
|
14238
|
+
...baseContext,
|
|
14239
|
+
matchStrategy: replyResult.matchStrategy,
|
|
14240
|
+
reply: replyResult.reply
|
|
14241
|
+
};
|
|
14242
|
+
if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
|
|
14243
|
+
else this.log(formatChatResponse(outputContext));
|
|
13901
14244
|
});
|
|
13902
14245
|
}
|
|
13903
14246
|
};
|
|
13904
14247
|
async function waitForReply(params) {
|
|
14248
|
+
const notice = params.notice ?? ((message) => {
|
|
14249
|
+
process.stderr.write(`${message}\n`);
|
|
14250
|
+
});
|
|
13905
14251
|
const totalDeadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
|
|
13906
14252
|
const strictDeadlineFromBudget = Date.now() + params.strictPhaseSeconds * 1e3;
|
|
13907
14253
|
const strictDeadline = params.strictOnly ? totalDeadline : totalDeadline === null ? strictDeadlineFromBudget : Math.min(strictDeadlineFromBudget, totalDeadline);
|
|
@@ -13970,11 +14316,14 @@ async function waitForReply(params) {
|
|
|
13970
14316
|
const detail = envelope?.data ?? envelope ?? null;
|
|
13971
14317
|
if (!detail) throw new Errors.CLIError(`Reply landed but the email body could not be loaded (id=${match.id}).`, { exit: 1 });
|
|
13972
14318
|
if (phase.label === "strict" && detail.reply_to_sent_email_id !== params.sentId) {
|
|
13973
|
-
if (!strictFilterUnsupported)
|
|
14319
|
+
if (!strictFilterUnsupported) notice(params.strictOnly ? "Strict-phase reply matching is not supported by this Primitive API host; --strict-only requires server support so the command will exit without a match." : "Strict-phase reply matching is not supported by this Primitive API host; falling back to time-window matching.");
|
|
13974
14320
|
strictFilterUnsupported = true;
|
|
13975
14321
|
continue;
|
|
13976
14322
|
}
|
|
13977
|
-
return
|
|
14323
|
+
return {
|
|
14324
|
+
reply: detail,
|
|
14325
|
+
matchStrategy: phase.label
|
|
14326
|
+
};
|
|
13978
14327
|
}
|
|
13979
14328
|
if (strictFilterUnsupported && phase.label === "strict") break;
|
|
13980
14329
|
if (lastAccepted !== void 0) continue;
|
|
@@ -13996,6 +14345,33 @@ function redactConfig(config) {
|
|
|
13996
14345
|
environments: Object.fromEntries(Object.entries(config.environments).map(([name, environment]) => [name, redactCliEnvironment(environment)]))
|
|
13997
14346
|
};
|
|
13998
14347
|
}
|
|
14348
|
+
function switchCliEnvironment(configDir, environmentName) {
|
|
14349
|
+
const environment = normalizeCliEnvironmentName(environmentName);
|
|
14350
|
+
const config = loadOrCreateConfig(configDir);
|
|
14351
|
+
if (!config.environments[environment]) throw new Errors.CLIError(`Primitive CLI environment ${environment} is not configured.`, { exit: 1 });
|
|
14352
|
+
const previousEnvironment = resolveConfigEnvironment(config)?.name ?? null;
|
|
14353
|
+
const nextConfig = {
|
|
14354
|
+
...config,
|
|
14355
|
+
current_environment: environment
|
|
14356
|
+
};
|
|
14357
|
+
const shouldClearCredentials = previousEnvironment !== environment;
|
|
14358
|
+
let removedCredentials = false;
|
|
14359
|
+
if (shouldClearCredentials) {
|
|
14360
|
+
const releaseLock = acquireCliCredentialsLock(configDir);
|
|
14361
|
+
try {
|
|
14362
|
+
saveCliConfig(configDir, nextConfig);
|
|
14363
|
+
removedCredentials = existsSync(credentialsPath(configDir));
|
|
14364
|
+
deleteCliCredentials(configDir);
|
|
14365
|
+
} finally {
|
|
14366
|
+
releaseLock();
|
|
14367
|
+
}
|
|
14368
|
+
} else saveCliConfig(configDir, nextConfig);
|
|
14369
|
+
return {
|
|
14370
|
+
environment,
|
|
14371
|
+
previousEnvironment,
|
|
14372
|
+
removedCredentials
|
|
14373
|
+
};
|
|
14374
|
+
}
|
|
13999
14375
|
var ConfigSetCommand = class ConfigSetCommand extends Command {
|
|
14000
14376
|
static summary = "Set a Primitive CLI request environment";
|
|
14001
14377
|
static flags = {
|
|
@@ -14032,20 +14408,16 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
|
|
|
14032
14408
|
};
|
|
14033
14409
|
var ConfigUseCommand = class ConfigUseCommand extends Command {
|
|
14034
14410
|
static summary = "Switch the active Primitive CLI request environment";
|
|
14411
|
+
static description = "Switch the active Primitive CLI request environment. When this switches to a different environment, the CLI removes saved OAuth credentials so the next authenticated command signs in against the newly active API host.";
|
|
14035
14412
|
static args = { environment: Args.string({
|
|
14036
14413
|
description: "Environment name to use",
|
|
14037
14414
|
required: true
|
|
14038
14415
|
}) };
|
|
14039
14416
|
async run() {
|
|
14040
14417
|
const { args } = await this.parse(ConfigUseCommand);
|
|
14041
|
-
const environment =
|
|
14042
|
-
const config = loadOrCreateConfig(this.config.configDir);
|
|
14043
|
-
if (!config.environments[environment]) throw new Errors.CLIError(`Primitive CLI environment ${environment} is not configured.`, { exit: 1 });
|
|
14044
|
-
saveCliConfig(this.config.configDir, {
|
|
14045
|
-
...config,
|
|
14046
|
-
current_environment: environment
|
|
14047
|
-
});
|
|
14418
|
+
const { environment, removedCredentials } = switchCliEnvironment(this.config.configDir, args.environment);
|
|
14048
14419
|
process.stderr.write(`Primitive CLI environment ${environment} is active.\n`);
|
|
14420
|
+
if (removedCredentials) process.stderr.write("Removed saved Primitive CLI credentials. Run `primitive signin` to authenticate in the active environment.\n");
|
|
14049
14421
|
}
|
|
14050
14422
|
};
|
|
14051
14423
|
var ConfigListCommand = class ConfigListCommand extends Command {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/cli",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.6",
|
|
4
4
|
"description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|