@agentcomms/resend 0.18.1 → 0.19.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
@@ -29,6 +29,14 @@ approves, whatever the policy. The approval id is the request's `Idempotency-Key
29
29
  send whose outcome is unknown (`SEND_OUTCOME_UNKNOWN`) is never repeated, only checked, and a scheduled one is
30
30
  "accepted", never "sent", until Resend's own last event for it says so.
31
31
 
32
+ **An agent's email says so in its From.** Prepare labels it `"Acme (agent)" <hello@acme.test>`: the name the From
33
+ gave, or, for a bare address, the label name the person set in the core (its CLI's `label name`, or `comms_label`), then their word —
34
+ `agent` unless they chose another ([design](../../docs/superpowers/specs/2026-10-15-agent-label-design.md), §6). With
35
+ neither name, or a label over Resend's 100 characters, prepare refuses. `--as me` (`as: "me"` on
36
+ `resend_send_prepare`) sends the From exactly as given, for when the person said so for that email; their label rules
37
+ decide otherwise, and with none an email is labelled. The preview's first line says which, and the approval binds the
38
+ whole From.
39
+
32
40
  **The key is typed by a person, at a terminal.** `agent-resend account add` reads it from a hidden prompt, or from
33
41
  `RESEND_API_KEY` in that terminal, and stores it in the system keychain. No MCP tool accepts a key: a key typed into
34
42
  a chat stays in the transcript.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { A as removeAccountChange, At as updateGateAtTerminal, C as inspectKey, Ct as runUpdateCheckChild, D as policyChange, Dt as terminalUpdateHooks, E as policyApprovalRefusal, Ft as writeResult, G as SEND_LOOKUP, H as CommsError, Ht as VERSION, J as approvalsOf, K as UPDATE_CHECK_CHILD_COMMAND, Lt as RESEND_CALLER, M as showAccount, Nt as waitForApproval, O as policyReport, P as ResendContext, Pt as wholeNumber, R as checkNewName, S as addAccountChange, St as runCommand, T as modeOf, Tt as serverPruneChange, U as EXIT_CODES, V as CHANNEL_SERVERS, W as PATH_OPTIONS, X as approveRefusedHint, Y as approveChangeAtTerminal, Z as canPrompt, _ as showSentEmail, _t as refuseUnclaimedApproval, a as revokeSendApproval, at as exemptFromUpdateGate, bt as renderPrune, c as downloadReceived, ct as handoffSentence, d as listReceived, dt as kindOf, et as cliHandoffs, f as listScheduled, g as showReceived, gt as pathOverridesFromCliOptions, ht as paint, i as prepareSend, it as escapeForDisplay, j as sendPolicyOf, k as policyWanted, kt as truncateDisplay, l as getMetrics, lt as handoffSentenceToFill, m as listSuppressions, mt as openCore, n as executeSend, nt as commandPathOf, o as sendStatus, p as listSentEmails, q as agentMarker, r as finishSendApproval, rt as describeFenceReport, s as cancelScheduledChange, st as gatedChangeAtTerminal, t as beginSendApproval, tt as colorEnabled, u as listDomains, ut as installExitStatus, vt as renderApprovalWait, w as listAccounts, wt as serverInstallChange, x as runDoctor, xt as resolvePaths, yt as renderInstall } from "./send-BOMCKvLT.mjs";
2
+ import { A as removeAccountChange, At as updateGateAtTerminal, C as inspectKey, Ct as runUpdateCheckChild, D as policyChange, Dt as terminalUpdateHooks, E as policyApprovalRefusal, Ft as writeResult, G as SEND_LOOKUP, H as CommsError, Ht as VERSION, J as approvalsOf, K as UPDATE_CHECK_CHILD_COMMAND, Lt as RESEND_CALLER, M as showAccount, Nt as waitForApproval, O as policyReport, P as ResendContext, Pt as wholeNumber, R as checkNewName, S as addAccountChange, St as runCommand, T as modeOf, Tt as serverPruneChange, U as EXIT_CODES, V as CHANNEL_SERVERS, W as PATH_OPTIONS, X as approveRefusedHint, Y as approveChangeAtTerminal, Z as canPrompt, _ as showSentEmail, _t as refuseUnclaimedApproval, a as revokeSendApproval, at as exemptFromUpdateGate, bt as renderPrune, c as downloadReceived, ct as handoffSentence, d as listReceived, dt as kindOf, et as cliHandoffs, f as listScheduled, g as showReceived, gt as pathOverridesFromCliOptions, ht as paint, i as prepareSend, it as escapeForDisplay, j as sendPolicyOf, k as policyWanted, kt as truncateDisplay, l as getMetrics, lt as handoffSentenceToFill, m as listSuppressions, mt as openCore, n as executeSend, nt as commandPathOf, o as sendStatus, p as listSentEmails, q as agentMarker, r as finishSendApproval, rt as describeFenceReport, s as cancelScheduledChange, st as gatedChangeAtTerminal, t as beginSendApproval, tt as colorEnabled, u as listDomains, ut as installExitStatus, vt as renderApprovalWait, w as listAccounts, wt as serverInstallChange, x as runDoctor, xt as resolvePaths, yt as renderInstall } from "./send-Ch-s9Q6H.mjs";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stripVTControlCharacters } from "node:util";
@@ -3407,7 +3407,7 @@ is out: update first, or put it off (the stop names both commands) · 64 usage
3407
3407
  writeResult(result, output(), (data) => renderRead(data, options.color), streams);
3408
3408
  }));
3409
3409
  const send = program.command("send").description("prepare an email, and send it once a person has approved it");
3410
- accountOption(send.command("prepare")).description("build an email and show exactly what would go, to whom. Sends nothing").requiredOption("--from <address>", "the sender, at a verified domain: `Name <you@example.com>` or a bare address").requiredOption("--to <address>", "a recipient; repeat for more", collect, []).option("--cc <address>", "a copy recipient; repeat for more", collect, []).option("--bcc <address>", "a blind recipient; repeat for more — the preview lists every one", collect, []).requiredOption("--subject <subject>", "the subject").option("--text <text>", "the plain-text body").option("--html <html>", "an HTML body whose visible text is the text body; no images, forms or hidden parts").option("--attach <path>", "a file to attach, from an allowed folder; repeat for more", collect, []).option("--reply-to <address>", "where replies go; repeat for more", collect, []).option("--in-reply-to <messageId>", "the Message-ID this replies to, to keep the thread").option("--references <messageId>", "an earlier Message-ID in the thread; repeat for more", collect, []).option("--scheduled-at <time>", "send later: an ISO 8601 time with a zone, at most 30 days ahead").action(act(async (context, _options, flags) => {
3410
+ accountOption(send.command("prepare")).description("build an email and show exactly what would go, to whom. Sends nothing").requiredOption("--from <address>", "the sender, at a verified domain: `Name <you@example.com>` or a bare address").requiredOption("--to <address>", "a recipient; repeat for more", collect, []).option("--cc <address>", "a copy recipient; repeat for more", collect, []).option("--bcc <address>", "a blind recipient; repeat for more — the preview lists every one", collect, []).requiredOption("--subject <subject>", "the subject").option("--text <text>", "the plain-text body").option("--html <html>", "an HTML body whose visible text is the text body; no images, forms or hidden parts").option("--attach <path>", "a file to attach, from an allowed folder; repeat for more", collect, []).option("--reply-to <address>", "where replies go; repeat for more", collect, []).option("--in-reply-to <messageId>", "the Message-ID this replies to, to keep the thread").option("--references <messageId>", "an earlier Message-ID in the thread; repeat for more", collect, []).option("--scheduled-at <time>", "send later: an ISO 8601 time with a zone, at most 30 days ahead").option("--as <who>", "who it appears to come from: agent (From labelled) or me (From as given, only when the person said so)").action(act(async (context, _options, flags) => {
3411
3411
  const prepared = await prepareSend(context, String(flags.account), {
3412
3412
  from: String(flags.from),
3413
3413
  to: flags.to,
@@ -3420,7 +3420,8 @@ is out: update first, or put it off (the stop names both commands) · 64 usage
3420
3420
  replyTo: flags.replyTo,
3421
3421
  inReplyTo: flags.inReplyTo === void 0 ? void 0 : String(flags.inReplyTo),
3422
3422
  references: flags.references,
3423
- scheduledAt: flags.scheduledAt === void 0 ? void 0 : String(flags.scheduledAt)
3423
+ scheduledAt: flags.scheduledAt === void 0 ? void 0 : String(flags.scheduledAt),
3424
+ as: flags.as === void 0 ? void 0 : String(flags.as)
3424
3425
  });
3425
3426
  writeResult(prepared, output(), renderPrepared, streams);
3426
3427
  }));
@@ -3505,7 +3506,7 @@ is out: update first, or put it off (the stop names both commands) · 64 usage
3505
3506
  }));
3506
3507
  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) => {
3507
3508
  ran = true;
3508
- const startResendStdioServer = deps.startMcp ?? (await import("./stdio-entry-BfcIk24a.mjs")).startResendStdioServer;
3509
+ const startResendStdioServer = deps.startMcp ?? (await import("./stdio-entry-FGuDOmop.mjs")).startResendStdioServer;
3509
3510
  const core = coreForInvocation();
3510
3511
  await startResendStdioServer({
3511
3512
  ...deps,
package/dist/index.d.mts CHANGED
@@ -4322,7 +4322,8 @@ declare namespace external_d_exports {
4322
4322
  export { $brand, $input, $output, BRAND, CompileOptions, GlobalMeta, INVALID, Infer$1 as Infer, IssueData, JSONType, NEVER, $RefinementCtx as RefinementCtx, SafeExtendShape, Schema, TimePrecision, TypeOf, ZodAny, ZodArray, ZodBase64, ZodBase64URL, ZodBigInt, ZodBigIntFormat, ZodBoolean, ZodCIDRv4, ZodCIDRv6, ZodCUID, ZodCUID2, ZodCatch, ZodCodec, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, ZodCoercedNumber, ZodCoercedString, ZodCompileAsyncError, ZodCompileUnsupportedError, ZodCreditCard, ZodCustom, ZodCustomStringFormat, ZodDate, ZodDefault, ZodDiscriminatedUnion, ZodE164, ZodEmail, ZodEmoji, ZodEnum, ZodError, ZodErrorMap, ZodExactOptional, ZodFile, ZodFirstPartySchemaTypes, ZodFirstPartyTypeKind, ZodFlattenedError, ZodFloat32, ZodFloat64, ZodFormattedError, ZodFunction, ZodGUID, ZodIBAN, ZodIPv4, ZodIPv6, ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime, ZodInstanceOf, ZodInt, ZodInt32, ZodInt64, ZodIntersection, ZodIssue, ZodIssueCode, ZodJSONSchema, ZodJSONSchemaInternals, ZodJWT, ZodKSUID, ZodLazy, ZodLiteral, ZodMAC, ZodMap, ZodNaN, ZodNanoID, ZodNever, ZodNonOptional, ZodNull, ZodNullable, ZodNumber, ZodNumberFormat, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPreprocess, ZodPromise, ZodRawShape$1 as ZodRawShape, ZodReadonly, ZodRealError, ZodRecord, ZodSafeParseError, ZodSafeParseResult, ZodSafeParseSuccess, ZodSchema, ZodSet, ZodStandardSchemaWithJSON, ZodString, ZodStringFormat, ZodSuccess, ZodSymbol, ZodTemplateLiteral, ZodTransform, ZodTuple, ZodType, ZodTypeAny, ZodUInt32, ZodUInt64, ZodULID, ZodURL, ZodUUID, ZodUndefined, ZodUnion, ZodUnknown, ZodVoid, ZodXID, ZodXor, _ZodBigInt, _ZodBoolean, _ZodDate, _ZodNumber, _ZodString, _ZodType, _default, _function, any, array, base64, base64url, bigint$1 as bigint, boolean$1 as boolean, _catch as catch, check, cidrv4, cidrv6, clone, codec, coerce_d_exports as coerce, compile, config, index_d_exports$1 as core, creditCard, cuid, cuid2, currencyCode, custom, date$2 as date, decode, decodeAsync, deepPartial, describe, discriminatedUnion, e164, email, emoji, encode, encodeAsync, _endsWith as endsWith, _enum as enum, exactOptional, file, flattenError, float32, float64, formatError, fromJSONSchema, _function as function, getDiscriminatedOption, getErrorMap, globalRegistry, _gt as gt, _gte as gte, guid, hash, hex, hostname, httpUrl, iban, _includes as includes, output$1 as infer, inferFlattenedErrors, inferFormattedError, input, _instanceof as instanceof, int, int32, int64, intersection, invertCodec, ipv4, ipv6, iso_d_exports as iso, json, jwt, keyof, ksuid, lazy, _length as length, literal, index_d_exports$2 as locales, looseObject, looseRecord, _lowercase as lowercase, _lt as lt, _lte as lte, mac, map, _maxLength as maxLength, _maxSize as maxSize, memoizer, meta, _mime as mime, _minLength as minLength, _minSize as minSize, _multipleOf as multipleOf, nan, nanoid, nativeEnum, _negative as negative, never, _nonnegative as nonnegative, nonoptional, _nonpositive as nonpositive, _normalize as normalize, _null as null, nullable, nullish, number$1 as number, object, optional, output, _overwrite as overwrite, parse, parseAsync, partialRecord, pipe, _positive as positive, prefault, preprocess, prettifyError, promise, _properties as properties, _property as property, readonly, record, refine, _regex as regex, regexes_d_exports as regexes, registry, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync, set, setErrorMap, _size as size, _slugify as slugify, _startsWith as startsWith, strictObject, string$1 as string, stringFormat, stringbool, success, superRefine, symbol, templateLiteral, toJSONSchema, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, toZod, transform, treeifyError, _trim as trim, tuple, uint32, uint64, ulid, _undefined as undefined, union, unknown, _uppercase as uppercase, url, util_d_exports as util, uuid, uuidv4, uuidv6, uuidv7, validate, validateAsync, _void as void, withParser, xid, xor };
4323
4323
  }
4324
4324
  //#endregion
4325
- //#region ../core/dist/change-flow-j65bYTxL.d.mts
4325
+ //#region ../core/dist/change-flow-DOyBTYin.d.mts
4326
+ //#endregion
4326
4327
  //#region src/config-hooks.d.ts
4327
4328
  /**
4328
4329
  * Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
@@ -4780,6 +4781,16 @@ interface ConfigBody {
4780
4781
  /** Superseded rotating credentials which still need conclusive per-token cleanup. */
4781
4782
  pendingRevocations?: PendingRevocation[] | undefined;
4782
4783
  defaults: Defaults;
4784
+ /**
4785
+ * How what an agent sends is labelled (design 2026-10-15-agent-label, L5): the word, the name, the rules that say
4786
+ * who a message appears to come from, and each Slack account's agent emoji.
4787
+ *
4788
+ * `unknown` here, on purpose: it is read only through `agentLabelOf`, which reads a value it does not understand
4789
+ * as the stricter one — an unknown word as `agent`, a rule list it cannot read as no rules — so a hand edit or a
4790
+ * later release's key can only tighten. Kept through every write as the schemas keep every key they do not name, and
4791
+ * judged by `classifyChange`.
4792
+ */
4793
+ agentLabel?: unknown;
4783
4794
  }
4784
4795
  interface ConfigV1 extends ConfigBody {
4785
4796
  version: 1;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { Bt as RESEND_DOWNLOAD_ORIGIN, Et as stripInvisible, H as CommsError, Ht as VERSION, It as PACKAGE_NAME, Lt as RESEND_CALLER, N as resendRequest, P as ResendContext, Rt as REFUSED, Vt as ROUTES, b as readBody, ft as neutralise, h as resendId, pt as newBoundary, v as addressesOf, y as attachmentRisks, z as keyPermissionOf, zt as RESEND_API_ORIGIN } from "./send-BOMCKvLT.mjs";
2
- import { t as createResendMcpServer } from "./server-CbcJL9CZ.mjs";
1
+ import { Bt as RESEND_DOWNLOAD_ORIGIN, Et as stripInvisible, H as CommsError, Ht as VERSION, It as PACKAGE_NAME, Lt as RESEND_CALLER, N as resendRequest, P as ResendContext, Rt as REFUSED, Vt as ROUTES, b as readBody, ft as neutralise, h as resendId, pt as newBoundary, v as addressesOf, y as attachmentRisks, z as keyPermissionOf, zt as RESEND_API_ORIGIN } from "./send-Ch-s9Q6H.mjs";
2
+ import { t as createResendMcpServer } from "./server-ERFs-XiT.mjs";
3
3
  //#region src/operations/events.ts
4
4
  const BODY_LIMIT = 2e4;
5
5
  const STATUS = /* @__PURE__ */ new Set([
@@ -12,7 +12,7 @@ import { execFile, spawn } from "node:child_process";
12
12
  //#endregion
13
13
  //#region src/version.ts
14
14
  /** The package version, read from package.json at build time. */
15
- const VERSION$1 = "0.18.1";
15
+ const VERSION$1 = "0.19.0";
16
16
  //#endregion
17
17
  //#region src/api/routes.ts
18
18
  /**
@@ -201,7 +201,7 @@ const RESEND_CALLER = Object.freeze({
201
201
  packageName: PACKAGE_NAME
202
202
  });
203
203
  //#endregion
204
- //#region ../core/dist/update-check-DvZxdFU_.mjs
204
+ //#region ../core/dist/update-check-CVvpp5G8.mjs
205
205
  /**
206
206
  * Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
207
207
  * optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
@@ -1150,6 +1150,228 @@ var TaintCollector = class {
1150
1150
  await store.record(this.#observations, exclusions, this.#handles);
1151
1151
  }
1152
1152
  };
1153
+ /**
1154
+ * One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
1155
+ * and the preview renderer (which makes them visible), so the two can never disagree.
1156
+ */
1157
+ /** Zero-width and formatting characters, bidi controls, variation selectors and Unicode tag characters. */
1158
+ const INVISIBLE_RANGES = [
1159
+ [173, 173],
1160
+ [847, 847],
1161
+ [1564, 1564],
1162
+ [4447, 4448],
1163
+ [6068, 6069],
1164
+ [6155, 6159],
1165
+ [8203, 8207],
1166
+ [8232, 8233],
1167
+ [8234, 8238],
1168
+ [8288, 8292],
1169
+ [8294, 8303],
1170
+ [12644, 12644],
1171
+ [65024, 65039],
1172
+ [65279, 65279],
1173
+ [65440, 65440],
1174
+ [917504, 917631]
1175
+ ];
1176
+ function isInvisible(codePoint) {
1177
+ for (const [from, to] of INVISIBLE_RANGES) if (codePoint >= from && codePoint <= to) return true;
1178
+ return false;
1179
+ }
1180
+ /**
1181
+ * C0 controls except tab and newline (ESC, and so every ANSI/OSC sequence, starts here), DEL, and C1 controls (CSI
1182
+ * among them). A lone carriage return counts: it moves a terminal cursor back over text already printed.
1183
+ */
1184
+ function isControl(codePoint) {
1185
+ if (codePoint === 9 || codePoint === 10) return false;
1186
+ return codePoint < 32 || codePoint === 127 || codePoint >= 128 && codePoint <= 159;
1187
+ }
1188
+ function isDangerous(codePoint) {
1189
+ return isControl(codePoint) || isInvisible(codePoint);
1190
+ }
1191
+ /**
1192
+ * Strips control characters (ESC, CSI, OSC and the rest), DEL, lone carriage returns, zero-width, bidi-control and tag
1193
+ * characters from sender-controlled text; returns the text and how many were removed. CRLF becomes LF first.
1194
+ *
1195
+ * This lives beside the table rather than in the sanitiser because `neutralise` needs it too, and the two must not
1196
+ * drift: a pattern that looks for `</untrusted-content` cannot see it through a zero-width space, so stripping
1197
+ * has to happen before any such pattern runs, on every path, not only on the ones that render a body.
1198
+ */
1199
+ function stripInvisible(text) {
1200
+ let removed = 0;
1201
+ let out = "";
1202
+ for (const char of text.replace(/\r\n/g, "\n")) if (isDangerous(char.codePointAt(0) ?? 0)) removed += 1;
1203
+ else out += char;
1204
+ return {
1205
+ text: out,
1206
+ removed
1207
+ };
1208
+ }
1209
+ /**
1210
+ * Labelling what an agent sends (design 2026-10-15-agent-label): the phrase `<name> (<word>)`, who a message appears to
1211
+ * come from, and which setting changes loosen that. Pure: nothing here reads the configuration or talks to a channel,
1212
+ * so every channel decides the same way from the same settings.
1213
+ */
1214
+ /** L2: the words a label may use, exactly as written. Adding one is a code change with its own review. */
1215
+ const AGENT_LABEL_WORDS = Object.freeze([
1216
+ "agent",
1217
+ "assistant",
1218
+ "twin",
1219
+ "minion",
1220
+ "bot",
1221
+ "fake",
1222
+ "AI"
1223
+ ]);
1224
+ /** L4: the kinds of act a rule may name. Slack posts, replies, reacts, edits and shares files; mail sends and replies. */
1225
+ const ACT_KINDS = Object.freeze([
1226
+ "post",
1227
+ "reply",
1228
+ "reaction",
1229
+ "edit",
1230
+ "file",
1231
+ "send"
1232
+ ]);
1233
+ /** L2's characters a name may not hold: parentheses would fake a second label, the rest break an address header. */
1234
+ const FORBIDDEN_NAME_CHARACTERS = /[()<>"@,;:\\]/u;
1235
+ function isAgentLabelWord(value) {
1236
+ return typeof value === "string" && AGENT_LABEL_WORDS.includes(value);
1237
+ }
1238
+ /** Why a name cannot be labelled, or undefined when it can. */
1239
+ function nameProblem(name) {
1240
+ if (name.length === 0) return "the name is empty";
1241
+ if (name.trim() !== name) return "the name starts or ends with a space";
1242
+ if (FORBIDDEN_NAME_CHARACTERS.test(name)) return "the name holds a parenthesis or one of <>\"@,;:\\";
1243
+ for (const char of name) if (isDangerous(char.codePointAt(0) ?? 0)) return "the name holds a hidden or control character";
1244
+ }
1245
+ /** L1: `<name> (<word>)`, refused rather than cut short when it does not fit `limit` characters. */
1246
+ function labelPhrase(name, word, limit) {
1247
+ const suffix = ` (${word})`;
1248
+ const bare = name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
1249
+ const problem = nameProblem(bare);
1250
+ if (problem !== void 0) throw new CommsError("USAGE", `the label cannot be made: ${problem}`);
1251
+ const phrase = `${bare}${suffix}`;
1252
+ if ([...phrase].length > limit) throw new CommsError("USAGE", `the label "${phrase}" is longer than this platform's ${limit} characters`, { hint: "Set a shorter name with `label name`." });
1253
+ return phrase;
1254
+ }
1255
+ /** L3's sentence for an unlabelled message, the first line of its preview. */
1256
+ const UNLABELLED_PREVIEW_LINE = "Sent as you, with no label: it will look as if you wrote it yourself";
1257
+ function addressMatches(pattern, address) {
1258
+ const want = pattern.toLowerCase();
1259
+ const got = address.toLowerCase();
1260
+ return want.startsWith("@") ? got.endsWith(want) && got.indexOf("@") === got.length - want.length : got === want;
1261
+ }
1262
+ function ruleMatches(rule, query) {
1263
+ if (rule.account !== query.account) return false;
1264
+ if (rule.accountId !== void 0 && rule.accountId !== query.accountId) return false;
1265
+ if (rule.kinds !== void 0 && !rule.kinds.includes(query.kind)) return false;
1266
+ if (rule.conversation === void 0) return true;
1267
+ if (query.slack !== void 0) return rule.conversation === query.slack.channel || rule.conversation === query.slack.dmWith;
1268
+ if (query.mail !== void 0) {
1269
+ const conversation = rule.conversation;
1270
+ return query.mail.recipients.length > 0 && query.mail.recipients.every((address) => addressMatches(conversation, address));
1271
+ }
1272
+ return false;
1273
+ }
1274
+ /** L4: conversation and kinds, then conversation, then kinds, then the bare account rule. */
1275
+ function specificity(rule) {
1276
+ return (rule.conversation === void 0 ? 0 : 2) + (rule.kinds === void 0 ? 0 : 1);
1277
+ }
1278
+ /** L3: the message, then the most specific matching rule (`agent` winning a tie), then the default `agent`. */
1279
+ function resolveIdentity(settings, query) {
1280
+ if (query.requested !== void 0) return {
1281
+ identity: query.requested,
1282
+ source: "message"
1283
+ };
1284
+ let best;
1285
+ for (const rule of settings?.rules ?? []) {
1286
+ if (!ruleMatches(rule, query)) continue;
1287
+ if (best === void 0 || specificity(rule) > specificity(best) || specificity(rule) === specificity(best) && rule.as === "agent") best = rule;
1288
+ }
1289
+ return best === void 0 ? {
1290
+ identity: "agent",
1291
+ source: "default"
1292
+ } : {
1293
+ identity: best.as,
1294
+ source: "rule",
1295
+ rule: best
1296
+ };
1297
+ }
1298
+ const ruleKey = (rule) => JSON.stringify([
1299
+ rule.account,
1300
+ rule.accountId ?? null,
1301
+ rule.conversation ?? null,
1302
+ rule.kinds === void 0 ? null : [...rule.kinds].sort(),
1303
+ rule.as
1304
+ ]);
1305
+ /**
1306
+ * L5: the changes that need the person's confirmation — anything that could leave a message unlabelled or change how
1307
+ * the label looks. Adding an `agent` rule or removing a `me` rule tightens, and is not listed.
1308
+ */
1309
+ function classifyAgentLabelChange(before, after) {
1310
+ const changes = [];
1311
+ const wordBefore = before?.word ?? "agent";
1312
+ const wordAfter = after?.word ?? "agent";
1313
+ if (wordBefore !== wordAfter) changes.push({
1314
+ path: "agentLabel.word",
1315
+ was: wordBefore,
1316
+ now: wordAfter
1317
+ });
1318
+ if ((before?.name ?? null) !== (after?.name ?? null)) changes.push({
1319
+ path: "agentLabel.name",
1320
+ was: before?.name,
1321
+ now: after?.name
1322
+ });
1323
+ const rulesBefore = new Map((before?.rules ?? []).map((rule) => [ruleKey(rule), rule]));
1324
+ const rulesAfter = new Map((after?.rules ?? []).map((rule) => [ruleKey(rule), rule]));
1325
+ const meAdded = [...rulesAfter].some(([key, rule]) => rule.as === "me" && !rulesBefore.has(key));
1326
+ const agentRemoved = [...rulesBefore].some(([key, rule]) => rule.as === "agent" && !rulesAfter.has(key));
1327
+ if (meAdded || agentRemoved) changes.push({
1328
+ path: "agentLabel.rules",
1329
+ was: before?.rules ?? [],
1330
+ now: after?.rules ?? []
1331
+ });
1332
+ if (JSON.stringify(sorted(before?.slackIcons)) !== JSON.stringify(sorted(after?.slackIcons))) changes.push({
1333
+ path: "agentLabel.slackIcons",
1334
+ was: before?.slackIcons ?? {},
1335
+ now: after?.slackIcons ?? {}
1336
+ });
1337
+ return changes;
1338
+ }
1339
+ function sorted(record) {
1340
+ return Object.entries(record ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1341
+ }
1342
+ const EMOJI_NAME = /^:[a-z0-9_+'-]{1,100}:$/u;
1343
+ function isRule(value) {
1344
+ if (typeof value !== "object" || value === null) return false;
1345
+ const rule = value;
1346
+ if (typeof rule.account !== "string" || rule.account.length === 0) return false;
1347
+ if (rule.as !== "agent" && rule.as !== "me") return false;
1348
+ if (rule.accountId !== void 0 && (typeof rule.accountId !== "string" || rule.accountId.length === 0)) return false;
1349
+ if (rule.conversation !== void 0 && (typeof rule.conversation !== "string" || rule.conversation.length === 0)) return false;
1350
+ if (rule.kinds !== void 0 && !(Array.isArray(rule.kinds) && rule.kinds.length > 0 && rule.kinds.every((kind) => ACT_KINDS.includes(kind)))) return false;
1351
+ return true;
1352
+ }
1353
+ /**
1354
+ * The stored `agentLabel`, read so that a bad value can only tighten. An unknown word reads as `agent`, an invalid
1355
+ * name as no name, and a rule list with any invalid rule is ignored whole: dropping one `agent` rule could leave a
1356
+ * broader `me` rule in charge. A malformed icon entry is dropped, falling back to the plain photo.
1357
+ */
1358
+ function readAgentLabelSettings(raw) {
1359
+ if (typeof raw !== "object" || raw === null) return {};
1360
+ const stored = raw;
1361
+ const settings = {};
1362
+ if (isAgentLabelWord(stored.word)) settings.word = stored.word;
1363
+ if (typeof stored.name === "string" && nameProblem(stored.name) === void 0) settings.name = stored.name;
1364
+ if (Array.isArray(stored.rules) && stored.rules.every(isRule)) settings.rules = stored.rules.map((rule) => ({ ...rule }));
1365
+ if (typeof stored.slackIcons === "object" && stored.slackIcons !== null) {
1366
+ const icons = Object.entries(stored.slackIcons).filter((entry) => typeof entry[1] === "string" && EMOJI_NAME.test(entry[1]));
1367
+ if (icons.length > 0) settings.slackIcons = Object.fromEntries(icons);
1368
+ }
1369
+ return settings;
1370
+ }
1371
+ /** The person's label settings from a loaded configuration, read as `readAgentLabelSettings` reads them. */
1372
+ function agentLabelOf(config) {
1373
+ return readAgentLabelSettings(config.agentLabel);
1374
+ }
1153
1375
  /** Every first-party channel, the core first: what core knows about each without installing it. */
1154
1376
  const CHANNEL_SNAPSHOT = [
1155
1377
  {
@@ -1319,7 +1541,11 @@ const CHANNEL_SNAPSHOT = [
1319
1541
  word: "slack",
1320
1542
  can: "post to Slack"
1321
1543
  },
1322
- hosts: ["slack.com", "files.slack.com"],
1544
+ hosts: [
1545
+ "slack.com",
1546
+ "files.slack.com",
1547
+ "avatars.slack-edge.com"
1548
+ ],
1323
1549
  approve: "agent-slack approve",
1324
1550
  skills: {
1325
1551
  prefix: "slack-",
@@ -3237,6 +3463,67 @@ function commandLinePackage(command, args) {
3237
3463
  const line = [command, ...withoutPathOptions(args).words].join(" ");
3238
3464
  return /(@[\w.-]+\/[\w.-]+|(?<=\s)[\w.-]+-mcp)(?=@|\s|$)/.exec(line)?.[1];
3239
3465
  }
3466
+ /**
3467
+ * The environment a child process is started with: `env` as it is, and on Windows also
3468
+ * `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
3469
+ * program from its current folder either. Elsewhere nothing is added: a Unix shell looks in the current folder only
3470
+ * when `PATH` says to.
3471
+ */
3472
+ function childEnvironment(env = process.env, platform = process.platform) {
3473
+ if (platform !== "win32") return env;
3474
+ return {
3475
+ ...env,
3476
+ NoDefaultCurrentDirectoryInExePath: "1"
3477
+ };
3478
+ }
3479
+ /**
3480
+ * The directories of a search path that name a folder on their own: absolute, and on Windows on a drive. An empty
3481
+ * entry means the current folder to a Unix shell, and a relative one is read against it; either would make the program
3482
+ * found — and run — depend on where this was started, which is where a download may have saved a stranger's file.
3483
+ */
3484
+ function absoluteSearchPath(entries, platform = process.platform) {
3485
+ return entries.filter((entry) => platform === "win32" ? /^[A-Za-z]:[\\/]/.test(entry) : entry !== "" && path.posix.isAbsolute(entry));
3486
+ }
3487
+ /** This Node's real file, or its path as reported when that cannot be resolved. */
3488
+ function realFile(path) {
3489
+ try {
3490
+ return realpathSync(path);
3491
+ } catch {
3492
+ return path;
3493
+ }
3494
+ }
3495
+ /** Two real paths are one file: compared without case on Windows, whose paths are. */
3496
+ function sameRealPath(a, b, platform) {
3497
+ return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
3498
+ }
3499
+ /** The names `node` is found by in one folder of PATH, as a shell finds it: with each PATHEXT extension on Windows. */
3500
+ function nodeNames(env, platform) {
3501
+ if (platform !== "win32") return ["node"];
3502
+ return (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean).map((extension) => `node${extension.toLowerCase()}`);
3503
+ }
3504
+ /**
3505
+ * The path a command names this Node by: the first `node` on `env`'s PATH that is the same real file as
3506
+ * `process.execPath`, as PATH spells it; otherwise `process.execPath`. With no `env`, nothing is looked for.
3507
+ *
3508
+ * Read when a command is located, as everything else in it is (D6): a link removed or repointed later is what the
3509
+ * command then meets. Only absolute folders of PATH are read, as the registrations' own search reads them.
3510
+ */
3511
+ function nodeProgram(env, platform = process.platform) {
3512
+ const own = process.execPath;
3513
+ if (env === void 0) return own;
3514
+ const real = realFile(own);
3515
+ const search = env.PATH ?? env.Path ?? "";
3516
+ for (const directory of absoluteSearchPath(search.split(delimiter), platform)) for (const name of nodeNames(env, platform)) {
3517
+ const candidate = join(directory, name);
3518
+ try {
3519
+ if (!statSync(candidate).isFile()) continue;
3520
+ accessSync(candidate, constants$1.X_OK);
3521
+ if (!sameRealPath(realpathSync(candidate), real, platform)) continue;
3522
+ return sameRealPath(candidate, own, platform) ? own : candidate;
3523
+ } catch {}
3524
+ }
3525
+ return own;
3526
+ }
3240
3527
  const RECEIPT_VERSION = 1;
3241
3528
  const RECEIPT_FILE = "managed-shims.json";
3242
3529
  const MUTATION_LOCK_FILE = "managed-shims.lock";
@@ -3574,27 +3861,6 @@ async function removeManagedShimsForRuntimes(context, runtimes, options = {}) {
3574
3861
  };
3575
3862
  });
3576
3863
  }
3577
- /**
3578
- * The environment a child process is started with: `env` as it is, and on Windows also
3579
- * `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
3580
- * program from its current folder either. Elsewhere nothing is added: a Unix shell looks in the current folder only
3581
- * when `PATH` says to.
3582
- */
3583
- function childEnvironment(env = process.env, platform = process.platform) {
3584
- if (platform !== "win32") return env;
3585
- return {
3586
- ...env,
3587
- NoDefaultCurrentDirectoryInExePath: "1"
3588
- };
3589
- }
3590
- /**
3591
- * The directories of a search path that name a folder on their own: absolute, and on Windows on a drive. An empty
3592
- * entry means the current folder to a Unix shell, and a relative one is read against it; either would make the program
3593
- * found — and run — depend on where this was started, which is where a download may have saved a stranger's file.
3594
- */
3595
- function absoluteSearchPath(entries, platform = process.platform) {
3596
- return entries.filter((entry) => platform === "win32" ? /^[A-Za-z]:[\\/]/.test(entry) : entry !== "" && path.posix.isAbsolute(entry));
3597
- }
3598
3864
  /** The install options that pin a server to one account: the generic one, and Gmail's and Slack's own names. */
3599
3865
  const PIN_OPTIONS = Object.freeze([
3600
3866
  "account",
@@ -5192,62 +5458,6 @@ const CONVERSION_HOOKS = Symbol("agentcomms.configConversionHooks");
5192
5458
  * by path can name.
5193
5459
  */
5194
5460
  const SEND_POLICY_HOOKS = Symbol("agentcomms.sendPolicyHooks");
5195
- /**
5196
- * One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
5197
- * and the preview renderer (which makes them visible), so the two can never disagree.
5198
- */
5199
- /** Zero-width and formatting characters, bidi controls, variation selectors and Unicode tag characters. */
5200
- const INVISIBLE_RANGES = [
5201
- [173, 173],
5202
- [847, 847],
5203
- [1564, 1564],
5204
- [4447, 4448],
5205
- [6068, 6069],
5206
- [6155, 6159],
5207
- [8203, 8207],
5208
- [8232, 8233],
5209
- [8234, 8238],
5210
- [8288, 8292],
5211
- [8294, 8303],
5212
- [12644, 12644],
5213
- [65024, 65039],
5214
- [65279, 65279],
5215
- [65440, 65440],
5216
- [917504, 917631]
5217
- ];
5218
- function isInvisible(codePoint) {
5219
- for (const [from, to] of INVISIBLE_RANGES) if (codePoint >= from && codePoint <= to) return true;
5220
- return false;
5221
- }
5222
- /**
5223
- * C0 controls except tab and newline (ESC, and so every ANSI/OSC sequence, starts here), DEL, and C1 controls (CSI
5224
- * among them). A lone carriage return counts: it moves a terminal cursor back over text already printed.
5225
- */
5226
- function isControl(codePoint) {
5227
- if (codePoint === 9 || codePoint === 10) return false;
5228
- return codePoint < 32 || codePoint === 127 || codePoint >= 128 && codePoint <= 159;
5229
- }
5230
- function isDangerous(codePoint) {
5231
- return isControl(codePoint) || isInvisible(codePoint);
5232
- }
5233
- /**
5234
- * Strips control characters (ESC, CSI, OSC and the rest), DEL, lone carriage returns, zero-width, bidi-control and tag
5235
- * characters from sender-controlled text; returns the text and how many were removed. CRLF becomes LF first.
5236
- *
5237
- * This lives beside the table rather than in the sanitiser because `neutralise` needs it too, and the two must not
5238
- * drift: a pattern that looks for `</untrusted-content` cannot see it through a zero-width space, so stripping
5239
- * has to happen before any such pattern runs, on every path, not only on the ones that render a body.
5240
- */
5241
- function stripInvisible(text) {
5242
- let removed = 0;
5243
- let out = "";
5244
- for (const char of text.replace(/\r\n/g, "\n")) if (isDangerous(char.codePointAt(0) ?? 0)) removed += 1;
5245
- else out += char;
5246
- return {
5247
- text: out,
5248
- removed
5249
- };
5250
- }
5251
5461
  const WINDOWS_RESERVED$1 = /^(con|prn|aux|nul|conin\$|conout\$|com[0-9¹²³]|lpt[0-9¹²³])$/i;
5252
5462
  const UNSAFE_FILENAME_CHARS = /[/\\\u0000-\u001f\u007f<>:"|?*]/g;
5253
5463
  /** Truncates a string to at most `maxBytes` UTF-8 bytes without splitting a code point. */
@@ -6586,11 +6796,39 @@ function classifyChange(before, after) {
6586
6796
  if (before.secrets === void 0 && after.secrets !== void 0 && after.secrets.store !== "keychain") {
6587
6797
  if (holdsSecrets(before)) loosen("secrets.store", secretsStoreOf(before), after.secrets.store);
6588
6798
  }
6799
+ for (const loosening of classifyAgentLabelChange(agentLabelOf(before), agentLabelOf(after))) loosen(loosening.path, labelSettingValue(loosening, loosening.was), labelSettingValue(loosening, loosening.now));
6589
6800
  return {
6590
6801
  loosened: changes.map((change) => change.path),
6591
6802
  changes
6592
6803
  };
6593
6804
  }
6805
+ /**
6806
+ * One label rule in words, as a preview lists it: `acme/slack in C0ACME for post/reply: me`. No comma inside, since a
6807
+ * preview joins a list of them with commas.
6808
+ */
6809
+ function agentLabelRuleText(rule) {
6810
+ const where = rule.conversation === void 0 ? "" : ` in ${rule.conversation}`;
6811
+ const kinds = rule.kinds === void 0 ? "" : ` for ${rule.kinds.join("/")}`;
6812
+ return `${rule.account}${where}${kinds}: ${rule.as}`;
6813
+ }
6814
+ /** A label setting's value as a loosening carries it: the rules in words, the icons as they are, the rest as text. */
6815
+ function labelSettingValue(loosening, value) {
6816
+ if (value === void 0 || value === null) return void 0;
6817
+ if (loosening.path === "agentLabel.rules") return value.map(agentLabelRuleText);
6818
+ if (loosening.path === "agentLabel.slackIcons") return { ...value };
6819
+ return String(value);
6820
+ }
6821
+ /** The label settings a change can write, as the file holds them — raw, as `changedSettings` records every setting. */
6822
+ const LABEL_SETTINGS = [
6823
+ "word",
6824
+ "name",
6825
+ "rules",
6826
+ "slackIcons"
6827
+ ];
6828
+ function storedAgentLabel(config) {
6829
+ const stored = config.agentLabel;
6830
+ return typeof stored === "object" && stored !== null && !Array.isArray(stored) ? stored : {};
6831
+ }
6594
6832
  /** The settings of a mailbox, and of an account, that a change can write — the ones the classifier judges. */
6595
6833
  const INBOX_SETTINGS = [
6596
6834
  "sendPolicy",
@@ -6604,7 +6842,7 @@ const ACCOUNT_SETTINGS = [
6604
6842
  ];
6605
6843
  /**
6606
6844
  * Every setting that differs between `before` and `after`, loosened or tightened: the defaults, the secret store,
6607
- * and each mailbox's and account's own settings — including those of one added or removed.
6845
+ * the agent label's settings, and each mailbox's and account's own settings — including those of one added or removed.
6608
6846
  *
6609
6847
  * What a change approval binds, beside its loosenings. A preview shows the whole change — "sends approved by never;
6610
6848
  * changes approved by chat" — and binding only the loosenings let a claim drop the tightening a person read, since
@@ -6642,6 +6880,9 @@ function changedSettings(before, after) {
6642
6880
  const defaultsAfter = after.defaults;
6643
6881
  for (const key of [.../* @__PURE__ */ new Set([...Object.keys(defaultsBefore), ...Object.keys(defaultsAfter)])].sort()) record(`defaults.${key}`, defaultsBefore[key], defaultsAfter[key]);
6644
6882
  record("secrets.store", before.secrets?.store, after.secrets?.store);
6883
+ const labelBefore = storedAgentLabel(before);
6884
+ const labelAfter = storedAgentLabel(after);
6885
+ for (const key of LABEL_SETTINGS) record(`agentLabel.${key}`, labelBefore[key], labelAfter[key]);
6645
6886
  return changes;
6646
6887
  }
6647
6888
  /**
@@ -10090,7 +10331,7 @@ function folding(platform) {
10090
10331
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
10091
10332
  }
10092
10333
  /** The package version, read from package.json at build time. */
10093
- const VERSION = "0.18.1";
10334
+ const VERSION = "0.19.0";
10094
10335
  /**
10095
10336
  * Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
10096
10337
  * to decide whether the release it last heard of is newer than the one running.
@@ -10268,8 +10509,10 @@ const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manif
10268
10509
  /**
10269
10510
  * Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
10270
10511
  *
10271
- * The program is always this process's Node (`process.execPath`), which must satisfy the target's `engines.node`; no
10272
- * interpreter named anywhere else is run, or asked its version. A TypeScript entry gets `--experimental-strip-types`,
10512
+ * The program is always this process's Node, which must satisfy the target's `engines.node`; no interpreter named
10513
+ * anywhere else is run, or asked its version. It is named as PATH finds it — the first `node` on `request.env`'s PATH
10514
+ * that is the same real file as `process.execPath` — and otherwise by `process.execPath` (CUE-492): the real file only
10515
+ * identifies it, and `process.version` is that file's. A TypeScript entry gets `--experimental-strip-types`,
10273
10516
  * which Node 22.12–22.17 needs, and `--experimental-transform-types` only when this source run was started with it;
10274
10517
  * no other flag this process was started with is carried — debug, preload, condition and memory flags are not what the
10275
10518
  * CLI needs. The suite directories the command uses go between the entry and its words, one canonical option each,
@@ -10310,6 +10553,7 @@ function locateCliCommand(request, runtime = {
10310
10553
  return fail("caller", `${nearest.root} cannot be resolved`);
10311
10554
  }
10312
10555
  if (!isAbsolute(process.execPath)) return fail("interpreter", `this Node, ${process.execPath}, is not a full path`);
10556
+ const program = nodeProgram(request.env);
10313
10557
  const pins = {};
10314
10558
  for (const key of request.uses) pins[key] = request.paths[key];
10315
10559
  let words;
@@ -10318,7 +10562,7 @@ function locateCliCommand(request, runtime = {
10318
10562
  } catch (error) {
10319
10563
  return fail("arguments", error instanceof Error ? error.message : String(error));
10320
10564
  }
10321
- const build = (found) => buildCommand(target, found, words, runtime, callerSource, platform);
10565
+ const build = (found) => buildCommand(target, found, words, runtime, callerSource, platform, program);
10322
10566
  const located = (built, found, direction) => Object.freeze({
10323
10567
  ok: true,
10324
10568
  command: built.command,
@@ -10458,7 +10702,7 @@ function locateCliCommand(request, runtime = {
10458
10702
  * Checks what was found and makes its command: the target's Node range against this Node, then the entry — the source
10459
10703
  * CLI or the manifest's bin for the command it declares — as a readable file inside its package.
10460
10704
  */
10461
- function buildCommand(target, found, words, runtime, callerSource, platform) {
10705
+ function buildCommand(target, found, words, runtime, callerSource, platform, program) {
10462
10706
  const engines = found.manifest.engines;
10463
10707
  const range = typeof engines === "object" && engines !== null ? engines.node : void 0;
10464
10708
  if (typeof range !== "string") return {
@@ -10495,7 +10739,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
10495
10739
  return {
10496
10740
  ok: true,
10497
10741
  command: new PrintedCommand(GATE, [
10498
- process.execPath,
10742
+ program,
10499
10743
  ...flags,
10500
10744
  checked.path,
10501
10745
  ...words
@@ -10799,7 +11043,8 @@ function cliHandoffs(options) {
10799
11043
  uses: usesOf(use),
10800
11044
  paths,
10801
11045
  platform,
10802
- ...registrations === void 0 ? {} : { registrations }
11046
+ ...registrations === void 0 ? {} : { registrations },
11047
+ ...options.env === void 0 ? {} : { env: options.env }
10803
11048
  }, ...options.runtime === void 0 ? [] : [options.runtime]);
10804
11049
  return result.ok ? result.command : result;
10805
11050
  };
@@ -11441,7 +11686,11 @@ const SETTING_LABELS = {
11441
11686
  "defaults.downloadsDir": "downloads folder",
11442
11687
  "defaults.confirm.elicitationClients": "clients trusted to ask for a send approval",
11443
11688
  "defaults.updateCheck": "daily update check",
11444
- "secrets.store": "where credentials are kept"
11689
+ "secrets.store": "where credentials are kept",
11690
+ "agentLabel.word": "the word after your name in the agent label",
11691
+ "agentLabel.name": "the name in the agent label",
11692
+ "agentLabel.rules": "who each kind of message appears to come from",
11693
+ "agentLabel.slackIcons": "the icon of the agent’s Slack posts"
11445
11694
  };
11446
11695
  /**
11447
11696
  * What the loosening means for the person, in their terms.
@@ -11466,6 +11715,10 @@ function meaning(field, after, scope) {
11466
11715
  case "defaults.confirm.elicitationClients": return "these clients may ask you to approve a send in their own window";
11467
11716
  case "defaults.updateCheck": return "nothing on this machine will ask npm for a newer release, or stop until one is installed";
11468
11717
  case "secrets.store": return "credentials will move out of the system keychain into files on this disk";
11718
+ case "agentLabel.word": return "what an agent sends will carry this word after your name";
11719
+ case "agentLabel.name": return after === null ? "where a channel has no name of its own for you, an agent’s message will be refused until a name is set" : "where a channel has no name of its own for you, what an agent sends will carry this name";
11720
+ case "agentLabel.rules": return "what a rule sends as `me` goes out as you, with no label: it will look as if you wrote it yourself";
11721
+ case "agentLabel.slackIcons": return "the app’s Slack posts will show this icon beside your name";
11469
11722
  default: return "";
11470
11723
  }
11471
11724
  }
@@ -12814,7 +13067,8 @@ async function withManagedShim(context, product, installed) {
12814
13067
  ...context.core.paths,
12815
13068
  downloadsDir: context.core.paths.dataDir
12816
13069
  },
12817
- platform: context.platform
13070
+ platform: context.platform,
13071
+ env: context.env
12818
13072
  });
12819
13073
  if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
12820
13074
  shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
@@ -12877,7 +13131,7 @@ function serverPruneChange(core, env, request, own) {
12877
13131
  };
12878
13132
  const { packageName, version } = ownProduct(request.channel, own) ?? {
12879
13133
  packageName: serverFacts(request.channel).packageName,
12880
- version: "0.18.1"
13134
+ version: "0.19.0"
12881
13135
  };
12882
13136
  const product = {
12883
13137
  packageName,
@@ -14611,7 +14865,7 @@ function terminalUpdateHooks(core, env, options) {
14611
14865
  };
14612
14866
  }
14613
14867
  //#endregion
14614
- //#region ../core/dist/tool-arguments-DLa_xsSp.mjs
14868
+ //#region ../core/dist/tool-arguments-BkdTZEb8.mjs
14615
14869
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
14616
14870
  const TARGET = "draft-2020-12";
14617
14871
  /**
@@ -27012,16 +27266,34 @@ function parseFrom(raw) {
27012
27266
  if (!angled) return {
27013
27267
  from: fromAddress,
27014
27268
  fromAddress,
27015
- fromDomain
27269
+ fromDomain,
27270
+ name: ""
27016
27271
  };
27017
27272
  const name = headerText((angled[1] ?? "").trim().replace(/^"(.*)"$/, "$1"), "the From name", 100);
27018
27273
  if (/[<>"@,;]/.test(name)) throw bad("the From name may not contain < > \" @ , or ;");
27019
27274
  return {
27020
27275
  from: name ? `${name} <${fromAddress}>` : fromAddress,
27021
27276
  fromAddress,
27022
- fromDomain
27277
+ fromDomain,
27278
+ name
27023
27279
  };
27024
27280
  }
27281
+ /**
27282
+ * The From an agent's send carries (design 2026-10-15-agent-label, R1): L1's phrase, quoted — unquoted, `(agent)` is
27283
+ * an RFC 5322 comment, which a mail client may drop — then the address. `parseFrom` never writes a quote, so a quoted
27284
+ * From is always one this package labelled.
27285
+ */
27286
+ function labelledFrom(phrase, fromAddress) {
27287
+ return `"${phrase}" <${fromAddress}>`;
27288
+ }
27289
+ /** Who a prepared From says the email comes from: the label this package wrote, with its phrase, or the person. */
27290
+ function identityOfFrom(from) {
27291
+ const phrase = /^"(.+)" <[^<>]+>$/.exec(from)?.[1];
27292
+ return phrase !== void 0 && AGENT_LABEL_WORDS.some((word) => phrase.endsWith(` (${word})`)) ? {
27293
+ identity: "agent",
27294
+ phrase
27295
+ } : { identity: "me" };
27296
+ }
27025
27297
  /** A body: controls other than newline and tab are refused, invisible characters are counted for a warning. */
27026
27298
  function bodyText(value) {
27027
27299
  const text = value.replace(/\r\n/g, "\n");
@@ -27081,7 +27353,7 @@ function linksInText(text) {
27081
27353
  /** Builds and checks the outbound message. Refuses anything an agent could not show a person in full. */
27082
27354
  async function buildMessage(input, options) {
27083
27355
  const warnings = [];
27084
- const { from, fromAddress, fromDomain } = parseFrom(input.from);
27356
+ const { from, fromAddress, fromDomain, name: fromName } = parseFrom(input.from);
27085
27357
  const to = list(input.to, "to");
27086
27358
  const cc = list(input.cc, "cc");
27087
27359
  const bcc = list(input.bcc, "bcc");
@@ -27167,6 +27439,7 @@ async function buildMessage(input, options) {
27167
27439
  references,
27168
27440
  scheduledAt
27169
27441
  },
27442
+ fromName,
27170
27443
  links,
27171
27444
  warnings
27172
27445
  };
@@ -28946,7 +29219,7 @@ function previewOf(options) {
28946
29219
  ...options.study.warnings,
28947
29220
  ...options.built.warnings
28948
29221
  ];
28949
- return renderMessagePreview({
29222
+ const preview = {
28950
29223
  recipients: {
28951
29224
  from: message.from,
28952
29225
  to: message.to,
@@ -28971,7 +29244,51 @@ function previewOf(options) {
28971
29244
  links: options.built.links,
28972
29245
  warnings,
28973
29246
  policy: describePolicy(options.effective, options.approvalId, options.flags, options.handoffs)
28974
- });
29247
+ };
29248
+ return `${identityLine(identityOfFrom(message.from))}\n${renderMessagePreview(preview)}`;
29249
+ }
29250
+ /** L3: the first line of a preview — the label recipients will see, or that there is none. */
29251
+ function identityLine(identity) {
29252
+ if (identity.identity === "me" || identity.phrase === void 0) return UNLABELLED_PREVIEW_LINE;
29253
+ return `Sent as ${truncateDisplay(identity.phrase, 120)}: labelled in the From line, so whoever receives it can tell your agent sent it`;
29254
+ }
29255
+ /**
29256
+ * R1: who the email appears to come from, and its From. For `agent`, L1's phrase from the caller's From name, or the
29257
+ * core's `agentLabel.name` when the caller gave a bare address, refused when there is neither or it does not fit; for
29258
+ * `me`, the From as the caller gave it.
29259
+ */
29260
+ function labelled(config, account, accountId, built, requested) {
29261
+ if (requested !== void 0 && requested !== "agent" && requested !== "me") throw new CommsError("USAGE", `"${requested}" is not who an email appears to come from`, { hint: "Use agent or me." });
29262
+ const settings = agentLabelOf(config);
29263
+ if (resolveIdentity(settings, {
29264
+ account,
29265
+ accountId,
29266
+ kind: "send",
29267
+ requested,
29268
+ mail: { recipients: uniqueRecipients(built.message) }
29269
+ }).identity === "me") return {
29270
+ message: built.message,
29271
+ identity: "me"
29272
+ };
29273
+ const name = built.fromName !== "" ? built.fromName : settings.name;
29274
+ if (name === void 0) throw new CommsError("USAGE", "this email is to be labelled as sent by your agent, and there is no name to label", { hint: "Give From a name, `Name <you@example.com>`, or set one with `label name`." });
29275
+ let phrase;
29276
+ try {
29277
+ phrase = labelPhrase(name, settings.word ?? "agent", 100);
29278
+ } catch (error) {
29279
+ if (!(error instanceof CommsError) || error.hint !== void 0) throw error;
29280
+ throw new CommsError(error.code, error.message, {
29281
+ hint: "Give From a name the label can use, or set one with `label name` and give From a bare address.",
29282
+ cause: error
29283
+ });
29284
+ }
29285
+ return {
29286
+ message: {
29287
+ ...built.message,
29288
+ from: labelledFrom(phrase, built.message.fromAddress)
29289
+ },
29290
+ identity: "agent"
29291
+ };
28975
29292
  }
28976
29293
  async function attachPolicy(context) {
28977
29294
  const config = await context.config();
@@ -29000,11 +29317,17 @@ async function prepareSend(context, name, input) {
29000
29317
  "--send",
29001
29318
  "confirm"
29002
29319
  ]), (command) => `A person can change it with ${command}.`) });
29003
- const built = await buildMessage(input, {
29320
+ const { as: requested, ...email } = input;
29321
+ const composed = await buildMessage(email, {
29004
29322
  now: context.now(),
29005
29323
  attach: await attachPolicy(context)
29006
29324
  });
29007
- const domainNote = await checkFromDomain(context, named, built.message.fromDomain);
29325
+ const domainNote = await checkFromDomain(context, named, composed.message.fromDomain);
29326
+ const { message: outbound, identity } = labelled(config, named.name, named.account.id, composed, requested);
29327
+ const built = {
29328
+ ...composed,
29329
+ message: outbound
29330
+ };
29008
29331
  const study = await studyRecipients(context, named, built.message, config.defaults.riskEscalation);
29009
29332
  const requiredPolicy = study.flags.length > 0 ? "confirm" : "chat";
29010
29333
  const effective = stricterPolicy(livePolicy, requiredPolicy);
@@ -29046,6 +29369,7 @@ async function prepareSend(context, name, input) {
29046
29369
  return {
29047
29370
  approvalId: record.approvalId,
29048
29371
  account: name,
29372
+ identity,
29049
29373
  preview: previewOf({
29050
29374
  name,
29051
29375
  approvalId: record.approvalId,
@@ -1,4 +1,4 @@
1
- import { $ as checkForUpdates, A as removeAccountChange, B as strictToolArguments, D as policyChange, E as policyApprovalRefusal, F as CHANGE_POLICIES, G as SEND_LOOKUP, H as CommsError, Ht as VERSION, I as MODES, L as SEND_POLICIES, M as showAccount, Mt as waitCallOptions, Nt as waitForApproval, O as policyReport, Ot as toCommsError, P as ResendContext, Q as changeToolResult, _ as showSentEmail, _t as refuseUnclaimedApproval, c as downloadReceived, d as listReceived, f as listScheduled, g as showReceived, i as prepareSend, jt as updateToolGate, k as policyWanted, l as getMetrics, m as listSuppressions, n as executeSend, o as sendStatus, ot as gatedChange, p as listSentEmails, s as cancelScheduledChange, u as listDomains, w as listAccounts, x as runDoctor } from "./send-BOMCKvLT.mjs";
1
+ import { $ as checkForUpdates, A as removeAccountChange, B as strictToolArguments, D as policyChange, E as policyApprovalRefusal, F as CHANGE_POLICIES, G as SEND_LOOKUP, H as CommsError, Ht as VERSION, I as MODES, L as SEND_POLICIES, M as showAccount, Mt as waitCallOptions, Nt as waitForApproval, O as policyReport, Ot as toCommsError, P as ResendContext, Q as changeToolResult, _ as showSentEmail, _t as refuseUnclaimedApproval, c as downloadReceived, d as listReceived, f as listScheduled, g as showReceived, i as prepareSend, jt as updateToolGate, k as policyWanted, l as getMetrics, m as listSuppressions, n as executeSend, o as sendStatus, ot as gatedChange, p as listSentEmails, s as cancelScheduledChange, u as listDomains, w as listAccounts, x as runDoctor } from "./send-Ch-s9Q6H.mjs";
2
2
  import { S as union, c as boolean, g as object, h as number, s as array, x as string } from "./iso-USuVU_sk.mjs";
3
3
  import { t as McpServer } from "./mcp-DIH4cS6P-CIWWZQbz.mjs";
4
4
  //#region src/mcp/server.ts
@@ -314,7 +314,7 @@ async function createResendMcpServer(options = {}) {
314
314
  })));
315
315
  server.registerTool("resend_send_prepare", {
316
316
  title: "Prepare an email",
317
- description: "Build an email and return the exact preview — every recipient, BCC included, the reach and the From domain — with an approval id. Nothing is sent. Show the preview in full and wait for the person’s yes.",
317
+ description: "Build an email and return the exact preview — every recipient, BCC included, the reach and the From domain — with an approval id. Nothing is sent. Its From is labelled `<name> (agent)` — the From’s name, or the person’s label name for a bare address — unless `as` is `me`, and the preview’s first line says which. Show the preview in full and wait for the person’s yes.",
318
318
  inputSchema: {
319
319
  ...accountArg,
320
320
  from: string().describe("the sender at a verified domain: `Name <you@example.com>` or a bare address"),
@@ -328,7 +328,8 @@ async function createResendMcpServer(options = {}) {
328
328
  replyTo: array(string()).optional(),
329
329
  inReplyTo: string().optional().describe("the Message-ID this replies to, to keep the thread"),
330
330
  references: array(string()).optional(),
331
- scheduledAt: string().optional().describe("send later: an ISO 8601 time with a zone, at most 30 days ahead")
331
+ scheduledAt: string().optional().describe("send later: an ISO 8601 time with a zone, at most 30 days ahead"),
332
+ as: string().min(1).optional().describe("who it appears to come from: agent (From labelled \"<name> (agent)\") or me (From as given). Pass `me` only when the person said so for this email; left out, their rules decide, and then agent")
332
333
  },
333
334
  annotations: {
334
335
  readOnlyHint: false,
@@ -1,4 +1,4 @@
1
- import { t as createResendMcpServer } from "./server-CbcJL9CZ.mjs";
1
+ import { t as createResendMcpServer } from "./server-ERFs-XiT.mjs";
2
2
  //#region src/mcp/stdio-entry.ts
3
3
  /**
4
4
  * Starting the stdio server. On stdio, **stdout is the protocol**: one stray `console.log` corrupts the stream, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcomms/resend",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Resend for coding agents: read domains, sent and received mail, metrics and suppressions, and send mail that nothing sends without a person's approval of exactly what goes out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "THIRD_PARTY_LICENSES"
32
32
  ],
33
33
  "dependencies": {
34
- "@agentcomms/core": "0.18.1"
34
+ "@agentcomms/core": "0.19.0"
35
35
  },
36
36
  "optionalDependencies": {
37
37
  "@napi-rs/keyring": "2.1.0"
@@ -107,5 +107,5 @@
107
107
  "typecheck": "tsc -p tsconfig.json",
108
108
  "test": "node --experimental-strip-types --test --test-timeout=600000 \"test/**/*.test.ts\""
109
109
  },
110
- "gitHead": "fc398edc67287e411f8c5cb821f434220cb83aa5"
110
+ "gitHead": "29a7121620a9ef305a4260f193c40db1980312bd"
111
111
  }