@primitivedotdev/cli 0.31.5 → 0.31.7
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 +557 -45
- 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,364 @@ 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 normalizeEmailAddress(value) {
|
|
13891
|
+
return value.trim().toLowerCase();
|
|
13892
|
+
}
|
|
13893
|
+
function derivedReplySubject(parent) {
|
|
13894
|
+
const subject = parent.subject?.trim();
|
|
13895
|
+
if (!subject) return "Re: (no subject)";
|
|
13896
|
+
return /^re:/i.test(subject) ? subject : `Re: ${subject}`;
|
|
13897
|
+
}
|
|
13898
|
+
function assertParentMatchesRecipient(parent, recipient) {
|
|
13899
|
+
if (normalizeEmailAddress(parent.from_email) === normalizeEmailAddress(recipient)) return;
|
|
13900
|
+
throw cliError$6(`Inbound email ${parent.id} is from ${parent.from_email}, not ${recipient}. Use \`primitive chat ${parent.from_email} --reply <message> --reply-to-email-id ${parent.id}\` or omit --reply-to-email-id to continue the latest inbound from ${recipient}.`);
|
|
13901
|
+
}
|
|
13902
|
+
function emailDetailFromEnvelope(envelope) {
|
|
13903
|
+
return envelope?.data ?? envelope ?? null;
|
|
13904
|
+
}
|
|
13905
|
+
function buildCommand(kind, description, argv, options = {}) {
|
|
13906
|
+
const requiresMessage = options.requiresMessage ?? false;
|
|
13907
|
+
return {
|
|
13908
|
+
argv,
|
|
13909
|
+
description,
|
|
13910
|
+
command: commandFromArgv(argv),
|
|
13911
|
+
kind,
|
|
13912
|
+
placeholders: requiresMessage ? [{
|
|
13913
|
+
description: "Replace with the message body before running.",
|
|
13914
|
+
token: "<message>"
|
|
13915
|
+
}] : [],
|
|
13916
|
+
requires_message: requiresMessage
|
|
13917
|
+
};
|
|
13918
|
+
}
|
|
13919
|
+
function buildChatFollowUpCommands(context) {
|
|
13920
|
+
const commands = [];
|
|
13921
|
+
const continueParts = [
|
|
13922
|
+
"primitive",
|
|
13923
|
+
"chat",
|
|
13924
|
+
context.recipient,
|
|
13925
|
+
"--reply",
|
|
13926
|
+
"<message>",
|
|
13927
|
+
"--from",
|
|
13928
|
+
context.from,
|
|
13929
|
+
"--reply-to-email-id",
|
|
13930
|
+
context.reply.id,
|
|
13931
|
+
"--timeout",
|
|
13932
|
+
String(context.timeoutSeconds)
|
|
13933
|
+
];
|
|
13934
|
+
if (context.json) continueParts.push("--json");
|
|
13935
|
+
if (context.quiet) continueParts.push("--quiet");
|
|
13936
|
+
if (context.strictOnly) continueParts.push("--strict-only");
|
|
13937
|
+
else if (context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
|
|
13938
|
+
commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
|
|
13939
|
+
commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
|
|
13940
|
+
"primitive",
|
|
13941
|
+
"reply",
|
|
13942
|
+
"--id",
|
|
13943
|
+
context.reply.id,
|
|
13944
|
+
"--from",
|
|
13945
|
+
context.from,
|
|
13946
|
+
"--body",
|
|
13947
|
+
"<message>"
|
|
13948
|
+
], { requiresMessage: true }));
|
|
13949
|
+
commands.push(buildCommand("inspect_reply", "Inspect the full inbound email", [
|
|
13950
|
+
"primitive",
|
|
13951
|
+
"emails",
|
|
13952
|
+
"get",
|
|
13953
|
+
"--id",
|
|
13954
|
+
context.reply.id
|
|
13955
|
+
]));
|
|
13956
|
+
commands.push(buildCommand("wait_for_more", "Wait for future replies to this send", [
|
|
13957
|
+
"primitive",
|
|
13958
|
+
"emails",
|
|
13959
|
+
"wait",
|
|
13960
|
+
"--reply-to-sent-email-id",
|
|
13961
|
+
context.sent.id,
|
|
13962
|
+
"--to",
|
|
13963
|
+
context.from,
|
|
13964
|
+
"--since",
|
|
13965
|
+
context.reply.received_at,
|
|
13966
|
+
"--timeout",
|
|
13967
|
+
String(context.timeoutSeconds)
|
|
13968
|
+
]));
|
|
13969
|
+
return commands;
|
|
13970
|
+
}
|
|
13971
|
+
function buildChatRecoveryCommands(context) {
|
|
13972
|
+
return [
|
|
13973
|
+
buildCommand("wait_threaded_reply", "Wait for the threaded reply again", [
|
|
13974
|
+
"primitive",
|
|
13975
|
+
"emails",
|
|
13976
|
+
"wait",
|
|
13977
|
+
"--reply-to-sent-email-id",
|
|
13978
|
+
context.sent.id,
|
|
13979
|
+
"--to",
|
|
13980
|
+
context.from,
|
|
13981
|
+
"--since",
|
|
13982
|
+
context.sentAtIso,
|
|
13983
|
+
"--timeout",
|
|
13984
|
+
String(context.timeoutSeconds)
|
|
13985
|
+
]),
|
|
13986
|
+
buildCommand("wait_fallback_reply", "Fallback wait by sender/time window", [
|
|
13987
|
+
"primitive",
|
|
13988
|
+
"emails",
|
|
13989
|
+
"wait",
|
|
13990
|
+
"--from",
|
|
13991
|
+
context.recipient,
|
|
13992
|
+
"--to",
|
|
13993
|
+
context.from,
|
|
13994
|
+
"--since",
|
|
13995
|
+
context.sentAtIso,
|
|
13996
|
+
"--timeout",
|
|
13997
|
+
String(context.timeoutSeconds)
|
|
13998
|
+
]),
|
|
13999
|
+
buildCommand("inspect_sent_email", "Inspect the outbound send", [
|
|
14000
|
+
"primitive",
|
|
14001
|
+
"sent",
|
|
14002
|
+
"get",
|
|
14003
|
+
"--id",
|
|
14004
|
+
context.sent.id
|
|
14005
|
+
])
|
|
14006
|
+
];
|
|
14007
|
+
}
|
|
14008
|
+
function buildChatJsonEnvelope(context) {
|
|
14009
|
+
const responseBody = resolveChatResponseBody(context.reply);
|
|
14010
|
+
return {
|
|
14011
|
+
sent: context.sent,
|
|
14012
|
+
reply: context.reply,
|
|
14013
|
+
response_body: responseBody.body,
|
|
14014
|
+
response_body_format: responseBody.format,
|
|
14015
|
+
match: {
|
|
14016
|
+
description: matchDescription(context.matchStrategy),
|
|
14017
|
+
reply_to_sent_email_id: context.reply.reply_to_sent_email_id ?? null,
|
|
14018
|
+
strategy: context.matchStrategy
|
|
14019
|
+
},
|
|
14020
|
+
follow_up_commands: buildChatFollowUpCommands(context)
|
|
14021
|
+
};
|
|
14022
|
+
}
|
|
14023
|
+
function formatChatResponse(context) {
|
|
14024
|
+
const accepted = context.sent.accepted.join(", ") || context.recipient;
|
|
14025
|
+
const responseBody = resolveChatResponseBody(context.reply);
|
|
14026
|
+
const lines = [
|
|
14027
|
+
"Reply received",
|
|
14028
|
+
"",
|
|
14029
|
+
"Sent",
|
|
14030
|
+
` To: ${accepted}`,
|
|
14031
|
+
` From: ${context.sent.from || context.from}`,
|
|
14032
|
+
` Subject: ${context.subject}`,
|
|
14033
|
+
` Sent email id: ${context.sent.id}`,
|
|
14034
|
+
` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
|
|
14035
|
+
"",
|
|
14036
|
+
"Reply",
|
|
14037
|
+
` Email id: ${context.reply.id}`,
|
|
14038
|
+
` From: ${context.reply.from_email}`,
|
|
14039
|
+
` To: ${context.reply.to_email}`,
|
|
14040
|
+
` Subject: ${context.reply.subject ?? "(no subject)"}`,
|
|
14041
|
+
` Received: ${context.reply.received_at}`,
|
|
14042
|
+
` Match: ${matchDescription(context.matchStrategy)}`
|
|
14043
|
+
];
|
|
14044
|
+
if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
|
|
14045
|
+
if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
|
|
14046
|
+
lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.");
|
|
14047
|
+
for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
|
|
14048
|
+
lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
|
|
14049
|
+
return lines.join("\n");
|
|
14050
|
+
}
|
|
14051
|
+
function formatChatRecoveryContext(context) {
|
|
14052
|
+
const lines = [
|
|
14053
|
+
"",
|
|
14054
|
+
"Sent message context",
|
|
14055
|
+
` To: ${context.sent.accepted.join(", ") || context.recipient}`,
|
|
14056
|
+
` From: ${context.sent.from || context.from}`,
|
|
14057
|
+
` Subject: ${context.subject}`,
|
|
14058
|
+
` Sent email id: ${context.sent.id}`,
|
|
14059
|
+
` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
|
|
14060
|
+
` Poll since: ${context.sentAtIso}`,
|
|
14061
|
+
"",
|
|
14062
|
+
"Helpful recovery commands"
|
|
14063
|
+
];
|
|
14064
|
+
for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}:`, ` ${command}`);
|
|
14065
|
+
return lines.join("\n");
|
|
14066
|
+
}
|
|
14067
|
+
async function loadInboundEmailDetail(params) {
|
|
14068
|
+
const result = await getEmail({
|
|
14069
|
+
client: params.apiClient.client,
|
|
14070
|
+
path: { id: params.id },
|
|
14071
|
+
responseStyle: "fields"
|
|
14072
|
+
});
|
|
14073
|
+
if (result.error) {
|
|
14074
|
+
const payload = extractErrorPayload(result.error);
|
|
14075
|
+
writeErrorWithHints(payload);
|
|
14076
|
+
surfaceUnauthorizedHint({
|
|
14077
|
+
...params.authFailureContext,
|
|
14078
|
+
payload
|
|
14079
|
+
});
|
|
14080
|
+
throw new Errors.CLIError(`Could not load inbound email ${params.id}.`, { exit: 1 });
|
|
14081
|
+
}
|
|
14082
|
+
const detail = emailDetailFromEnvelope(result.data);
|
|
14083
|
+
if (!detail) throw new Errors.CLIError(`Could not load inbound email ${params.id}: the API returned no email body.`, { exit: 1 });
|
|
14084
|
+
return detail;
|
|
14085
|
+
}
|
|
14086
|
+
async function findLatestInboundFromRecipient(params) {
|
|
14087
|
+
const result = await searchEmails({
|
|
14088
|
+
client: params.apiClient.client,
|
|
14089
|
+
query: {
|
|
14090
|
+
from: params.recipient,
|
|
14091
|
+
to: params.from,
|
|
14092
|
+
include_facets: "false",
|
|
14093
|
+
limit: params.pageSize,
|
|
14094
|
+
snippet: "false",
|
|
14095
|
+
sort: "received_at_desc"
|
|
14096
|
+
},
|
|
14097
|
+
responseStyle: "fields"
|
|
14098
|
+
});
|
|
14099
|
+
if (result.error) {
|
|
14100
|
+
const payload = extractErrorPayload(result.error);
|
|
14101
|
+
writeErrorWithHints(payload);
|
|
14102
|
+
surfaceUnauthorizedHint({
|
|
14103
|
+
...params.authFailureContext,
|
|
14104
|
+
payload
|
|
14105
|
+
});
|
|
14106
|
+
throw new Errors.CLIError("Could not find a prior chat reply.", { exit: 1 });
|
|
14107
|
+
}
|
|
14108
|
+
const row = (result.data?.data ?? []).find((email) => email.status === "accepted" || email.status === "completed");
|
|
14109
|
+
if (!row) return null;
|
|
14110
|
+
return loadInboundEmailDetail({
|
|
14111
|
+
apiClient: params.apiClient,
|
|
14112
|
+
authFailureContext: params.authFailureContext,
|
|
14113
|
+
id: row.id
|
|
14114
|
+
});
|
|
14115
|
+
}
|
|
13758
14116
|
var ChatCommand = class ChatCommand extends Command {
|
|
13759
14117
|
static description = `Send a message to an address and wait for the reply.
|
|
13760
14118
|
|
|
@@ -13763,17 +14121,37 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13763
14121
|
\`primitive chat\` is semantic (send + wait for the threaded reply).
|
|
13764
14122
|
|
|
13765
14123
|
The message body can be given as the second positional argument or
|
|
13766
|
-
piped via stdin. The
|
|
13767
|
-
|
|
14124
|
+
piped via stdin. The default output confirms the reply was received,
|
|
14125
|
+
prints exchange metadata, shows the response body, and lists helpful
|
|
14126
|
+
follow-up commands as templates. The default transcript is for humans;
|
|
14127
|
+
agents and scripts should pass --json for parse-safe output.
|
|
14128
|
+
|
|
14129
|
+
To continue an existing chat, pass --reply '<message>'. By default,
|
|
14130
|
+
the CLI replies to the latest inbound email from the recipient to
|
|
14131
|
+
your sender address. For exact continuation, pass
|
|
14132
|
+
--reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
|
|
14133
|
+
reply endpoint, so the reply subject and threading headers are
|
|
14134
|
+
derived from the inbound email instead of copied into CLI flags.
|
|
14135
|
+
|
|
14136
|
+
--json emits a structured envelope with both sides of the exchange,
|
|
14137
|
+
a direct response_body field, match details, and follow-up command
|
|
14138
|
+
metadata such as kind, argv, placeholders, and requires_message.
|
|
13768
14139
|
|
|
13769
|
-
Matching the reply:
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
14140
|
+
Matching the reply: chat first waits in strict threading mode by
|
|
14141
|
+
filtering inbound mail with reply_to_sent_email_id=<sent id>. If
|
|
14142
|
+
no strict match arrives before the strict phase ends, and
|
|
14143
|
+
--strict-only is not set, it falls back to a weaker sender/time
|
|
14144
|
+
window match: from=<recipient>, to=<sender>, and since=<send time>.
|
|
14145
|
+
The fallback can catch clients that strip threading headers, but it
|
|
14146
|
+
is less exact than strict matching. Progress is written to stderr
|
|
14147
|
+
while the CLI waits. Exits non-zero on timeout and prints recovery
|
|
14148
|
+
commands when the send succeeded but no reply was returned.`;
|
|
13773
14149
|
static summary = "Chat with an agent over email (send and wait for the reply)";
|
|
13774
14150
|
static examples = [
|
|
13775
14151
|
"<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
|
|
13776
14152
|
"cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
|
|
14153
|
+
"<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
|
|
14154
|
+
"<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
|
|
13777
14155
|
"<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
|
|
13778
14156
|
"<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
|
|
13779
14157
|
];
|
|
@@ -13801,8 +14179,11 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13801
14179
|
}),
|
|
13802
14180
|
from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
|
|
13803
14181
|
subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
|
|
13804
|
-
|
|
13805
|
-
|
|
14182
|
+
reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
|
|
14183
|
+
"reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
|
|
14184
|
+
"in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
|
|
14185
|
+
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." }),
|
|
14186
|
+
quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
|
|
13806
14187
|
timeout: Flags.integer({
|
|
13807
14188
|
default: DEFAULT_CHAT_TIMEOUT_SECONDS,
|
|
13808
14189
|
description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
|
|
@@ -13830,8 +14211,12 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13830
14211
|
};
|
|
13831
14212
|
async run() {
|
|
13832
14213
|
const { args, flags } = await this.parse(ChatCommand);
|
|
13833
|
-
const
|
|
13834
|
-
if (
|
|
14214
|
+
const replyMode = flags.reply !== void 0 || flags["reply-to-email-id"] !== void 0;
|
|
14215
|
+
if (flags.reply !== void 0 && args.message !== void 0 && args.message !== "") throw cliError$6("Pass the reply body either as --reply or as the positional message, not both.");
|
|
14216
|
+
if (replyMode && flags.subject !== void 0) throw cliError$6("--subject is not used with --reply. Primitive derives the reply subject from the inbound email.");
|
|
14217
|
+
if (replyMode && flags["in-reply-to"] !== void 0) throw cliError$6("Use --reply-to-email-id with --reply instead of raw --in-reply-to.");
|
|
14218
|
+
const message = flags.reply !== void 0 ? flags.reply : args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
|
|
14219
|
+
if (!message.trim()) throw cliError$6(replyMode ? "Reply body is empty." : "Message body is empty.");
|
|
13835
14220
|
await runWithTiming(flags.time, async () => {
|
|
13836
14221
|
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
13837
14222
|
apiKey: flags["api-key"],
|
|
@@ -13844,10 +14229,71 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13844
14229
|
baseUrlOverridden,
|
|
13845
14230
|
configDir: this.config.configDir
|
|
13846
14231
|
};
|
|
13847
|
-
const
|
|
13848
|
-
|
|
14232
|
+
const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
|
|
14233
|
+
let from;
|
|
14234
|
+
let parentReply;
|
|
14235
|
+
let subject;
|
|
14236
|
+
if (replyMode) {
|
|
14237
|
+
const replyContext = await (async () => {
|
|
14238
|
+
let replyContextFailureMessage = "Could not load reply context.";
|
|
14239
|
+
try {
|
|
14240
|
+
if (flags["reply-to-email-id"] !== void 0) {
|
|
14241
|
+
progress?.start(`Loading reply context for ${flags["reply-to-email-id"]}`);
|
|
14242
|
+
const exactParentReply = await loadInboundEmailDetail({
|
|
14243
|
+
apiClient,
|
|
14244
|
+
authFailureContext,
|
|
14245
|
+
id: flags["reply-to-email-id"]
|
|
14246
|
+
});
|
|
14247
|
+
replyContextFailureMessage = `Inbound email ${flags["reply-to-email-id"]} does not match recipient ${args.recipient}.`;
|
|
14248
|
+
assertParentMatchesRecipient(exactParentReply, args.recipient);
|
|
14249
|
+
return {
|
|
14250
|
+
from: flags.from ?? exactParentReply.to_email,
|
|
14251
|
+
parentReply: exactParentReply
|
|
14252
|
+
};
|
|
14253
|
+
}
|
|
14254
|
+
const replyFrom = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
|
|
14255
|
+
progress?.start(`Finding latest inbound email from ${args.recipient}`);
|
|
14256
|
+
const latestParentReply = await findLatestInboundFromRecipient({
|
|
14257
|
+
apiClient,
|
|
14258
|
+
authFailureContext,
|
|
14259
|
+
from: replyFrom,
|
|
14260
|
+
pageSize: flags["page-size"],
|
|
14261
|
+
recipient: args.recipient
|
|
14262
|
+
});
|
|
14263
|
+
if (!latestParentReply) {
|
|
14264
|
+
replyContextFailureMessage = "No prior inbound email found.";
|
|
14265
|
+
throw cliError$6(`No prior inbound email from ${args.recipient} to ${replyFrom}. Start a new chat with \`primitive chat ${args.recipient} <message>\`, pass --from, or pass --reply-to-email-id <inbound-email-id>.`);
|
|
14266
|
+
}
|
|
14267
|
+
replyContextFailureMessage = `Inbound email ${latestParentReply.id} does not match recipient ${args.recipient}.`;
|
|
14268
|
+
assertParentMatchesRecipient(latestParentReply, args.recipient);
|
|
14269
|
+
return {
|
|
14270
|
+
from: replyFrom,
|
|
14271
|
+
parentReply: latestParentReply
|
|
14272
|
+
};
|
|
14273
|
+
} catch (error) {
|
|
14274
|
+
progress?.fail(replyContextFailureMessage);
|
|
14275
|
+
throw error;
|
|
14276
|
+
}
|
|
14277
|
+
})();
|
|
14278
|
+
from = replyContext.from;
|
|
14279
|
+
parentReply = replyContext.parentReply;
|
|
14280
|
+
subject = derivedReplySubject(replyContext.parentReply);
|
|
14281
|
+
} else {
|
|
14282
|
+
from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
|
|
14283
|
+
subject = flags.subject ?? deriveSubject(message);
|
|
14284
|
+
}
|
|
13849
14285
|
const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
13850
|
-
|
|
14286
|
+
if (replyMode) progress?.update(`Sending reply to ${args.recipient}`);
|
|
14287
|
+
else progress?.start(`Sending message to ${args.recipient}`);
|
|
14288
|
+
const sendResult = parentReply !== void 0 ? await replyToEmail({
|
|
14289
|
+
body: {
|
|
14290
|
+
body_text: message,
|
|
14291
|
+
from
|
|
14292
|
+
},
|
|
14293
|
+
client: apiClient.client,
|
|
14294
|
+
path: { id: parentReply.id },
|
|
14295
|
+
responseStyle: "fields"
|
|
14296
|
+
}) : await sendEmail({
|
|
13851
14297
|
body: {
|
|
13852
14298
|
from,
|
|
13853
14299
|
to: args.recipient,
|
|
@@ -13859,6 +14305,7 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13859
14305
|
responseStyle: "fields"
|
|
13860
14306
|
});
|
|
13861
14307
|
if (sendResult.error) {
|
|
14308
|
+
progress?.fail(replyMode ? "Reply send failed." : "Message send failed.");
|
|
13862
14309
|
const errorPayload = extractErrorPayload(sendResult.error);
|
|
13863
14310
|
writeErrorWithHints(errorPayload);
|
|
13864
14311
|
surfaceUnauthorizedHint({
|
|
@@ -13869,39 +14316,78 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
13869
14316
|
return;
|
|
13870
14317
|
}
|
|
13871
14318
|
const sent = sendResult.data?.data;
|
|
13872
|
-
if (!sent)
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
13878
|
-
|
|
14319
|
+
if (!sent) {
|
|
14320
|
+
progress?.fail("Send succeeded but the API returned no data.");
|
|
14321
|
+
throw cliError$6("Send succeeded but the API returned no data.");
|
|
14322
|
+
}
|
|
14323
|
+
const replyAddress = sent.from || from;
|
|
14324
|
+
progress?.update(`${replyMode ? "Reply" : "Message"} sent; waiting for reply from ${args.recipient}`, {
|
|
14325
|
+
heartbeatMs: 15e3,
|
|
14326
|
+
timeoutSeconds: flags.timeout
|
|
14327
|
+
});
|
|
14328
|
+
const baseContext = {
|
|
14329
|
+
from: replyAddress,
|
|
14330
|
+
json: flags.json,
|
|
14331
|
+
parentReply,
|
|
14332
|
+
quiet: flags.quiet,
|
|
13879
14333
|
recipient: args.recipient,
|
|
14334
|
+
sent,
|
|
13880
14335
|
sentAtIso,
|
|
13881
|
-
sentId: sent.id,
|
|
13882
14336
|
strictOnly: flags["strict-only"],
|
|
13883
14337
|
strictPhaseSeconds: flags["strict-phase-seconds"],
|
|
14338
|
+
subject,
|
|
13884
14339
|
timeoutSeconds: flags.timeout
|
|
13885
|
-
}
|
|
13886
|
-
|
|
13887
|
-
|
|
14340
|
+
};
|
|
14341
|
+
let replyResult;
|
|
14342
|
+
try {
|
|
14343
|
+
replyResult = await waitForReply({
|
|
14344
|
+
apiClient,
|
|
14345
|
+
authFailureContext,
|
|
14346
|
+
from: replyAddress,
|
|
14347
|
+
interval: flags.interval,
|
|
14348
|
+
notice: (message) => {
|
|
14349
|
+
if (progress) {
|
|
14350
|
+
progress.notice(message);
|
|
14351
|
+
return;
|
|
14352
|
+
}
|
|
14353
|
+
process.stderr.write(`${message}\n`);
|
|
14354
|
+
},
|
|
14355
|
+
pageSize: flags["page-size"],
|
|
14356
|
+
recipient: args.recipient,
|
|
14357
|
+
sentAtIso,
|
|
14358
|
+
sentId: sent.id,
|
|
14359
|
+
strictOnly: flags["strict-only"],
|
|
14360
|
+
strictPhaseSeconds: flags["strict-phase-seconds"],
|
|
14361
|
+
timeoutSeconds: flags.timeout
|
|
14362
|
+
});
|
|
14363
|
+
} catch (error) {
|
|
14364
|
+
progress?.fail("Reply polling failed.");
|
|
14365
|
+
process.stderr.write(`${formatChatRecoveryContext(baseContext)}\n`);
|
|
14366
|
+
throw error;
|
|
14367
|
+
}
|
|
14368
|
+
if (replyResult === null) {
|
|
14369
|
+
const timeoutMessage = `Timed out after ${flags.timeout}s waiting for a reply from ${args.recipient}.`;
|
|
14370
|
+
progress?.fail(timeoutMessage);
|
|
14371
|
+
if (progress === null) process.stderr.write(`${timeoutMessage}\n`);
|
|
14372
|
+
process.stderr.write(`${formatChatRecoveryContext(baseContext)}\n`);
|
|
13888
14373
|
process.exitCode = 1;
|
|
13889
14374
|
return;
|
|
13890
14375
|
}
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
this.log(body);
|
|
13900
|
-
}
|
|
14376
|
+
progress?.succeed(`Reply received from ${replyResult.reply.from_email}`);
|
|
14377
|
+
const outputContext = {
|
|
14378
|
+
...baseContext,
|
|
14379
|
+
matchStrategy: replyResult.matchStrategy,
|
|
14380
|
+
reply: replyResult.reply
|
|
14381
|
+
};
|
|
14382
|
+
if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
|
|
14383
|
+
else this.log(formatChatResponse(outputContext));
|
|
13901
14384
|
});
|
|
13902
14385
|
}
|
|
13903
14386
|
};
|
|
13904
14387
|
async function waitForReply(params) {
|
|
14388
|
+
const notice = params.notice ?? ((message) => {
|
|
14389
|
+
process.stderr.write(`${message}\n`);
|
|
14390
|
+
});
|
|
13905
14391
|
const totalDeadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
|
|
13906
14392
|
const strictDeadlineFromBudget = Date.now() + params.strictPhaseSeconds * 1e3;
|
|
13907
14393
|
const strictDeadline = params.strictOnly ? totalDeadline : totalDeadline === null ? strictDeadlineFromBudget : Math.min(strictDeadlineFromBudget, totalDeadline);
|
|
@@ -13970,11 +14456,14 @@ async function waitForReply(params) {
|
|
|
13970
14456
|
const detail = envelope?.data ?? envelope ?? null;
|
|
13971
14457
|
if (!detail) throw new Errors.CLIError(`Reply landed but the email body could not be loaded (id=${match.id}).`, { exit: 1 });
|
|
13972
14458
|
if (phase.label === "strict" && detail.reply_to_sent_email_id !== params.sentId) {
|
|
13973
|
-
if (!strictFilterUnsupported)
|
|
14459
|
+
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
14460
|
strictFilterUnsupported = true;
|
|
13975
14461
|
continue;
|
|
13976
14462
|
}
|
|
13977
|
-
return
|
|
14463
|
+
return {
|
|
14464
|
+
reply: detail,
|
|
14465
|
+
matchStrategy: phase.label
|
|
14466
|
+
};
|
|
13978
14467
|
}
|
|
13979
14468
|
if (strictFilterUnsupported && phase.label === "strict") break;
|
|
13980
14469
|
if (lastAccepted !== void 0) continue;
|
|
@@ -13996,6 +14485,33 @@ function redactConfig(config) {
|
|
|
13996
14485
|
environments: Object.fromEntries(Object.entries(config.environments).map(([name, environment]) => [name, redactCliEnvironment(environment)]))
|
|
13997
14486
|
};
|
|
13998
14487
|
}
|
|
14488
|
+
function switchCliEnvironment(configDir, environmentName) {
|
|
14489
|
+
const environment = normalizeCliEnvironmentName(environmentName);
|
|
14490
|
+
const config = loadOrCreateConfig(configDir);
|
|
14491
|
+
if (!config.environments[environment]) throw new Errors.CLIError(`Primitive CLI environment ${environment} is not configured.`, { exit: 1 });
|
|
14492
|
+
const previousEnvironment = resolveConfigEnvironment(config)?.name ?? null;
|
|
14493
|
+
const nextConfig = {
|
|
14494
|
+
...config,
|
|
14495
|
+
current_environment: environment
|
|
14496
|
+
};
|
|
14497
|
+
const shouldClearCredentials = previousEnvironment !== environment;
|
|
14498
|
+
let removedCredentials = false;
|
|
14499
|
+
if (shouldClearCredentials) {
|
|
14500
|
+
const releaseLock = acquireCliCredentialsLock(configDir);
|
|
14501
|
+
try {
|
|
14502
|
+
saveCliConfig(configDir, nextConfig);
|
|
14503
|
+
removedCredentials = existsSync(credentialsPath(configDir));
|
|
14504
|
+
deleteCliCredentials(configDir);
|
|
14505
|
+
} finally {
|
|
14506
|
+
releaseLock();
|
|
14507
|
+
}
|
|
14508
|
+
} else saveCliConfig(configDir, nextConfig);
|
|
14509
|
+
return {
|
|
14510
|
+
environment,
|
|
14511
|
+
previousEnvironment,
|
|
14512
|
+
removedCredentials
|
|
14513
|
+
};
|
|
14514
|
+
}
|
|
13999
14515
|
var ConfigSetCommand = class ConfigSetCommand extends Command {
|
|
14000
14516
|
static summary = "Set a Primitive CLI request environment";
|
|
14001
14517
|
static flags = {
|
|
@@ -14032,20 +14548,16 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
|
|
|
14032
14548
|
};
|
|
14033
14549
|
var ConfigUseCommand = class ConfigUseCommand extends Command {
|
|
14034
14550
|
static summary = "Switch the active Primitive CLI request environment";
|
|
14551
|
+
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
14552
|
static args = { environment: Args.string({
|
|
14036
14553
|
description: "Environment name to use",
|
|
14037
14554
|
required: true
|
|
14038
14555
|
}) };
|
|
14039
14556
|
async run() {
|
|
14040
14557
|
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
|
-
});
|
|
14558
|
+
const { environment, removedCredentials } = switchCliEnvironment(this.config.configDir, args.environment);
|
|
14048
14559
|
process.stderr.write(`Primitive CLI environment ${environment} is active.\n`);
|
|
14560
|
+
if (removedCredentials) process.stderr.write("Removed saved Primitive CLI credentials. Run `primitive signin` to authenticate in the active environment.\n");
|
|
14049
14561
|
}
|
|
14050
14562
|
};
|
|
14051
14563
|
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.7",
|
|
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,
|