@agentcomms/resend 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,12 +17,13 @@ code, but the key itself would not stop anything else that held it. A sending-on
17
17
  enforces that — and it cannot read anything, so reads with it are reported as unavailable. `agent-resend doctor`
18
18
  says this on every run.
19
19
 
20
- **Nothing is sent without a person.** A send is prepared, previewed — every recipient, BCC included, the reach
21
- (unique recipients), the From domain and whether it is verified — and approved: a yes in the conversation under the
22
- account's `chat` policy, or `agent-resend approve <id>` and a typed code under `confirm`. Above ten recipients, or to
23
- an address that arrived in mail read here, a person at a terminal approves, whatever the policy. The approval id is
24
- the request's `Idempotency-Key` and an `agentcomms_approval` tag; a send whose outcome is unknown is never repeated,
25
- only checked.
20
+ **Nothing is sent without a person.** A send is prepared, previewed — every recipient, BCC included, the reach (unique
21
+ recipients), the From domain and whether it is verified — and approved: a yes in the conversation under the account's
22
+ `chat` policy, or the approve command the result gives and a typed code under `confirm` — which the agent learns of
23
+ with `resend_send_wait`. Above ten recipients, or to an address that arrived in mail read here, a person at a terminal
24
+ approves, whatever the policy. The approval id is the request's `Idempotency-Key` and an `agentcomms_approval` tag; a
25
+ send whose outcome is unknown (`SEND_OUTCOME_UNKNOWN`) is never repeated, only checked, and a scheduled one is
26
+ "accepted", never "sent", until Resend's own last event for it says so.
26
27
 
27
28
  **The key is typed by a person, at a terminal.** `agent-resend account add` reads it from a hidden prompt, or from
28
29
  `RESEND_API_KEY` in that terminal, and stores it in the system keychain. No MCP tool accepts a key: a key typed into
@@ -96,9 +97,10 @@ terminal), `approve` (under `confirm`, a person at a terminal) and `mcp` (it sta
96
97
  | `agent-resend send prepare` | `resend_send_prepare` |
97
98
  | `agent-resend send execute <approvalId>` | `resend_send_execute` |
98
99
  | `agent-resend send status <approvalId>` | `resend_send_status` |
100
+ | `agent-resend send wait <approvalId>` | `resend_send_wait` |
99
101
  | `agent-resend scheduled list` | `resend_scheduled_list` |
100
102
  | `agent-resend scheduled cancel <id>` | `resend_scheduled_cancel` |
101
- | `agent-resend approve <approvalId>` | — a person at a terminal |
103
+ | `agent-resend approve <approvalId>` | — a person at a terminal, with the command the result gives |
102
104
  | `agent-resend mcp` | — it starts the server |
103
105
  | `agent-resend mcp install --client <client>` | `comms_server_install` (core) |
104
106
  | `agent-resend mcp prune` | `comms_server_prune` (core) |
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as inlineCommand, B as agentMarker, C as policyChange, D as sendPolicyOf, E as removeAccountChange, F as CHANNEL_SERVERS, H as approvalsOf, I as CommsError, J as commandPathOf, L as EXIT_CODES, N as checkNewName, O as showAccount, Q as gatedChangeAtTerminal, R as SEND_LOOKUP, S as policyApprovalRefusal, T as policyWanted, U as approveChangeAtTerminal, V as approvalKind, W as canPrompt, X as exemptFromUpdateGate, Y as escapeForDisplay, _ as runDoctor, a as revokeSendApproval, at as renderPrune, b as listAccounts, c as downloadReceived, ct as serverInstallChange, d as listReceived, dt as terminalUpdateHooks, et as installExitStatus, f as listScheduled, g as showSentEmail, gt as writeResult, h as showReceived, i as prepareSend, it as renderInstall, k as ResendContext, l as getMetrics, lt as serverPruneChange, m as listSuppressions, mt as updateGateAtTerminal, n as executeSend, nt as paint, o as sendStatus, ot as runCommand, p as listSentEmails, pt as truncateDisplay, q as colorEnabled, r as finishSendApproval, rt as refuseUnclaimedApproval, s as cancelScheduledChange, st as runUpdateCheckChild, t as beginSendApproval, tt as openCore, u as listDomains, ut as shellCommand, v as addAccountChange, w as policyReport, x as modeOf, xt as VERSION, y as inspectKey, z as UPDATE_CHECK_CHILD_COMMAND } from "./send-D2ZOWO_6.mjs";
2
+ import { $ as exemptFromUpdateGate, B as UPDATE_CHECK_CHILD_COMMAND, C as policyChange, D as sendPolicyOf, E as removeAccountChange, Et as writeResult, F as CHANNEL_SERVERS, G as canPrompt, H as approvalsOf, I as CommsError, J as cliHandoffs, L as EXIT_CODES, N as checkNewName, Nt as VERSION, O as showAccount, Ot as RESEND_CALLER, Q as escapeForDisplay, R as PATH_OPTIONS, S as policyApprovalRefusal, T as policyWanted, Tt as wholeNumber, U as approveChangeAtTerminal, V as agentMarker, W as approveRefusedHint, X as commandPathOf, Y as colorEnabled, Z as describeFenceReport, _ as runDoctor, _t as serverPruneChange, a as revokeSendApproval, at as kindOf, b as listAccounts, bt as truncateDisplay, c as downloadReceived, ct as pathOverridesFromCliOptions, d as listReceived, dt as renderInstall, f as listScheduled, ft as renderPrune, g as showSentEmail, gt as serverInstallChange, h as showReceived, ht as runUpdateCheckChild, i as prepareSend, it as installExitStatus, k as ResendContext, l as getMetrics, lt as refuseUnclaimedApproval, m as listSuppressions, mt as runCommand, n as executeSend, nt as handoffSentence, o as sendStatus, ot as openCore, p as listSentEmails, pt as resolvePaths, r as finishSendApproval, rt as handoffSentenceToFill, s as cancelScheduledChange, st as paint, t as beginSendApproval, tt as gatedChangeAtTerminal, u as listDomains, ut as renderApprovalWait, v as addAccountChange, vt as terminalUpdateHooks, w as policyReport, wt as waitForApproval, x as modeOf, xt as updateGateAtTerminal, y as inspectKey, z as SEND_LOOKUP } from "./send-9pskzeUY.mjs";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stripVTControlCharacters } from "node:util";
@@ -3040,12 +3040,12 @@ const KEY_SHAPE = /^re_[A-Za-z0-9_]{8,200}$/;
3040
3040
  async function readApiKey(env, streams, options) {
3041
3041
  const marker = agentMarker(env);
3042
3042
  if (marker) throw new CommsError("AUTH_REQUIRED", "a Resend API key is added by a person at their own terminal, not by an agent", {
3043
- hint: `Ask the user to run ${inlineCommand(options.command)} in their own terminal. Never paste a key into a chat: the transcript keeps it.`,
3043
+ hint: `${handoffSentence(options.command, (command) => `Ask the user to run ${command} in their own terminal.`)} Never paste a key into a chat: the transcript keeps it.`,
3044
3044
  details: { marker }
3045
3045
  });
3046
3046
  let key = env["RESEND_API_KEY"]?.trim() ?? "";
3047
3047
  if (key === "") {
3048
- if (!canPrompt(env, streams, { json: options.json })) throw new CommsError("AUTH_REQUIRED", "a Resend API key is needed, and there is no terminal to ask for it", { hint: `A person runs ${inlineCommand(options.command)} in a terminal, which asks for the key without showing it, or sets ${KEY_ENV} for that one command.` });
3048
+ if (!canPrompt(env, streams, { json: options.json })) throw new CommsError("AUTH_REQUIRED", "a Resend API key is needed, and there is no terminal to ask for it", { hint: handoffSentence(options.command, (command) => `A person runs ${command} in a terminal, which asks for the key without showing it, or sets ${KEY_ENV} for that one command.`) });
3049
3049
  key = (await askHidden(streams, "Resend API key (https://resend.com/api-keys; it is not shown as you type): ")).trim();
3050
3050
  }
3051
3051
  if (!KEY_SHAPE.test(key)) throw new CommsError("BAD_DATA", "that does not look like a Resend API key (they start with re_)", { hint: "Nothing was stored. Copy the key again from the Resend dashboard." });
@@ -3069,8 +3069,9 @@ function renderAccount(view, color) {
3069
3069
  ` ${view.guarantee}`
3070
3070
  ].join("\n");
3071
3071
  }
3072
- function renderAccounts(result, color) {
3073
- if (result.accounts.length === 0) return "No Resend account yet. A person adds one with `agent-resend account add <org/resend>`.";
3072
+ /** The accounts; with none, how a person adds one — this installation's own command, `handoffs`'s, or why there is none. */
3073
+ function renderAccounts(result, color, handoffs) {
3074
+ if (result.accounts.length === 0) return handoffSentenceToFill(handoffs.own(["account", "add"]), ["<org/resend>"], (command) => `No Resend account yet. A person adds one with ${command}.`, { instead: "No Resend account yet." });
3074
3075
  return result.accounts.map((view) => renderAccount(view, color)).join("\n\n");
3075
3076
  }
3076
3077
  function renderAdded(view, color) {
@@ -3088,7 +3089,8 @@ function renderPolicy(report) {
3088
3089
  return [
3089
3090
  `${report.name}: ${report.mode} mode, send policy ${report.sendPolicy}${report.sendPolicyFrom === "default" ? " (the machine default)" : ""}`,
3090
3091
  `Changes to it are approved under the machine's change policy: ${report.changePolicy}.`,
3091
- `A send to more than ${report.confirmAboveRecipients} people needs a person at a terminal, whatever the policy.`
3092
+ `A send to more than ${report.confirmAboveRecipients} people needs a person at a terminal, whatever the policy.`,
3093
+ ...report.fenced === void 0 ? [] : describeFenceReport(report.fenced)
3092
3094
  ].join("\n");
3093
3095
  }
3094
3096
  function renderDoctor(result, color) {
@@ -3114,15 +3116,22 @@ function renderRead(result, color) {
3114
3116
  function renderPrepared(result) {
3115
3117
  return `${result.preview}\n\nNext: ${escapeForDisplay(result.nextStep)}`;
3116
3118
  }
3119
+ /**
3120
+ * What Resend's acceptance means (`said`), with its id when it gave one: a scheduled send is accepted for its time and
3121
+ * never called sent; one with no id says so, and names none.
3122
+ */
3117
3123
  function renderSent(result) {
3118
- const sent = result.state === "scheduled" ? `Scheduled for ${result.scheduledAt ?? "?"} as ${result.resendId}, to ${truncateDisplay(result.to.join(", "), 200)}.` : `Sent as ${result.resendId}, to ${truncateDisplay(result.to.join(", "), 200)}.`;
3124
+ const to = truncateDisplay(result.to.join(", "), 200);
3125
+ const said = `${result.said.charAt(0).toUpperCase()}${result.said.slice(1)}`;
3126
+ const sent = result.resendId === void 0 ? result.state === "scheduled" ? `${said} — scheduled for ${result.scheduledAt ?? "?"}, to ${to}.` : `${said} — to ${to}.` : result.state === "scheduled" ? `${said}, as ${result.resendId}, to ${to}.` : `Sent as ${result.resendId}, to ${to}.`;
3119
3127
  return result.note ? `${sent}\nNote: ${escapeForDisplay(result.note)}.` : sent;
3120
3128
  }
3121
3129
  function renderStatus(result) {
3122
3130
  return `${result.approvalId}: ${escapeForDisplay(result.verdict)}`;
3123
3131
  }
3124
3132
  function renderCancelled(result) {
3125
- return `Cancelled ${result.id} (${result.recipients} recipient(s)). It cannot be rescheduled.`;
3133
+ const cancelled = `Cancelled ${result.id} (${result.recipients} recipient(s)). It cannot be rescheduled.`;
3134
+ return result.hint ? `${cancelled}\nNote: ${escapeForDisplay(result.hint)}` : cancelled;
3126
3135
  }
3127
3136
  //#endregion
3128
3137
  //#region src/cli/program.ts
@@ -3131,6 +3140,17 @@ function withoutOptionValues(text) {
3131
3140
  return text.replace(/'(-{1,2}[^'=\s]+)=[^']*'/g, "'$1=…'").replace(/\bre_[A-Za-z0-9_]{4,}/g, "re_…");
3132
3141
  }
3133
3142
  const collect = (value, previous = []) => [...previous, value];
3143
+ /** What a usage error tells the person: this installation's own `--help`, located — or why there is none here. */
3144
+ function usageHint(help) {
3145
+ return handoffSentence(help, (command) => `Run ${command} to see the commands.`);
3146
+ }
3147
+ /**
3148
+ * What a command line that ran nothing says. Commander shows the help for an empty one itself, so this is the fallback
3149
+ * for a parse that ends with no command run and no error — never a bare name either way.
3150
+ */
3151
+ function nothingToDo(help) {
3152
+ return handoffSentence(help, (command) => `Nothing to do. Try ${command}.`);
3153
+ }
3134
3154
  /** `--expect-to a@x.test,b@x.test`, or `none`: a list is always written out, so a permission prompt shows it. */
3135
3155
  function expectList(raw, flag) {
3136
3156
  const value = String(raw ?? "").trim();
@@ -3155,17 +3175,44 @@ async function run(argv, deps = {}) {
3155
3175
  writeErr: (text) => streams.stderr.write(text),
3156
3176
  outputError: (text, write) => write(withoutOptionValues(text))
3157
3177
  }).addHelpText("after", `
3158
- Getting started:
3159
- agent-resend account add acme/resend a person types the key; it goes into the keychain
3160
- agent-resend doctor what works, and who enforces what
3161
- agent-resend domains --account acme/resend
3162
- agent-resend received list --account acme/resend
3163
- agent-resend mcp install --client claude-code register the server with an agent, once a person approves it
3178
+ Getting started — run these with this CLI:
3179
+ account add acme/resend a person types the key; it goes into the keychain
3180
+ doctor what works, and who enforces what
3181
+ domains --account acme/resend
3182
+ received list --account acme/resend
3183
+ mcp install --client claude-code register the server with an agent, once a person approves it
3164
3184
 
3165
3185
  Exit codes: 0 ok · 1 unexpected · 10 a send or a change was refused or needs approval · 11 an update
3166
- is out: update first, or put it off (agentcomms update, agentcomms update --later) · 64 usage ·
3186
+ is out: update first, or put it off (the stop names both commands) · 64 usage ·
3167
3187
  65 bad data · 66 not found · 69 provider or secret store unavailable · 75 temporary (retry later) ·
3168
3188
  77 key or permission needed · 78 configuration problem.`).exitOverride();
3189
+ for (const { flag } of PATH_OPTIONS) program.addOption(new Option(`${flag} <dir>`, "pin this suite directory for this run").argParser((value) => {
3190
+ if (value.length === 0) throw new InvalidArgumentError(`${flag} needs a non-empty directory`);
3191
+ return value;
3192
+ }));
3193
+ let invocationCore;
3194
+ const coreForInvocation = () => {
3195
+ if (invocationCore) return invocationCore;
3196
+ const options = program.opts();
3197
+ const pathOverrides = pathOverridesFromCliOptions(Object.fromEntries(PATH_OPTIONS.map(({ key, option }) => [option, options[key]])));
3198
+ invocationCore = Object.keys(pathOverrides).length === 0 && deps.core ? deps.core : openCore({
3199
+ env,
3200
+ platform,
3201
+ pathOverrides,
3202
+ caller: RESEND_CALLER
3203
+ });
3204
+ return invocationCore;
3205
+ };
3206
+ /** This installation's own `--help`, for a usage error or an empty command line: it opens no folder, so pins none. */
3207
+ const help = () => cliHandoffs({
3208
+ caller: RESEND_CALLER,
3209
+ paths: resolvePaths({
3210
+ env,
3211
+ platform
3212
+ }),
3213
+ platform,
3214
+ env
3215
+ }).own(["--help"], { uses: [] });
3169
3216
  const globals = () => {
3170
3217
  const options = program.opts();
3171
3218
  return {
@@ -3182,7 +3229,7 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3182
3229
  program.hook("preAction", async (_program, command) => {
3183
3230
  const path = commandPathOf(command);
3184
3231
  if (exemptFromUpdateGate(path, ["update-check-child"])) return;
3185
- const core = openCore({ env });
3232
+ const core = coreForInvocation();
3186
3233
  let ended = null;
3187
3234
  const code = await runCommand(output(), async () => {
3188
3235
  ended = await updateGateAtTerminal({
@@ -3194,13 +3241,12 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3194
3241
  output: output(),
3195
3242
  noInput: false,
3196
3243
  streams,
3197
- approveCommand: "agent-resend approve",
3198
3244
  approvals: approvalsOf(command),
3199
- approvalClaim: path.join(" ") === "send status" ? SEND_LOOKUP : void 0,
3245
+ approvalClaim: path.join(" ") === "send status" ? SEND_LOOKUP : path.join(" ") === "send wait" ? { lookup: true } : void 0,
3200
3246
  ...terminalUpdateHooks(core, env, {
3247
+ channel: "resend",
3201
3248
  output: output(),
3202
- streams,
3203
- approveCommand: "agent-resend approve"
3249
+ streams
3204
3250
  })
3205
3251
  });
3206
3252
  }, streams);
@@ -3213,24 +3259,30 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3213
3259
  exitCode = gated;
3214
3260
  return;
3215
3261
  }
3262
+ const core = coreForInvocation();
3216
3263
  const context = new ResendContext({
3217
3264
  ...deps,
3265
+ core,
3218
3266
  env,
3267
+ pathOverrides: core.pathOverrides,
3219
3268
  surface: "cli"
3220
3269
  });
3221
3270
  exitCode = await runCommand(output(), () => body(context, globals(), ...args), streams);
3222
3271
  if (exitCode === 0 && softExit !== null) exitCode = softExit;
3223
3272
  };
3224
- /** A change the way every changing command runs one: core's `gatedChangeAtTerminal`. */
3225
- const changeAt = (context, change, flags, command) => gatedChangeAtTerminal(context.core, change, {
3273
+ /**
3274
+ * A change the way every changing command runs one: core's `gatedChangeAtTerminal`. `rerun` is this command's words
3275
+ * after the program, which core locates as Resend's own and runs again with `--approval <id>`.
3276
+ */
3277
+ const changeAt = (context, change, flags, rerun) => gatedChangeAtTerminal(context.core, change, {
3278
+ channel: "resend",
3226
3279
  approvalId: flags.approval === void 0 ? void 0 : String(flags.approval),
3227
3280
  env,
3228
3281
  output: output(),
3229
- command,
3230
- approveCommand: "agent-resend approve",
3282
+ rerun,
3231
3283
  streams
3232
3284
  });
3233
- const approvalOption = (command) => command.option("--approval <approvalId>", "apply a change a person approved: said yes to in chat, or approved with `agent-resend approve`");
3285
+ const approvalOption = (command) => command.option("--approval <approvalId>", "apply a change a person approved: said yes to in chat, or approved with the approve command it gave");
3234
3286
  const accountOption = (command) => command.requiredOption("--account <name>", "which account, as `organisation/resend`");
3235
3287
  const account = program.command("account").description("connect, inspect, re-police and remove Resend accounts");
3236
3288
  approvalOption(account.command("add <name>").description("connect a Resend API key, typed by a person at this terminal (or RESEND_API_KEY); never from a chat").addOption(new Option("--mode <mode>", "read (the default for a full-access key) or send").choices(["read", "send"])).addOption(new Option("--send <policy>", "how its sends are approved; the machine default if left out").choices([
@@ -3239,19 +3291,18 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3239
3291
  "never"
3240
3292
  ])).option("--domain <domain>", "for a sending-only key restricted to one domain: that domain")).action(act(async (context, options, name, flags) => {
3241
3293
  const mode = modeOf(flags.mode);
3242
- const command = shellCommand([
3243
- "agent-resend",
3294
+ const rerun = [
3244
3295
  "account",
3245
3296
  "add",
3246
3297
  name,
3247
3298
  ...mode ? ["--mode", mode] : [],
3248
3299
  ...flags.send ? ["--send", String(flags.send)] : [],
3249
3300
  ...flags.domain ? ["--domain", String(flags.domain)] : []
3250
- ], platform);
3251
- checkNewName(await context.config(), name, context.platform);
3301
+ ];
3302
+ checkNewName(await context.config(), name, context.handoffs);
3252
3303
  const key = await readApiKey(env, streams, {
3253
3304
  json: options.json,
3254
- command
3305
+ command: context.handoffs.own(rerun)
3255
3306
  });
3256
3307
  const inspection = await inspectKey(context, key);
3257
3308
  const added = await changeAt(context, addAccountChange(context, {
@@ -3260,24 +3311,23 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3260
3311
  mode,
3261
3312
  sendPolicy: sendPolicyOf(flags.send),
3262
3313
  domain: flags.domain === void 0 ? void 0 : String(flags.domain)
3263
- }, inspection), flags, command);
3314
+ }, inspection), flags, rerun);
3264
3315
  writeResult(added, output(), (data) => renderAdded(data, options.color), streams);
3265
3316
  }));
3266
3317
  account.command("list").description("the Resend accounts this machine can use, and what each may do").action(act(async (context, options) => {
3267
3318
  const result = await listAccounts(context);
3268
- writeResult(result, output(), (data) => renderAccounts(data, options.color), streams);
3319
+ writeResult(result, output(), (data) => renderAccounts(data, options.color, context.handoffs), streams);
3269
3320
  }));
3270
3321
  account.command("show <name>").description("one account: its key’s permission, its mode, its send policy, and who enforces what").action(act(async (context, options, name) => {
3271
3322
  const result = await showAccount(context, name);
3272
3323
  writeResult(result, output(), (data) => renderAccount(data, options.color), streams);
3273
3324
  }));
3274
3325
  approvalOption(account.command("remove <name>").description("forget an account and delete its key, once a person approves it")).action(act(async (context, _options, name, flags) => {
3275
- const removed = await changeAt(context, removeAccountChange(context, name), flags, shellCommand([
3276
- "agent-resend",
3326
+ const removed = await changeAt(context, removeAccountChange(context, name), flags, [
3277
3327
  "account",
3278
3328
  "remove",
3279
3329
  name
3280
- ], platform));
3330
+ ]);
3281
3331
  writeResult(removed, output(), renderRemoved, streams);
3282
3332
  }));
3283
3333
  approvalOption(account.command("policy <name>").description("report how its sends and changes are approved, or set --send, --mode and --change; loosening is approved by a person").option("--send <policy>", "chat, confirm or never").option("--mode <mode>", "read or send").option("--change <policy>", "how a loosening of this account is approved: chat or confirm")).action(act(async (context, _options, name, flags) => {
@@ -3288,15 +3338,14 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3288
3338
  });
3289
3339
  const reporting = wanted.send === void 0 && wanted.mode === void 0 && wanted.change === void 0;
3290
3340
  if (reporting) refuseUnclaimedApproval(flags.approval, policyApprovalRefusal("cli"));
3291
- const result = reporting ? await policyReport(context, name) : await changeAt(context, policyChange(context, name, wanted), flags, shellCommand([
3292
- "agent-resend",
3341
+ const result = reporting ? await policyReport(context, name) : await changeAt(context, policyChange(context, name, wanted), flags, [
3293
3342
  "account",
3294
3343
  "policy",
3295
3344
  name,
3296
3345
  ...wanted.send ? ["--send", wanted.send] : [],
3297
3346
  ...wanted.mode ? ["--mode", wanted.mode] : [],
3298
3347
  ...wanted.change ? ["--change", wanted.change] : []
3299
- ], platform));
3348
+ ]);
3300
3349
  writeResult(result, output(), renderPolicy, streams);
3301
3350
  }));
3302
3351
  program.command("doctor").description("check what has to work, and say who enforces what — read-only is this package’s promise, not the key’s").option("--account <name>", "check only this account").option("--offline", "do not ask Resend anything", false).action(act(async (context, options, flags) => {
@@ -3391,39 +3440,42 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3391
3440
  const status = await sendStatus(context, String(flags.account), approvalId);
3392
3441
  writeResult(status, output(), renderStatus, streams);
3393
3442
  }));
3443
+ send.command("wait <approvalId>").description("wait for an approval to be usable or finished, and say where it stands — it only looks, and never sends").option("--wait-seconds <n>", `how long to wait: 30 when left out, 300 at most, 0 for the status now`).action(act(async (context, _options, approvalId, flags) => {
3444
+ const result = await waitForApproval(context.core, approvalId, {
3445
+ waitSeconds: wholeNumber(flags.waitSeconds, {
3446
+ name: "--wait-seconds",
3447
+ min: 0,
3448
+ max: 300
3449
+ }),
3450
+ channel: "resend"
3451
+ });
3452
+ writeResult(result, output(), renderApprovalWait, streams);
3453
+ }));
3394
3454
  const scheduled = program.command("scheduled").description("emails waiting to be sent at a later time");
3395
3455
  accountOption(scheduled.command("list")).description("scheduled emails among the most recent 300 sent").action(act(async (context, options, flags) => {
3396
3456
  const result = await listScheduled(context, String(flags.account));
3397
3457
  writeResult(result, output(), (data) => renderRead(data, options.color), streams);
3398
3458
  }));
3399
3459
  approvalOption(accountOption(scheduled.command("cancel <id>")).description("cancel a scheduled email; one this machine did not schedule is approved by a person first")).action(act(async (context, _options, id, flags) => {
3400
- const cancelled = await changeAt(context, cancelScheduledChange(context, String(flags.account), id), flags, shellCommand([
3401
- "agent-resend",
3460
+ const cancelled = await changeAt(context, cancelScheduledChange(context, String(flags.account), id), flags, [
3402
3461
  "scheduled",
3403
3462
  "cancel",
3404
3463
  id,
3405
3464
  "--account",
3406
3465
  String(flags.account)
3407
- ], platform));
3466
+ ]);
3408
3467
  writeResult(cancelled, output(), renderCancelled, streams);
3409
3468
  }));
3410
3469
  program.command("approve <approvalId>").description("approve a send or a change at this terminal: read it, then type the code back").action(act(async (context, options, approvalId) => {
3411
3470
  const marker = agentMarker(env);
3471
+ const approve = context.handoffs.own(["approve", approvalId]);
3412
3472
  if (marker) throw new CommsError("APPROVAL_REQUIRED", "only a person can approve a send or a change, not an agent", {
3413
- hint: `Ask the user to run ${inlineCommand(shellCommand([
3414
- "agent-resend",
3415
- "approve",
3416
- approvalId
3417
- ], context.platform))} in their own terminal.`,
3473
+ hint: approveRefusedHint(context.handoffs, approvalId),
3418
3474
  details: { marker }
3419
3475
  });
3420
- if (!canPrompt(env, streams, { json: options.json })) throw new CommsError("APPROVAL_REQUIRED", "approving needs an interactive terminal", { hint: `Run ${inlineCommand(shellCommand([
3421
- "agent-resend",
3422
- "approve",
3423
- approvalId
3424
- ], context.platform))} directly in a terminal.` });
3476
+ if (!canPrompt(env, streams, { json: options.json })) throw new CommsError("APPROVAL_REQUIRED", "approving needs an interactive terminal", { hint: handoffSentence(approve, (command) => `Run ${command} directly in a terminal.`) });
3425
3477
  const pending = await context.core.approvals.get(approvalId);
3426
- if (pending && approvalKind(pending) === "change") {
3478
+ if (kindOf(pending) === "change") {
3427
3479
  const outcome = await approveChangeAtTerminal(context.core, approvalId, env, {
3428
3480
  json: options.json,
3429
3481
  color: options.color,
@@ -3445,10 +3497,14 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3445
3497
  }));
3446
3498
  const mcp = program.command("mcp").description("run the MCP server on stdio, for a coding agent to connect to").option("--account <name>", "pin the server to one account; every tool then acts on it and no other").action(async (flags) => {
3447
3499
  ran = true;
3448
- const { startResendStdioServer } = await import("./stdio-entry-BCnq_EyF.mjs");
3500
+ const startResendStdioServer = deps.startMcp ?? (await import("./stdio-entry-PsvT2AI2.mjs")).startResendStdioServer;
3501
+ const core = coreForInvocation();
3449
3502
  await startResendStdioServer({
3450
3503
  ...deps,
3504
+ core,
3451
3505
  env,
3506
+ pathOverrides: core.pathOverrides,
3507
+ platform,
3452
3508
  ...flags.account ? { account: String(flags.account) } : {}
3453
3509
  });
3454
3510
  });
@@ -3465,12 +3521,16 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3465
3521
  "npx",
3466
3522
  "local"
3467
3523
  ])).option("--no-verify", "do not start the server to check the entry works").option("--force", "replace this server's own earlier entry — this is how you upgrade", false).option("--print", "only print what would be written", false).option("--approval <approvalId>", "register the server this approval was given for").action(act(async (context, options, flags) => {
3468
- if (!flags.client) throw new CommsError("USAGE", "name the client with --client", { hint: "For example: `agent-resend mcp install --client claude-code`." });
3524
+ if (!flags.client) throw new CommsError("USAGE", "name the client with --client", { hint: handoffSentence(context.handoffs.own([
3525
+ "mcp",
3526
+ "install",
3527
+ "--client",
3528
+ "claude-code"
3529
+ ]), (command) => `For example: ${command}.`) });
3469
3530
  const pinned = flags.account ?? mcp.opts().account;
3470
3531
  const launcher = flags.launcher;
3471
3532
  const name = flags.name;
3472
- const again = shellCommand([
3473
- "agent-resend",
3533
+ const again = [
3474
3534
  "mcp",
3475
3535
  "install",
3476
3536
  "--client",
@@ -3480,7 +3540,7 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3480
3540
  ...launcher !== void 0 ? ["--launcher", launcher] : [],
3481
3541
  ...flags.verify === false ? ["--no-verify"] : [],
3482
3542
  ...flags.force === true ? ["--force"] : []
3483
- ], platform);
3543
+ ];
3484
3544
  const result = await changeAt(context, serverInstallChange(context.core, env, {
3485
3545
  channel: "resend",
3486
3546
  client: flags.client,
@@ -3500,12 +3560,11 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3500
3560
  channel: "resend",
3501
3561
  dryRun: flags.dryRun === true,
3502
3562
  includePrinted: flags.includePrinted === true
3503
- }, RESEND_MCP), flags, shellCommand([
3504
- "agent-resend",
3563
+ }, RESEND_MCP), flags, [
3505
3564
  "mcp",
3506
3565
  "prune",
3507
3566
  ...flags.includePrinted === true ? ["--include-printed"] : []
3508
- ], platform));
3567
+ ]);
3509
3568
  writeResult(result, output(), () => renderPrune(result, options.color), streams);
3510
3569
  }));
3511
3570
  program.command(`${UPDATE_CHECK_CHILD_COMMAND} <claimedAt>`, { hidden: true }).description("internal: finish the day's update check a command handed on").action(act(async (context, _options, claimedAt) => {
@@ -3522,13 +3581,13 @@ is out: update first, or put it off (agentcomms update, agentcomms update --late
3522
3581
  ].includes(error.code)) return 0;
3523
3582
  const message = withoutOptionValues(error.message.replace(/^error: /, ""));
3524
3583
  return runCommand(output(), async () => {
3525
- throw new CommsError("USAGE", message, { hint: "Run `agent-resend --help` to see the commands." });
3584
+ throw new CommsError("USAGE", message, { hint: usageHint(help()) });
3526
3585
  }, streams);
3527
3586
  }
3528
3587
  throw error;
3529
3588
  }
3530
3589
  if (!ran) {
3531
- streams.stderr.write(`${paint(globals().color, "dim", "Nothing to do. Try `agent-resend --help`.")}\n`);
3590
+ streams.stderr.write(`${paint(globals().color, "dim", nothingToDo(help()))}\n`);
3532
3591
  return 64;
3533
3592
  }
3534
3593
  return exitCode;