@agentcomms/resend 0.8.0 → 0.10.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/dist/cli.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { B as canPrompt, C as policyReport, D as showAccount, E as sendPolicyOf, F as CommsError, G as gatedChangeAtTerminal, H as colorEnabled, I as EXIT_CODES, J as renderInstall, K as installExitStatus, L as agentMarker, M as checkNewName, O as ResendContext, P as CHANNEL_SERVERS, Q as serverPruneChange, R as approvalKind, S as policyChange, T as removeAccountChange, U as escapeForDisplay, X as runCommand, Y as renderPrune, Z as serverInstallChange, _ as runDoctor, a as revokeSendApproval, b as listAccounts, c as downloadReceived, d as listReceived, et as truncateDisplay, f as listScheduled, g as showSentEmail, h as showReceived, i as prepareSend, l as getMetrics, m as listSuppressions, n as executeSend, o as sendStatus, ot as VERSION, p as listSentEmails, q as paint, r as finishSendApproval, s as cancelScheduledChange, t as beginSendApproval, tt as writeResult, u as listDomains, v as addAccountChange, w as policyWanted, x as modeOf, y as inspectKey, z as approveChangeAtTerminal } from "./send-BpwXJL8C.mjs";
2
+ import { $ as openCore, B as approvalKind, C as policyChange, D as sendPolicyOf, E as removeAccountChange, F as CHANNEL_SERVERS, H as approveChangeAtTerminal, I as CommsError, J as escapeForDisplay, K as colorEnabled, L as EXIT_CODES, N as checkNewName, O as showAccount, Q as installExitStatus, R as SEND_LOOKUP, S as policyApprovalRefusal, T as policyWanted, U as canPrompt, V as approvalsOf, Y as exemptFromUpdateGate, Z as gatedChangeAtTerminal, _ as runDoctor, _t as VERSION, a as revokeSendApproval, at as serverInstallChange, b as listAccounts, c as downloadReceived, d as listReceived, et as paint, f as listScheduled, ft as writeResult, g as showSentEmail, h as showReceived, i as prepareSend, it as runCommand, k as ResendContext, l as getMetrics, lt as truncateDisplay, m as listSuppressions, n as executeSend, nt as renderInstall, o as sendStatus, ot as serverPruneChange, p as listSentEmails, q as commandPathOf, r as finishSendApproval, rt as renderPrune, s as cancelScheduledChange, st as terminalUpdateHooks, t as beginSendApproval, tt as refuseUnclaimedApproval, u as listDomains, ut as updateGateAtTerminal, v as addAccountChange, w as policyReport, x as modeOf, y as inspectKey, z as agentMarker } from "./send-Ba1caJ6x.mjs";
3
3
  import path from "node:path";
4
+ import childProcess from "node:child_process";
4
5
  import { createInterface } from "node:readline/promises";
5
6
  import { stripVTControlCharacters } from "node:util";
6
- import childProcess from "node:child_process";
7
7
  import fs from "node:fs";
8
8
  import process$1 from "node:process";
9
9
  import { EventEmitter } from "node:events";
@@ -3160,7 +3160,8 @@ Getting started:
3160
3160
  agent-resend received list --account acme/resend
3161
3161
  agent-resend mcp install --client claude-code register the server with an agent, once a person approves it
3162
3162
 
3163
- Exit codes: 0 ok · 1 unexpected · 10 a send or a change was refused or needs approval · 64 usage ·
3163
+ Exit codes: 0 ok · 1 unexpected · 10 a send or a change was refused or needs approval · 11 an update
3164
+ is out: update first, or put it off (agentcomms update, agentcomms update --later) · 64 usage ·
3164
3165
  65 bad data · 66 not found · 69 provider or secret store unavailable · 75 temporary (retry later) ·
3165
3166
  77 key or permission needed · 78 configuration problem.`).exitOverride();
3166
3167
  const globals = () => {
@@ -3174,9 +3175,41 @@ Exit codes: 0 ok · 1 unexpected · 10 a send or a change was refused or needs a
3174
3175
  json: globals().json,
3175
3176
  color: globals().color
3176
3177
  });
3178
+ let gated = null;
3179
+ program.hook("preAction", async (_program, command) => {
3180
+ const path = commandPathOf(command);
3181
+ if (exemptFromUpdateGate(path)) return;
3182
+ const core = openCore({ env });
3183
+ let ended = null;
3184
+ const code = await runCommand(output(), async () => {
3185
+ ended = await updateGateAtTerminal({
3186
+ core,
3187
+ env,
3188
+ binary: "agent-resend",
3189
+ channel: "resend",
3190
+ running: VERSION,
3191
+ output: output(),
3192
+ noInput: false,
3193
+ streams,
3194
+ approveCommand: "agent-resend approve",
3195
+ approvals: approvalsOf(command),
3196
+ approvalClaim: path.join(" ") === "send status" ? SEND_LOOKUP : void 0,
3197
+ ...terminalUpdateHooks(core, env, {
3198
+ output: output(),
3199
+ streams,
3200
+ approveCommand: "agent-resend approve"
3201
+ })
3202
+ });
3203
+ }, streams);
3204
+ gated = code !== 0 ? code : ended;
3205
+ });
3177
3206
  const act = (body) => async (...args) => {
3178
3207
  ran = true;
3179
3208
  softExit = null;
3209
+ if (gated !== null) {
3210
+ exitCode = gated;
3211
+ return;
3212
+ }
3180
3213
  const context = new ResendContext({
3181
3214
  ...deps,
3182
3215
  env,
@@ -3237,7 +3270,9 @@ Exit codes: 0 ok · 1 unexpected · 10 a send or a change was refused or needs a
3237
3270
  mode: flags.mode,
3238
3271
  change: flags.change
3239
3272
  });
3240
- const result = wanted.send === void 0 && wanted.mode === void 0 && wanted.change === void 0 ? await policyReport(context, name) : await changeAt(context, policyChange(context, name, wanted), flags, `agent-resend account policy ${name}${wanted.send ? ` --send ${wanted.send}` : ""}${wanted.mode ? ` --mode ${wanted.mode}` : ""}${wanted.change ? ` --change ${wanted.change}` : ""}`);
3273
+ const reporting = wanted.send === void 0 && wanted.mode === void 0 && wanted.change === void 0;
3274
+ if (reporting) refuseUnclaimedApproval(flags.approval, policyApprovalRefusal("cli"));
3275
+ const result = reporting ? await policyReport(context, name) : await changeAt(context, policyChange(context, name, wanted), flags, `agent-resend account policy ${name}${wanted.send ? ` --send ${wanted.send}` : ""}${wanted.mode ? ` --mode ${wanted.mode}` : ""}${wanted.change ? ` --change ${wanted.change}` : ""}`);
3241
3276
  writeResult(result, output(), renderPolicy, streams);
3242
3277
  }));
3243
3278
  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) => {
@@ -3370,7 +3405,7 @@ Exit codes: 0 ok · 1 unexpected · 10 a send or a change was refused or needs a
3370
3405
  }));
3371
3406
  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) => {
3372
3407
  ran = true;
3373
- const { startResendStdioServer } = await import("./stdio-entry-CVMfmkCS.mjs");
3408
+ const { startResendStdioServer } = await import("./stdio-entry-CKC1hDbG.mjs");
3374
3409
  await startResendStdioServer({
3375
3410
  ...deps,
3376
3411
  env,
package/dist/index.d.mts CHANGED
@@ -11519,6 +11519,8 @@ type SendPolicy = "chat" | "confirm" | "never";
11519
11519
  * way to change it, and that passes no gate at all.
11520
11520
  */
11521
11521
  type ChangePolicy = "chat" | "confirm";
11522
+ /** Whether this machine checks for a newer release once a day, and stops until it is updated or put off. */
11523
+ type UpdateCheckSetting = "on" | "off";
11522
11524
  type StoreKind = "keychain" | "file";
11523
11525
  interface ClientConfig {
11524
11526
  provider: string;
@@ -11573,6 +11575,15 @@ interface Defaults {
11573
11575
  */
11574
11576
  elicitationClients: string[];
11575
11577
  };
11578
+ /**
11579
+ * The daily update check (design 2026-09-28): once a day this machine asks npm whether a newer release is out, and
11580
+ * every server and command stops until it is updated or put off until tomorrow. Absent reads as `on`.
11581
+ *
11582
+ * Absent rather than filled in, as `changePolicy` is: a default the schema supplied would be written into the file
11583
+ * by the next unrelated change, where it would look like a choice somebody made. Turning it off is a loosening —
11584
+ * it is what keeps a machine from quietly falling behind a release — so it is approved like one; on applies at once.
11585
+ */
11586
+ updateCheck?: UpdateCheckSetting | undefined;
11576
11587
  }
11577
11588
  /**
11578
11589
  * A connected account on a platform that is not mail: a Slack workspace, and every channel after it (design
@@ -11786,6 +11797,11 @@ interface LooseningConsent {
11786
11797
  */
11787
11798
  changes?: readonly Loosening[] | undefined;
11788
11799
  }
11800
+ /**
11801
+ * Why a saved name has {@link DOWNLOAD_SUFFIX} after it: a name tools read or run on their own (`auto-read`), an
11802
+ * extension that is not one of the inert ones (`type`), or no extension at all (`no-extension`).
11803
+ */
11804
+ type RenameReason = "auto-read" | "type" | "no-extension";
11789
11805
  //#endregion
11790
11806
  //#region src/approvals.d.ts
11791
11807
  /**
@@ -11801,17 +11817,26 @@ interface LooseningConsent {
11801
11817
  *
11802
11818
  * A change approval (`kind: 'change'`) lives in the same store and goes through the same states, except that its
11803
11819
  * claim goes straight to `used`: what it permits is a write to the configuration, which the claimant makes itself.
11820
+ *
11821
+ * So does a download's question (`kind: 'download'`): where to save the files a Gmail or Slack download names. It is
11822
+ * held to the change policy of the mailbox or workspace it is about, as every other change there is. Under `chat` the
11823
+ * person's answer, relayed from the chat, claims it straight from `pending`. Under `confirm` it has to be answered
11824
+ * where an agent cannot answer for them — at their own terminal, or in a form a trusted client shows them — which
11825
+ * records the answer and moves it to `approved`; only then is it claimed, straight to `used`, and the recorded answer
11826
+ * is the one that saves. It gets the store's expiry, its single use and its binding for the reason a change does: an
11827
+ * answer given for three invoices must not save the next conversation's files, nor be spent twice.
11804
11828
  */
11805
11829
  type ApprovalState = "pending" | "approved" | "sending" | "used" | "failed" | "unknown" | "expired" | "revoked";
11806
11830
  type ApprovalChannel = "elicitation" | "terminal";
11807
11831
  /**
11808
- * What an approval permits: a send (a mail, a post, a reaction) or a change to the configuration.
11832
+ * What an approval permits: a send (a mail, a post, a reaction), a change to the configuration, or a download saved
11833
+ * where the person said.
11809
11834
  *
11810
- * One store for both, so a change gets the machinery a send already has — expiry, single use, the typed code — and
11811
- * a kind on every record, so that neither can be spent as the other. Without it, a person who approved a post at a
11835
+ * One store for all three, so a change gets the machinery a send already has — expiry, single use, the typed code —
11836
+ * and a kind on every record, so that none can be spent as another. Without it, a person who approved a post at a
11812
11837
  * terminal would also have approved whatever change an agent claimed under the same id.
11813
11838
  */
11814
- type ApprovalKind = "send" | "change";
11839
+ type ApprovalKind = "send" | "change" | "download";
11815
11840
  /** The inbox or account a change is about. `id` is absent when the change connects it, and it does not exist yet. */
11816
11841
  interface ChangeTarget {
11817
11842
  kind: "inbox" | "account";
@@ -11839,6 +11864,70 @@ interface ChangeBinding {
11839
11864
  /** What it does outside the configuration, in words: a sign-in, a registration, files removed. */
11840
11865
  effects: string[];
11841
11866
  }
11867
+ /**
11868
+ * Exactly which download a question was asked for, stored on the record so the answer can be held to it.
11869
+ *
11870
+ * The files are the ones the question listed, by the platform's own ids — `<message id>/<part id>` for Gmail, the file
11871
+ * id for Slack — in the order they were listed: a claim for any other set is a claim for a download the person was
11872
+ * not asked about. So are the names they would be saved under, in the same order: a Slack file renamed between the
11873
+ * question and the answer would otherwise be saved under a name the person was never shown. The folders are the two
11874
+ * the question showed, as absolute paths. They are not part of the digest, because they are what the answer *means*
11875
+ * rather than what it is for: `downloads` in the answer is the path the person read, even when an agent runs the
11876
+ * download again from another folder.
11877
+ */
11878
+ interface DownloadBinding {
11879
+ /** One line about the download, for a listing: "where to save 2 files from acme/gmail". Not part of the digest. */
11880
+ summary: string;
11881
+ /** The mailbox or workspace the files come from, by the id it has now. */
11882
+ target: {
11883
+ kind: "inbox" | "account";
11884
+ name: string;
11885
+ id: string;
11886
+ };
11887
+ /** Which download asked: `attachments.download`, `files.download`. */
11888
+ operation: string;
11889
+ /** The request as the caller made it, in the words of its own arguments: the selection, and how many at most. */
11890
+ request: Record<string, unknown>;
11891
+ /** The files the question listed, by the platform's ids, in order. */
11892
+ files: string[];
11893
+ /** The names those files would be saved under, as the question listed them, in the same order. */
11894
+ names: string[];
11895
+ /** The two folders the question offered, as absolute paths. */
11896
+ folders: {
11897
+ downloads: string;
11898
+ current: string;
11899
+ };
11900
+ /**
11901
+ * The files as the question listed them to the person — the names they would be saved under, their sizes, why a
11902
+ * name has `.download` after it, and their risk flags — so a terminal or a form can show the question, and its
11903
+ * warnings, again. Not part of the digest: the ids and names above are.
11904
+ */
11905
+ listing?: ListedFile[] | undefined;
11906
+ /** The person's answer, when they gave it where an agent cannot: at a terminal, or in a trusted client's form. */
11907
+ answer?: RecordedSaveAnswer | undefined;
11908
+ }
11909
+ /** One file as a question lists it. */
11910
+ interface ListedFile {
11911
+ /** The name it would be saved under: the sender's made safe, with `.download` after it unless it is inert. */
11912
+ name: string;
11913
+ size: number | null;
11914
+ /** Why `.download` is after its name; absent when the name is the sender's, made safe. */
11915
+ renamed?: RenameReason | undefined;
11916
+ /** Its risk flags, as the file's own listing gives them. */
11917
+ flags?: string[] | undefined;
11918
+ }
11919
+ /**
11920
+ * An answer recorded on a question: one of the two folders it offered, or the folder the person named, as an absolute
11921
+ * path — resolved where they typed it, so it means the folder they read.
11922
+ */
11923
+ type RecordedSaveAnswer = {
11924
+ readonly choice: "downloads" | "current";
11925
+ } | {
11926
+ readonly choice: "other";
11927
+ readonly folder: string;
11928
+ };
11929
+ /** What a claim of a download's question is held to: all of the binding but its summary and the folders it offered. */
11930
+ type DownloadRequest = Pick<DownloadBinding, "target" | "operation" | "request" | "files" | "names">;
11842
11931
  interface Expectation {
11843
11932
  to: string[];
11844
11933
  cc: string[];
@@ -11877,6 +11966,8 @@ interface ApprovalRecord {
11877
11966
  reason?: string | undefined;
11878
11967
  /** For a change: exactly what it permits. `digest` is `changeDigest` of this. */
11879
11968
  change?: ChangeBinding | undefined;
11969
+ /** For a download's question: what it asked about, and the folders it offered. `digest` is `downloadDigest`. */
11970
+ download?: DownloadBinding | undefined;
11880
11971
  }
11881
11972
  interface CreateApprovalInput {
11882
11973
  inboxId: string;
@@ -11922,6 +12013,7 @@ declare class ApprovalStore {
11922
12013
  constructor(stateDir: string, options?: {
11923
12014
  now?: () => Date;
11924
12015
  ttlMs?: number;
12016
+ downloadTtlMs?: number;
11925
12017
  });
11926
12018
  create(input: CreateApprovalInput): Promise<ApprovalRecord>;
11927
12019
  get(approvalId: string): Promise<ApprovalRecord | null>;
@@ -11968,6 +12060,52 @@ declare class ApprovalStore {
11968
12060
  * person to have typed the code at a terminal first; until then the refusal leaves the record as it is.
11969
12061
  */
11970
12062
  claimForChange(approvalId: string, live: LiveChange, options?: ClaimOptions): Promise<ApprovalRecord>;
12063
+ /**
12064
+ * A download's question, pending, bound to `downloadDigest(input.download)` — computed here, never taken from the
12065
+ * caller, as a change's digest is. It stands in for the draft revision too.
12066
+ *
12067
+ * `policy` is the change policy of the mailbox or workspace the files come from, as it stands when the question is
12068
+ * asked: where a stranger's files land on this machine is a change to it, and is answered the way that account's
12069
+ * other changes are approved. A claim holds the question to the stricter of this and the policy then.
12070
+ */
12071
+ createDownload(input: {
12072
+ download: DownloadBinding;
12073
+ policy: ChangePolicy;
12074
+ }): Promise<ApprovalRecord & {
12075
+ download: DownloadBinding;
12076
+ }>;
12077
+ /**
12078
+ * Records the person's answer to a download's question, given where an agent cannot give it: at their own terminal,
12079
+ * or in a form a trusted client showed them. The question moves to `approved`, carrying the answer, and waits for
12080
+ * the download to claim it — which it may then do under `confirm`, and which saves where this answer says.
12081
+ *
12082
+ * Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
12083
+ */
12084
+ answerDownload(approvalId: string, via: ApprovalChannel, answer: RecordedSaveAnswer): Promise<ApprovalRecord & {
12085
+ download: DownloadBinding;
12086
+ }>;
12087
+ /**
12088
+ * Claims a download's question, once, for the download the caller is about to make — and returns it, with the
12089
+ * folders it offered, so that `downloads` and `current` in the answer mean the paths the person read, and with the
12090
+ * answer the person recorded, when they recorded one.
12091
+ *
12092
+ * `live` is the download as the caller computes it now: the same account, the same request, the same files under
12093
+ * the same names — the person answered for those files and no others. Any other is refused, and the question left
12094
+ * open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
12095
+ * question for it would make the person answer again for nothing. It still expires, and is claimed once.
12096
+ *
12097
+ * `options.policy` is the change policy of that account now, and the stricter of it and the one the question was
12098
+ * asked under decides. Under `chat`, a pending question is claimed with the answer the caller carries: the person
12099
+ * gave it in the conversation. Under `confirm`, only a question the person answered at a terminal or in a trusted
12100
+ * form can be claimed; a pending one is refused, and left as it is, so they can still answer it. One used, voided or
12101
+ * expired is refused as an approval in that state is.
12102
+ */
12103
+ claimForDownload(approvalId: string, live: DownloadRequest, options?: {
12104
+ policy?: ChangePolicy | undefined;
12105
+ pendingHint?: string | undefined;
12106
+ }): Promise<ApprovalRecord & {
12107
+ download: DownloadBinding;
12108
+ }>;
11971
12109
  /** Records the outcome of the one send attempt. */
11972
12110
  complete(approvalId: string, outcome: {
11973
12111
  sentMessageId: string;
@@ -12067,7 +12205,10 @@ interface ResolvedPaths {
12067
12205
  secretsDir: string;
12068
12206
  /** Managed runtime installs for MCP clients. */
12069
12207
  dataDir: string;
12070
- /** Default root for attachment downloads and exports. */
12208
+ /**
12209
+ * Default root for exports and for the files Resend's received mail carries. A Gmail attachment or a Slack file is
12210
+ * not saved here: those downloads ask the person where to save (`save-destination.ts`).
12211
+ */
12071
12212
  downloadsDir: string;
12072
12213
  }
12073
12214
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { at as ROUTES, it as RESEND_DOWNLOAD_ORIGIN, nt as REFUSED, ot as VERSION, rt as RESEND_API_ORIGIN } from "./send-BpwXJL8C.mjs";
2
- import { t as createResendMcpServer } from "./server-C3PKeQSN.mjs";
1
+ import { _t as VERSION, gt as ROUTES, ht as RESEND_DOWNLOAD_ORIGIN, mt as RESEND_API_ORIGIN, pt as REFUSED } from "./send-Ba1caJ6x.mjs";
2
+ import { t as createResendMcpServer } from "./server-BHHLf1lV.mjs";
3
3
  //#region src/index.ts
4
4
  /**
5
5
  * The library entry of `@agentcomms/resend`.