@agentcomms/resend 0.16.0 → 0.18.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.
@@ -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.16.0";
15
+ const VERSION$1 = "0.18.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-23Ii90fQ.mjs
204
+ //#region ../core/dist/update-check-C_PW2LlM.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
@@ -1386,6 +1386,24 @@ const CHANNEL_SNAPSHOT = [
1386
1386
  }
1387
1387
  }
1388
1388
  ];
1389
+ /** Every declared service server: what core knows about it without installing it. */
1390
+ const SERVICE_SNAPSHOT = [{
1391
+ packageName: "@agentcomms/events-daemon",
1392
+ declaration: {
1393
+ kind: "service",
1394
+ binary: "agent-events",
1395
+ server: {
1396
+ defaultName: "events",
1397
+ entry: "src/mcp/server.ts",
1398
+ factory: "createEventsMcpServer"
1399
+ },
1400
+ operations: "src/operations",
1401
+ skills: {
1402
+ prefix: "agentcomms-",
1403
+ contract: "skills/_shared/contract-events.md"
1404
+ }
1405
+ }
1406
+ }];
1389
1407
  const DISCLOSURE_VERSION_KINDS = [
1390
1408
  "rule",
1391
1409
  "target",
@@ -1575,8 +1593,8 @@ function bindingDigestOf(record) {
1575
1593
  * Everywhere but Windows that shell is a POSIX one, and a word goes in single quotes, inside which nothing is special
1576
1594
  * but the quote itself. Windows has two shells, and neither reads single quotes that way: cmd.exe does not take them
1577
1595
  * as quotes at all, so `'C:\Profiles\First Last\outgoing'` reached the command as two words, quote marks and all, and
1578
- * PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A command printed on Windows
1579
- * has to be safe in both, because nothing says which one it will be pasted into (CUE-306) — and safe is not enough:
1596
+ * PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A common line printed on
1597
+ * Windows has to be safe in both — and safe is not enough:
1580
1598
  * the program has to receive the same words from either. Three readers stand between the line and the program. cmd.exe
1581
1599
  * hands the program the line as it is, and the program's own parser (the C runtime's, Node's) splits it. PowerShell
1582
1600
  * reads the line itself, a double-quoted word with its backslashes as plain characters, and then writes a new command
@@ -1603,15 +1621,17 @@ function bindingDigestOf(record) {
1603
1621
  * backslash, which the C runtime reads with the closing quote as `\"` — and doubled for it, PowerShell 7 passes both
1604
1622
  * backslashes on where cmd.exe and Windows PowerShell pass one; and a word with `& | < > ^ ( )` but no whitespace,
1605
1623
  * which PowerShell passes to a `.cmd` script unquoted, for cmd.exe to run `&whoami` from or to split at `|`.
1606
- * - With any other word in it, no line is printed at all (`line` is null), and the printers show the command's words
1624
+ * - With any other word in it, the common renderer has no line, and the printers show the command's words
1607
1625
  * as JSON instead, saying it has to be typed. Not a line with the word left out: a placeholder in its place was
1608
1626
  * still a command that ran — `claude mcp remove NAME` removed whatever entry was called `NAME`, and an install
1609
1627
  * hint's `--force` replaced it — and before that, a word quoted for PowerShell's single quotes, which cmd.exe reads
1610
1628
  * as characters, ran `whoami` from a server named `$x&whoami&`. See `commandAsJson` for why the JSON runs nothing.
1611
1629
  *
1612
- * - The program itself has to be left as it is. Quoted, it is a string to PowerShell rather than a command — PowerShell
1613
- * runs a quoted program only after `&`, which cmd.exe reads as the end of a command — so a program whose path needs
1614
- * quotes, as `C:\Program Files\nodejs\node.exe` does, leaves the command with no line either (CUE-403).
1630
+ * - The program itself has to be left as it is in the common form. Quoted, it is a string to PowerShell rather than a
1631
+ * command. When its path needs quoting, CUE-410 may emit a labelled PowerShell line: `&`, then every word in single
1632
+ * quotes, with an inner single quote doubled. Empty words, trailing backslashes, double quotes and control or
1633
+ * formatting characters retain the inert-JSON fallback because their native-program boundary cannot be proved
1634
+ * across the supported PowerShell versions.
1615
1635
  *
1616
1636
  * Everywhere else there is always a line: single quotes make any word safe.
1617
1637
  *
@@ -1627,9 +1647,23 @@ function quoteCommand(words, platform) {
1627
1647
  };
1628
1648
  const printed = words.map(windowsShellWord);
1629
1649
  const programBare = words.length === 0 || printed[0] === words[0];
1650
+ if (!printed.includes(null) && programBare) return {
1651
+ words,
1652
+ line: printed.join(" "),
1653
+ platform
1654
+ };
1655
+ if (!programBare && printed.slice(1).every((word) => word !== null)) {
1656
+ const powershell = words.map(powerShellWord);
1657
+ if (!powershell.includes(null)) return {
1658
+ words,
1659
+ line: `& ${powershell.join(" ")}`,
1660
+ platform,
1661
+ shell: "powershell"
1662
+ };
1663
+ }
1630
1664
  return {
1631
1665
  words,
1632
- line: printed.includes(null) || !programBare ? null : printed.join(" "),
1666
+ line: null,
1633
1667
  platform
1634
1668
  };
1635
1669
  }
@@ -1642,6 +1676,14 @@ function quotedLineWithWordsToFill(command, ...toFill) {
1642
1676
  if (command.line === null) return null;
1643
1677
  const sentinel = command.words.indexOf("--");
1644
1678
  if (sentinel < 0) return [command.line, ...toFill].join(" ");
1679
+ if (command.shell === "powershell") {
1680
+ const printed = command.words.map((each) => powerShellWord(each));
1681
+ return `& ${[
1682
+ ...printed.slice(0, sentinel),
1683
+ ...toFill,
1684
+ ...printed.slice(sentinel)
1685
+ ].join(" ")}`;
1686
+ }
1645
1687
  const word = command.platform === "win32" ? windowsShellWord : posixShellWord;
1646
1688
  const printed = command.words.map((each) => word(each));
1647
1689
  return [
@@ -1669,6 +1711,11 @@ function windowsShellWord(word) {
1669
1711
  if (/[&|<>^()]/.test(word) && !/\s/.test(word)) return null;
1670
1712
  return `"${word}"`;
1671
1713
  }
1714
+ /** One native-command word in a PowerShell single-quoted literal, or null when a one-line call is not provable. */
1715
+ function powerShellWord(word) {
1716
+ if (word === "" || word.endsWith("\\") || /["\u201C-\u201E]|[\p{C}\p{Zl}\p{Zp}]/u.test(word)) return null;
1717
+ return `'${word.replaceAll("'", "''")}'`;
1718
+ }
1672
1719
  /**
1673
1720
  * The words of a command as a JSON array, for a command that cannot be printed as a line: something to read, and to
1674
1721
  * parse, and nothing to run.
@@ -1699,17 +1746,19 @@ function commandAsJson(words) {
1699
1746
  /** What is said beside a command shown as JSON. */
1700
1747
  const TO_TYPE = "the command's words, written as JSON: one of them cannot be quoted the same way for cmd.exe and for PowerShell, so type the command yourself, with that word quoted for the shell you use";
1701
1748
  /** What is said beside a command shown as JSON because its program's path needs quotes. */
1702
- const TO_TYPE_PROGRAM = "the command's words, written as JSON: the program's path needs quotes, and PowerShell runs a quoted program only after `&`, which cmd.exe does not accept, so type the command yourself, with each word quoted for the shell you use and, in PowerShell, `&` before the program";
1749
+ const TO_TYPE_PROGRAM = "the command's words, written as JSON: the program's path needs quoting and at least one word cannot be represented safely in a one-line PowerShell call, so type the command yourself, with each word quoted for the shell you use and, in PowerShell, `&` before the program";
1703
1750
  function toType(command) {
1704
1751
  return programNeedsQuotes(command) ? TO_TYPE_PROGRAM : TO_TYPE;
1705
1752
  }
1706
1753
  /** Quoted words in backticks, for a sentence — as words in JSON, saying it has to be typed, when there is no line. */
1707
1754
  function inlineQuoted(command) {
1708
- return command.line === null ? `\`${commandAsJson(command.words)}\` (${toType(command)})` : `\`${command.line}\``;
1755
+ if (command.line === null) return `\`${commandAsJson(command.words)}\` (${toType(command)})`;
1756
+ return `\`${command.line}\`${command.shell === "powershell" ? " (PowerShell)" : ""}`;
1709
1757
  }
1710
1758
  /** Quoted words as text of their own — a list's line, a field's value — or as words in JSON, saying to type them. */
1711
1759
  function quotedText(command) {
1712
- return command.line === null ? `${commandAsJson(command.words)} (${toType(command)})` : command.line;
1760
+ if (command.line === null) return `${commandAsJson(command.words)} (${toType(command)})`;
1761
+ return command.shell === "powershell" ? `PowerShell:\n${command.line}` : command.line;
1713
1762
  }
1714
1763
  /**
1715
1764
  * The same command with words the agent is to fill in — a placeholder such as `<folder>` — before its first `--`: for
@@ -2173,7 +2222,7 @@ function handoffSentence(handoff, say, options = {}) {
2173
2222
  function handoffSentenceToFill(handoff, toFill, say, options = {}) {
2174
2223
  if (!isCommand(handoff)) return noCommand(handoff, options);
2175
2224
  const line = lineWithWordsToFill(handoff, ...toFill);
2176
- if (line !== null) return say(`\`${line}\``);
2225
+ if (line !== null) return say(`\`${line}\`${handoff.shell === "powershell" ? " (PowerShell)" : ""}`);
2177
2226
  return say(inlineQuoted(quotedWithWordsToFill(handoff, toFill)));
2178
2227
  }
2179
2228
  /**
@@ -2393,7 +2442,7 @@ function nearestPackage(start) {
2393
2442
  * Whether `path` is `root` or inside it, by whole path segments: `/x/core-evil` is not inside `/x/core`, which a
2394
2443
  * comparison of strings would say it was. Both are taken as given; a caller compares real paths with real paths.
2395
2444
  */
2396
- function isWithin(root, path) {
2445
+ function isWithin$1(root, path) {
2397
2446
  const between = relative(resolve(root), resolve(path));
2398
2447
  return between === "" || !isAbsolute(between) && between !== ".." && !/^\.\.[\\/]/.test(between);
2399
2448
  }
@@ -2463,15 +2512,18 @@ var ExternalCommand = class {
2463
2512
  #reason;
2464
2513
  /** The words, as the program is to receive them. */
2465
2514
  words;
2466
- /** The line to paste; null when one of the words has no printing that every Windows shell passes on alike. */
2515
+ /** The line to paste; null when no supported common or labelled shell rendering is safe. */
2467
2516
  line;
2468
2517
  platform;
2518
+ shell;
2469
2519
  constructor(gate, words, platform, reason) {
2470
2520
  if (gate !== GATE$1) throw new TypeError("an external command is made by externalCommand, and only there");
2471
2521
  this.#reason = reason;
2472
2522
  this.words = Object.freeze([...words]);
2473
- this.line = quoteCommand(this.words, platform).line;
2523
+ const quoted = quoteCommand(this.words, platform);
2524
+ this.line = quoted.line;
2474
2525
  this.platform = platform;
2526
+ if (quoted.shell !== void 0) this.shell = quoted.shell;
2475
2527
  Object.freeze(this);
2476
2528
  }
2477
2529
  /** Why this program, which is not part of this suite, is one a person may be told to run. */
@@ -3185,6 +3237,343 @@ function commandLinePackage(command, args) {
3185
3237
  const line = [command, ...withoutPathOptions(args).words].join(" ");
3186
3238
  return /(@[\w.-]+\/[\w.-]+|(?<=\s)[\w.-]+-mcp)(?=@|\s|$)/.exec(line)?.[1];
3187
3239
  }
3240
+ const RECEIPT_VERSION = 1;
3241
+ const RECEIPT_FILE = "managed-shims.json";
3242
+ const MUTATION_LOCK_FILE = "managed-shims.lock";
3243
+ const POSIX_SHIM_MODE = 493;
3244
+ const WINDOWS_SHIM_NOTICE = "Managed CLI shims are not offered on Windows yet because cmd.exe re-parses arguments forwarded through .cmd files, including shell metacharacters. Use the full-path command or the labelled PowerShell & line the result provides.";
3245
+ function platformOf(context) {
3246
+ return context.platform ?? process.platform;
3247
+ }
3248
+ /** Why the selected platform has no managed shim in this release. */
3249
+ function managedShimUnsupportedNotice(context = {}) {
3250
+ return platformOf(context) === "win32" ? WINDOWS_SHIM_NOTICE : "Managed CLI shims are offered only on macOS and Linux in this release. Use the full-path command the result provides.";
3251
+ }
3252
+ /** The per-user directory managed installs put their command shims in. */
3253
+ function managedShimDirectory(context) {
3254
+ if (!managedShimsSupported(context)) throw new CommsError("CONFIG", managedShimUnsupportedNotice(context));
3255
+ return join(homeDirectory(context.env), ".local", "bin");
3256
+ }
3257
+ /** Managed CLI shims are POSIX-only until Windows has an argument-forwarding form that does not re-parse input. */
3258
+ function managedShimsSupported(context = {}) {
3259
+ const platform = platformOf(context);
3260
+ return platform === "darwin" || platform === "linux";
3261
+ }
3262
+ /** A managed shim's name: one plain command word, never a path, so joining it to the directory stays inside it. */
3263
+ function isSafeBinary(binary) {
3264
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(binary);
3265
+ }
3266
+ function checkedBinary(binary) {
3267
+ if (!isSafeBinary(binary)) throw new CommsError("CONFIG", `${JSON.stringify(binary)} is not a safe managed CLI binary name`);
3268
+ return binary;
3269
+ }
3270
+ /** The exact path for one declared binary's managed shim. */
3271
+ function managedShimPath(context, binary) {
3272
+ return join(managedShimDirectory(context), checkedBinary(binary));
3273
+ }
3274
+ function receiptPath(context) {
3275
+ return join(context.dataDir, RECEIPT_FILE);
3276
+ }
3277
+ function emptyReceipt() {
3278
+ return {
3279
+ version: RECEIPT_VERSION,
3280
+ shims: {}
3281
+ };
3282
+ }
3283
+ async function readReceipt(context) {
3284
+ let parsed;
3285
+ try {
3286
+ parsed = JSON.parse(await readFile(receiptPath(context), "utf8"));
3287
+ } catch (error) {
3288
+ return error.code === "ENOENT" ? emptyReceipt() : null;
3289
+ }
3290
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== RECEIPT_VERSION || typeof parsed.shims !== "object" || parsed.shims === null) return null;
3291
+ const shims = {};
3292
+ for (const [binary, value] of Object.entries(parsed.shims)) {
3293
+ if (!isSafeBinary(binary) || typeof value !== "object" || value === null || ![
3294
+ "path",
3295
+ "digest",
3296
+ "node",
3297
+ "entry"
3298
+ ].every((key) => typeof value[key] === "string")) return null;
3299
+ shims[binary] = value;
3300
+ }
3301
+ return {
3302
+ version: RECEIPT_VERSION,
3303
+ shims
3304
+ };
3305
+ }
3306
+ async function writeReceipt(context, receipt) {
3307
+ await writeFileAtomic(receiptPath(context), `${JSON.stringify(receipt, null, 2)}\n`);
3308
+ }
3309
+ function sha256(bytes) {
3310
+ return createHash("sha256").update(bytes).digest("hex");
3311
+ }
3312
+ function posixWord(word) {
3313
+ return `'${word.replaceAll("'", `'"'"'`)}'`;
3314
+ }
3315
+ function shimSource(spec) {
3316
+ return `#!/bin/sh\n# managed CLI shim v1\nexec ${spec.command.map(posixWord).join(" ")} "$@"\n`;
3317
+ }
3318
+ async function pinShimDirectory(context, create) {
3319
+ const publicDirectory = managedShimDirectory(context);
3320
+ if (create) await mkdir(publicDirectory, {
3321
+ recursive: true,
3322
+ mode: 493
3323
+ });
3324
+ let resolved;
3325
+ try {
3326
+ resolved = await realpath(publicDirectory);
3327
+ } catch (error) {
3328
+ if (!create && error.code === "ENOENT") return null;
3329
+ throw error;
3330
+ }
3331
+ return {
3332
+ public: publicDirectory,
3333
+ resolved
3334
+ };
3335
+ }
3336
+ async function assertPinnedDirectory(directory) {
3337
+ let now;
3338
+ try {
3339
+ now = await realpath(directory.public);
3340
+ } catch {
3341
+ throw new CommsError("CONFIG", `the managed shim directory's resolved directory changed from ${directory.resolved} during the operation`, { hint: "Nothing was changed. Inspect the directory link, then run the install, update or prune again." });
3342
+ }
3343
+ if (now !== directory.resolved) throw new CommsError("CONFIG", `the managed shim directory's resolved directory changed from ${directory.resolved} to ${now} during the operation`, { hint: "Nothing was changed. Inspect the directory link, then run the install, update or prune again." });
3344
+ }
3345
+ async function regularFileBytes(path) {
3346
+ try {
3347
+ if (!(await lstat(path)).isFile()) return null;
3348
+ return await readFile(path, "utf8");
3349
+ } catch (error) {
3350
+ if (error.code === "ENOENT") return null;
3351
+ throw error;
3352
+ }
3353
+ }
3354
+ async function pathExists(path) {
3355
+ try {
3356
+ await lstat(path);
3357
+ return true;
3358
+ } catch (error) {
3359
+ if (error.code === "ENOENT") return false;
3360
+ throw error;
3361
+ }
3362
+ }
3363
+ /** Serialize this installation's receipt and shim mutations across its processes. */
3364
+ async function withMutationLock(context, change) {
3365
+ await mkdir(context.dataDir, {
3366
+ recursive: true,
3367
+ mode: 448
3368
+ });
3369
+ return withFileLock(join(context.dataDir, MUTATION_LOCK_FILE), change, {
3370
+ timeoutMs: 5e3,
3371
+ staleMs: 3e4,
3372
+ renewMs: 1e4,
3373
+ ...context.lockOptions
3374
+ });
3375
+ }
3376
+ /** Publish complete shim bytes only if the target is still absent. */
3377
+ async function createShimExclusive(directory, path, source) {
3378
+ const nonce = randomBytes(6).toString("hex");
3379
+ const temp = join(directory.resolved, `.${basename(path)}.${nonce}.tmp`);
3380
+ await assertPinnedDirectory(directory);
3381
+ const handle = await open(temp, "wx", POSIX_SHIM_MODE);
3382
+ try {
3383
+ await handle.writeFile(source);
3384
+ await handle.sync();
3385
+ } finally {
3386
+ await handle.close();
3387
+ }
3388
+ await assertPinnedDirectory(directory);
3389
+ await chmod(temp, POSIX_SHIM_MODE);
3390
+ try {
3391
+ await assertPinnedDirectory(directory);
3392
+ await link(temp, path);
3393
+ } finally {
3394
+ await assertPinnedDirectory(directory);
3395
+ await rm(temp, { force: true });
3396
+ }
3397
+ }
3398
+ /** Restore an object moved aside without ever replacing a raced object at its public path. */
3399
+ async function restoreMoved(directory, path, moved) {
3400
+ try {
3401
+ await assertPinnedDirectory(directory);
3402
+ await link(moved, path);
3403
+ } catch (error) {
3404
+ if (error.code === "EEXIST") return false;
3405
+ throw error;
3406
+ }
3407
+ await assertPinnedDirectory(directory);
3408
+ await rm(moved);
3409
+ return true;
3410
+ }
3411
+ /**
3412
+ * Move the exact current object aside atomically, then prove that object still has the owned digest. A raced foreign
3413
+ * file is put back; if another object has already occupied its path, both are preserved and the change is refused.
3414
+ */
3415
+ async function moveOwned(directory, path, digest, suffix, before) {
3416
+ await before?.(path);
3417
+ await assertPinnedDirectory(directory);
3418
+ const moved = join(directory.resolved, `.${basename(path)}.${randomBytes(6).toString("hex")}.${suffix}`);
3419
+ try {
3420
+ await rename(path, moved);
3421
+ } catch (error) {
3422
+ if (error.code === "ENOENT") return null;
3423
+ throw error;
3424
+ }
3425
+ const bytes = await regularFileBytes(moved);
3426
+ if (bytes !== null && sha256(bytes) === digest) return moved;
3427
+ if (!await restoreMoved(directory, path, moved)) throw new CommsError("CONFIG", `${path} changed during a managed shim operation; its bytes were preserved at ${moved}`, { hint: "Resolve the two files by hand, then run the install, update or prune again." });
3428
+ return null;
3429
+ }
3430
+ /** Remove only the exact object whose bytes prove it is still owned. */
3431
+ async function removeOwned(directory, path, digest, before) {
3432
+ const moved = await moveOwned(directory, path, digest, "remove", before);
3433
+ if (moved === null) return false;
3434
+ await assertPinnedDirectory(directory);
3435
+ await rm(moved);
3436
+ return true;
3437
+ }
3438
+ function normalPath(path, platform) {
3439
+ const trimmed = path.replace(/[\\/]+$/, "");
3440
+ return platform === "win32" ? trimmed.toLowerCase() : trimmed;
3441
+ }
3442
+ function directoryOnPath(context, directory) {
3443
+ const platform = platformOf(context);
3444
+ const value = context.env.PATH ?? context.env.Path ?? "";
3445
+ const separator = platform === "win32" ? ";" : ":";
3446
+ return value.split(separator).filter(Boolean).some((part) => normalPath(part, platform) === normalPath(directory, platform));
3447
+ }
3448
+ function pathNotice(directory) {
3449
+ return `The shim directory ${directory} is not on PATH. For this shell, run: export PATH=${posixWord(directory)}:"$PATH". No shell profile was edited.`;
3450
+ }
3451
+ /** Create or repoint one shim, but only while its target is free or still matches this installation's receipt. */
3452
+ async function writeManagedShim(context, spec) {
3453
+ checkedBinary(spec.binary);
3454
+ const directory = await pinShimDirectory(context, true);
3455
+ if (directory === null) throw new CommsError("UNEXPECTED", "the managed shim directory was not created");
3456
+ const publicPath = join(directory.public, spec.binary);
3457
+ const path = join(directory.resolved, spec.binary);
3458
+ const result = (status) => {
3459
+ const onPath = directoryOnPath(context, directory.public);
3460
+ return {
3461
+ path: publicPath,
3462
+ directory: directory.public,
3463
+ status,
3464
+ onPath,
3465
+ ...onPath ? {} : { pathNotice: pathNotice(directory.public) }
3466
+ };
3467
+ };
3468
+ return withMutationLock(context, async () => {
3469
+ const receipt = await readReceipt(context);
3470
+ const recorded = receipt?.shims[spec.binary];
3471
+ const current = await regularFileBytes(path);
3472
+ const owned = receipt !== null && recorded !== void 0 && recorded.path === path && current !== null && sha256(current) === recorded.digest;
3473
+ if (await pathExists(path) && !owned) return result("foreign");
3474
+ if (receipt === null || recorded !== void 0 && recorded.path !== path) return result("foreign");
3475
+ const node = spec.command[0];
3476
+ if (node === void 0) throw new CommsError("CONFIG", "the managed shim command has no program");
3477
+ const source = shimSource(spec);
3478
+ const digest = sha256(source);
3479
+ let previous = null;
3480
+ if (owned) {
3481
+ previous = await moveOwned(directory, path, recorded.digest, "old", context.hooks?.beforeReplace);
3482
+ if (previous === null) return result("foreign");
3483
+ }
3484
+ try {
3485
+ await createShimExclusive(directory, path, source);
3486
+ } catch (error) {
3487
+ if (previous !== null) {
3488
+ if (!await restoreMoved(directory, path, previous)) throw new CommsError("CONFIG", `${path} was occupied while its owned shim was being updated; the old shim remains at ${previous}`, {
3489
+ hint: "Resolve the two files by hand, then run the managed install again.",
3490
+ cause: error
3491
+ });
3492
+ }
3493
+ if (error.code === "EEXIST") return result("foreign");
3494
+ throw error;
3495
+ }
3496
+ receipt.shims[spec.binary] = {
3497
+ path,
3498
+ digest,
3499
+ node,
3500
+ entry: spec.entry
3501
+ };
3502
+ try {
3503
+ await assertPinnedDirectory(directory);
3504
+ await writeReceipt(context, receipt);
3505
+ } catch (error) {
3506
+ const removed = await removeOwned(directory, path, digest);
3507
+ if (previous !== null && removed && !await restoreMoved(directory, path, previous)) throw new CommsError("CONFIG", `the shim receipt failed and the old shim could not be restored from ${previous}`, {
3508
+ hint: "Restore that file to the reported shim path, then run the managed install again.",
3509
+ cause: error
3510
+ });
3511
+ throw error;
3512
+ }
3513
+ if (previous !== null) {
3514
+ await assertPinnedDirectory(directory);
3515
+ await rm(previous);
3516
+ }
3517
+ return result(owned ? "updated" : "created");
3518
+ });
3519
+ }
3520
+ function isWithin(root, path) {
3521
+ const child = relative(root, path);
3522
+ return child === "" || !child.startsWith("..") && !isAbsolute(child);
3523
+ }
3524
+ /** Remove unchanged owned shims whose recorded entry is inside one of the runtimes prune removed. */
3525
+ async function removeManagedShimsForRuntimes(context, runtimes, options = {}) {
3526
+ if (!managedShimsSupported(context)) return {
3527
+ removed: [],
3528
+ kept: []
3529
+ };
3530
+ const directory = await pinShimDirectory(context, false);
3531
+ if (directory === null) return {
3532
+ removed: [],
3533
+ kept: []
3534
+ };
3535
+ return withMutationLock(context, async () => {
3536
+ const receipt = await readReceipt(context);
3537
+ if (receipt === null) return {
3538
+ removed: [],
3539
+ kept: []
3540
+ };
3541
+ const removed = [];
3542
+ const kept = [];
3543
+ let changed = false;
3544
+ for (const [binary, recorded] of Object.entries(receipt.shims)) {
3545
+ if (!runtimes.some((runtime) => isWithin(runtime, recorded.entry))) continue;
3546
+ const publicPath = join(directory.public, binary);
3547
+ const path = join(directory.resolved, binary);
3548
+ if (recorded.path !== path) {
3549
+ kept.push(recorded.path);
3550
+ continue;
3551
+ }
3552
+ if (options.dryRun) {
3553
+ const bytes = await regularFileBytes(path);
3554
+ if (bytes !== null && sha256(bytes) === recorded.digest) removed.push(publicPath);
3555
+ else if (await pathExists(path)) kept.push(publicPath);
3556
+ continue;
3557
+ }
3558
+ if (await removeOwned(directory, path, recorded.digest, context.hooks?.beforeRemove)) {
3559
+ delete receipt.shims[binary];
3560
+ changed = true;
3561
+ removed.push(publicPath);
3562
+ } else if (!await pathExists(path)) {
3563
+ delete receipt.shims[binary];
3564
+ changed = true;
3565
+ } else kept.push(publicPath);
3566
+ }
3567
+ if (changed) {
3568
+ await assertPinnedDirectory(directory);
3569
+ await writeReceipt(context, receipt);
3570
+ }
3571
+ return {
3572
+ removed,
3573
+ kept
3574
+ };
3575
+ });
3576
+ }
3188
3577
  /**
3189
3578
  * The environment a child process is started with: `env` as it is, and on Windows also
3190
3579
  * `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
@@ -4269,8 +4658,8 @@ async function mergeIntoJsonConfig(path, client, name, entry) {
4269
4658
  }
4270
4659
  /** Starts the server exactly as a client would, and completes the handshake. */
4271
4660
  async function verifyEntry(entry, product) {
4272
- const { Client } = await import("./dist-COPefvtq-COGS2a-K.mjs");
4273
- const { StdioClientTransport } = await import("./stdio-CHZ2WRcx-B2_wGdA5.mjs");
4661
+ const { Client } = await import("./dist-C7DG2LdL-l0mj9_dH.mjs");
4662
+ const { StdioClientTransport } = await import("./stdio-CyjIgrch-59zcjSEt.mjs");
4274
4663
  const client = new Client({
4275
4664
  name: `${product.binary}-install-check`,
4276
4665
  version: product.version
@@ -4395,7 +4784,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
4395
4784
  runtimeDir: join(context.core.paths.dataDir, "runtime"),
4396
4785
  dryRun: options.dryRun === true,
4397
4786
  removed: [],
4398
- kept: []
4787
+ kept: [],
4788
+ shims: {
4789
+ removed: [],
4790
+ kept: []
4791
+ }
4399
4792
  };
4400
4793
  const candidates = [];
4401
4794
  for (const { path, version } of await listManagedRuntimes(context.core.paths.dataDir, product.packageName)) {
@@ -4462,6 +4855,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
4462
4855
  const left = handedOut.filter((record) => !gone.some((path) => mentions([path], record.runtime)));
4463
4856
  if (left.length !== handedOut.length) await writeFileAtomic(ledger, left.map((record) => `${JSON.stringify(record)}\n`).join(""));
4464
4857
  }
4858
+ if (result.removed.length > 0) result.shims = await removeManagedShimsForRuntimes({
4859
+ env: context.env,
4860
+ dataDir: context.core.paths.dataDir,
4861
+ platform: context.platform
4862
+ }, result.removed.map((item) => item.path), { dryRun: result.dryRun });
4465
4863
  return result;
4466
4864
  }
4467
4865
  /** Every runtime the installer handed out, or null when the record is there and cannot be read. */
@@ -4594,6 +4992,8 @@ function rivalWordWarnings(servers, word, can, product) {
4594
4992
  }
4595
4993
  /** Every channel in core's snapshot, the core first. */
4596
4994
  const CHANNELS = Object.freeze(CHANNEL_SNAPSHOT.map((entry) => entry.manifest.channel));
4995
+ /** Every declared MCP server, channels first and then services, with no separate hand-kept list. */
4996
+ const SERVERS = Object.freeze([...CHANNELS, ...SERVICE_SNAPSHOT.map((entry) => entry.declaration.server.defaultName)]);
4597
4997
  /**
4598
4998
  * The value a pin's flag carries in an entry's arguments: `--account acme/resend`, or `--account=acme/resend` — the
4599
4999
  * first given, in either form.
@@ -4654,8 +5054,28 @@ function serverFactsOf({ packageName, manifest }) {
4654
5054
  };
4655
5055
  }
4656
5056
  const CHANNEL_SERVERS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, serverFactsOf(entry)])));
5057
+ /** Service server facts, derived from each service package's declaration. */
5058
+ const SERVICE_SERVERS = Object.freeze(Object.fromEntries(SERVICE_SNAPSHOT.map(({ packageName, declaration }) => [declaration.server.defaultName, {
5059
+ packageName,
5060
+ binary: declaration.binary,
5061
+ defaultServerName: declaration.server.defaultName,
5062
+ npxPackage: packageName,
5063
+ npxArgs: ["mcp"],
5064
+ serverArgs: () => [],
5065
+ narrowingOf: () => ({})
5066
+ }])));
5067
+ /** All managed servers, derived from channel manifests and service declarations. */
5068
+ const SERVERS_BY_ID = Object.freeze({
5069
+ ...CHANNEL_SERVERS,
5070
+ ...SERVICE_SERVERS
5071
+ });
4657
5072
  /** How each server is named to a person: in a preview, and in what a tool returns. */
4658
5073
  const CHANNEL_LABELS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, entry.manifest.label])));
5074
+ /** Labels for every managed server; a service declares its public server name. */
5075
+ const SERVER_LABELS = Object.freeze({
5076
+ ...CHANNEL_LABELS,
5077
+ ...Object.fromEntries(SERVICE_SNAPSHOT.map(({ declaration }) => [declaration.server.defaultName, declaration.server.defaultName]))
5078
+ });
4659
5079
  /** A channel's manifest, from core's snapshot. */
4660
5080
  function channelManifest(channel) {
4661
5081
  return CHANNEL_SNAPSHOT.find((entry) => entry.manifest.channel === channel)?.manifest;
@@ -4663,10 +5083,18 @@ function channelManifest(channel) {
4663
5083
  function isChannel(value) {
4664
5084
  return typeof value === "string" && CHANNELS.includes(value);
4665
5085
  }
5086
+ /** Whether a word identifies any channel or service server in this release. */
5087
+ function isServer(value) {
5088
+ return typeof value === "string" && SERVERS.includes(value);
5089
+ }
4666
5090
  /** Refuses a word that is not a channel of this release, naming the ones that are. */
4667
5091
  function notAChannel(value) {
4668
5092
  return new CommsError("USAGE", `"${String(value)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
4669
5093
  }
5094
+ /** Refuses a word that is not a declared server of this release. */
5095
+ function notAServer(value) {
5096
+ return new CommsError("USAGE", `"${String(value)}" is not a server`, { hint: `One of: ${SERVERS.join(", ")}.` });
5097
+ }
4670
5098
  /** A channel's server facts, by its word; refused for a word that is not a channel. */
4671
5099
  function channelServer(channel) {
4672
5100
  if (!isChannel(channel)) throw notAChannel(channel);
@@ -4677,6 +5105,16 @@ function channelLabel(channel) {
4677
5105
  if (!isChannel(channel)) throw notAChannel(channel);
4678
5106
  return CHANNEL_LABELS[channel];
4679
5107
  }
5108
+ /** A channel or service's server facts, by its declared server name. */
5109
+ function serverFacts(server) {
5110
+ if (!isServer(server)) throw notAServer(server);
5111
+ return SERVERS_BY_ID[server];
5112
+ }
5113
+ /** How a channel or service server is named to a person. */
5114
+ function serverLabel(server) {
5115
+ if (!isServer(server)) throw notAServer(server);
5116
+ return SERVER_LABELS[server];
5117
+ }
4680
5118
  /** A channel's manifest, by its word; refused for a word that is not a channel. */
4681
5119
  function requireChannelManifest(channel) {
4682
5120
  const manifest = channelManifest(channel);
@@ -9643,7 +10081,7 @@ function folding(platform) {
9643
10081
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
9644
10082
  }
9645
10083
  /** The package version, read from package.json at build time. */
9646
- const VERSION = "0.16.0";
10084
+ const VERSION = "0.18.0";
9647
10085
  /**
9648
10086
  * Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
9649
10087
  * to decide whether the release it last heard of is newer than the one running.
@@ -9779,15 +10217,18 @@ var PrintedCommand = class {
9779
10217
  #entry;
9780
10218
  /** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
9781
10219
  words;
9782
- /** The line to paste; null when one of the words has no printing that every Windows shell passes on alike. */
10220
+ /** The line to paste; null when no supported common or labelled shell rendering is safe. */
9783
10221
  line;
9784
10222
  platform;
10223
+ shell;
9785
10224
  constructor(gate, words, platform, entry) {
9786
10225
  if (gate !== GATE) throw new TypeError("a printed command is made by locateCliCommand, and only there");
9787
10226
  this.#entry = entry;
9788
10227
  this.words = Object.freeze([...words]);
9789
- this.line = quoteCommand(this.words, platform).line;
10228
+ const quoted = quoteCommand(this.words, platform);
10229
+ this.line = quoted.line;
9790
10230
  this.platform = platform;
10231
+ if (quoted.shell !== void 0) this.shell = quoted.shell;
9791
10232
  Object.freeze(this);
9792
10233
  }
9793
10234
  /**
@@ -9814,7 +10255,7 @@ const STRIP_TYPES = "--experimental-strip-types";
9814
10255
  const TRANSFORM_TYPES = "--experimental-transform-types";
9815
10256
  /** The suite's packages, by name, and its channel packages, which reach core through their own dependency on it. */
9816
10257
  const SUITE_PACKAGES = new Set(CHANNEL_SNAPSHOT.map(({ packageName }) => packageName));
9817
- const CHANNEL_PACKAGES$1 = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
10258
+ const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
9818
10259
  /**
9819
10260
  * Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
9820
10261
  *
@@ -9897,7 +10338,7 @@ function locateCliCommand(request, runtime = {
9897
10338
  sameCheckout: true
9898
10339
  });
9899
10340
  }
9900
- if (target.manifest.channel === "core" && CHANNEL_PACKAGES$1.has(caller.packageName)) {
10341
+ if (target.manifest.channel === "core" && CHANNEL_PACKAGES.has(caller.packageName)) {
9901
10342
  const installed = installedPackage(dirname(callerFile), target.packageName);
9902
10343
  if (installed === null) return fail("core", `${caller.packageName} has no ${target.packageName} installed where it can find one`);
9903
10344
  const manifest = readPackageManifest(installed);
@@ -9925,7 +10366,9 @@ function locateCliCommand(request, runtime = {
9925
10366
  }
9926
10367
  if (!SUITE_PACKAGES.has(caller.packageName)) return fail("direction", `${caller.packageName} is not a package of this suite`);
9927
10368
  if (request.registrations === void 0) return fail("no-registrations", "nothing registered was given to look through for another product's command");
9928
- const facts = registrationFactsOf(target);
10369
+ const channelTarget = CHANNEL_SNAPSHOT.find((entry) => entry.packageName === target.packageName);
10370
+ if (channelTarget === void 0) return fail("direction", `${target.packageName} is not a channel package this locator can find through a registration`);
10371
+ const facts = registrationFactsOf(channelTarget);
9929
10372
  const notUsed = [];
9930
10373
  const usable = [];
9931
10374
  let engineRange;
@@ -10023,8 +10466,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
10023
10466
  let entry;
10024
10467
  if (found.source) entry = join(found.root, "src", "cli.ts");
10025
10468
  else {
10026
- const declared = found.manifest.agentcomms;
10027
- const binary = typeof declared === "object" && declared !== null ? declared.binary : void 0;
10469
+ const binary = declaredBinary(found.manifest);
10028
10470
  const bin = found.manifest.bin;
10029
10471
  const path = binary === target.manifest.binary && typeof bin === "object" && bin !== null && Object.hasOwn(bin, target.manifest.binary) ? bin[target.manifest.binary] : void 0;
10030
10472
  if (typeof path !== "string" || path.length === 0) return {
@@ -10053,6 +10495,16 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
10053
10495
  };
10054
10496
  }
10055
10497
  /**
10498
+ * The command a package declares for itself: a channel's in its `agentcomms` field, a service's in `agentcommsPackage`
10499
+ * (D14). A library declares none, and no package declares both: the registry refuses one that does.
10500
+ */
10501
+ function declaredBinary(manifest) {
10502
+ const channel = manifest.agentcomms;
10503
+ if (typeof channel === "object" && channel !== null) return channel.binary;
10504
+ const service = manifest.agentcommsPackage;
10505
+ if (typeof service === "object" && service !== null && service.kind === "service") return service.binary;
10506
+ }
10507
+ /**
10056
10508
  * The target's package a registration's files lead to, or null: a script it starts, real or as written when it is
10057
10509
  * gone, inside the package (or inside the product's own server wrapper, which depends on it); or, for the product's
10058
10510
  * own command, the package that command links into or that npm installed beside it.
@@ -10184,7 +10636,7 @@ function notLocated(target, version, reason, detail, nodeVersion, extra) {
10184
10636
  * anywhere else for the product, PATH included, and a later locate checks the path again.
10185
10637
  */
10186
10638
  function checkedEntry(root, entry) {
10187
- if (!isWithin(root, entry)) return {
10639
+ if (!isWithin$1(root, entry)) return {
10188
10640
  ok: false,
10189
10641
  why: `${entry} is outside its package, ${root}`
10190
10642
  };
@@ -10197,7 +10649,7 @@ function checkedEntry(root, entry) {
10197
10649
  why: `${entry} is not there`
10198
10650
  };
10199
10651
  }
10200
- if (!isWithin(root, real)) return {
10652
+ if (!isWithin$1(root, real)) return {
10201
10653
  ok: false,
10202
10654
  why: `${entry} leads to ${real}, outside its package, ${root}`
10203
10655
  };
@@ -10611,6 +11063,10 @@ function renderInstall(result, color) {
10611
11063
  lines.push(paint(color, "bold", `Add this to ${result.configPath ?? `the MCP configuration of ${result.client}`}:`), result.snippet.trimEnd());
10612
11064
  }
10613
11065
  if (result.backupPath) lines.push(`The entry it replaced was saved to ${result.backupPath}.`);
11066
+ if (result.shim && result.shim.status !== "foreign") lines.push(`CLI shim ${result.shim.status}: ${result.shim.path}.`);
11067
+ if (result.shimFailure) lines.push(paint(color, "yellow", `CLI shim failed: ${result.shimFailure}.`));
11068
+ if (result.shimNotice) lines.push(paint(color, "yellow", result.shimNotice));
11069
+ if (result.shim?.pathNotice) lines.push(result.shim.pathNotice);
10614
11070
  if (result.verification === "passed") lines.push(paint(color, "green", `Checked: ${result.verifyDetail}`));
10615
11071
  else if (result.verification === "failed") {
10616
11072
  lines.push(paint(color, "red", `Failed to start: ${result.verifyDetail ?? "no reason given"}`));
@@ -10625,6 +11081,8 @@ function renderPrune(result, color) {
10625
11081
  if (result.refused) lines.push(paint(color, "yellow", `Nothing removed: ${result.refused}.`));
10626
11082
  const verb = result.dryRun ? "Would remove" : "Removed";
10627
11083
  for (const item of result.removed) lines.push(paint(color, "green", `${verb} ${item.version}: ${item.path}`));
11084
+ for (const path of result.shims.removed) lines.push(paint(color, "green", `${verb} CLI shim: ${path}`));
11085
+ for (const path of result.shims.kept) lines.push(`Kept foreign CLI shim: ${path}`);
10628
11086
  for (const item of result.kept) lines.push(`Kept ${item.version} (${item.reason}): ${item.path}`);
10629
11087
  if (lines.length === 0) lines.push(`No managed runtimes to remove in ${result.runtimeDir}.`);
10630
11088
  return lines.join("\n");
@@ -10636,7 +11094,7 @@ function describeItem(item) {
10636
11094
  const where = item.scope === "project" ? `for a project, in ${item.path}` : `in ${item.path}`;
10637
11095
  const pins = item.narrowing.length > 0 ? `, ${item.narrowing.join(" ")}` : "";
10638
11096
  const version = item.version === null ? "pins no release" : `${item.version}${item.version === item.latest ? "" : ` → ${item.latest}`}`;
10639
- return `${channelLabel(item.channel)} with ${item.client} as "${item.name}" ${where} (${item.launcher}, ${version}${pins})`;
11097
+ return `${serverLabel(item.channel)} with ${item.client} as "${item.name}" ${where} (${item.launcher}, ${version}${pins})`;
10640
11098
  }
10641
11099
  function renderUpdate(result, color) {
10642
11100
  const lines = [];
@@ -10648,6 +11106,8 @@ function renderUpdate(result, color) {
10648
11106
  if (step.verification === "passed") lines.push(` Checked: ${step.detail}`);
10649
11107
  else if (step.verification === "failed") lines.push(paint(color, "red", ` Failed to start: ${step.detail}`));
10650
11108
  else lines.push(paint(color, "yellow", ` Not checked: ${step.detail ?? "skipped"}`));
11109
+ if (step.shim && step.shim.status !== "foreign") lines.push(` CLI shim ${step.shim.status}: ${step.shim.path}.`);
11110
+ if (step.shim?.pathNotice) lines.push(` ${step.shim.pathNotice}`);
10651
11111
  for (const warning of step.warnings ?? []) lines.push(` ${warning}`);
10652
11112
  } else lines.push(paint(color, "red", `${step.outcome === "skipped" ? "Skipped" : "Could not register"} ${what}: ${step.detail}`));
10653
11113
  } else lines.push(step.outcome === "updated" ? paint(color, "green", `Updated the global ${step.package} from ${step.from} to ${step.to}.`) : paint(color, "red", `Could not update the global ${step.package}: ${step.detail ?? "no reason given"}`));
@@ -12052,10 +12512,10 @@ async function npmInstallGlobal(env, spec) {
12052
12512
  }
12053
12513
  }
12054
12514
  /**
12055
- * Registering, pruning and listing the MCP servers of every channel, from the core.
12515
+ * Registering, pruning and listing every declared channel or service MCP server, from the core.
12056
12516
  *
12057
- * `agentcomms mcp install` registers the core server; `comms_server_install` registers any channel's; and
12058
- * `agent-gmail mcp install` and `agent-slack mcp install` register their own. All four are the one change here, over
12517
+ * `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
12518
+ * server; and channel CLIs register their own. They are the one change here, over
12059
12519
  * the shared installer (`mcp-install.ts`), and all go through the change flow: registering a server is a loosening in
12060
12520
  * the design's terms (§3.1) — it hands a client a new set of tools — and so is asked for once and applied on the
12061
12521
  * second call with the approval. Printing an entry registers nothing and asks nobody.
@@ -12066,7 +12526,7 @@ async function npmInstallGlobal(env, spec) {
12066
12526
  * the other — or worse, a channel's own command would register with no approval at all, which is what it did.
12067
12527
  *
12068
12528
  * The version installed is this core's own, or the calling channel's when it passes its product. The packages are
12069
- * released together, at one version, so the core that is asked to install Gmail installs the Gmail of its own release.
12529
+ * released together, at one version, so the core asked to install Gmail or events installs its own release.
12070
12530
  */
12071
12531
  const CLIENTS = Object.freeze([
12072
12532
  "claude-code",
@@ -12107,10 +12567,10 @@ const exists = (path) => access(path, constants.R_OK).then(() => true, () => fal
12107
12567
  * core depends on none of them. So the channel is looked for beside this core — `packages/gmail` next to
12108
12568
  * `packages/core` in a checkout — and from source when this core runs from source, as the channel CLIs do.
12109
12569
  */
12110
- async function localModuleUrl(channel) {
12570
+ async function localModuleUrl(server) {
12111
12571
  const coreRoot = await corePackageRoot();
12112
- const unscopedName = channelServer(channel).packageName.split("/").at(-1) ?? channel;
12113
- const root = channel === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
12572
+ const unscopedName = serverFacts(server).packageName.split("/").at(-1) ?? server;
12573
+ const root = server === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
12114
12574
  const source = join(root, "src", "cli.ts");
12115
12575
  const built = join(root, "dist", "cli.mjs");
12116
12576
  const fromSource = fileURLToPath(import.meta.url).endsWith(".ts");
@@ -12118,7 +12578,7 @@ async function localModuleUrl(channel) {
12118
12578
  if (!await exists(cli)) continue;
12119
12579
  return pathToFileURL(cli === source ? join(root, "src", "mcp", "module.ts") : join(root, "dist", "module.mjs")).href;
12120
12580
  }
12121
- throw new CommsError("USAGE", `\`--launcher local\` registers a checkout's own code, and there is no ${channelServer(channel).packageName} beside this one`, { hint: `Looked in ${root}. Use the managed launcher, or run the install from that checkout.` });
12581
+ throw new CommsError("USAGE", `\`--launcher local\` registers a checkout's own code, and there is no ${serverFacts(server).packageName} beside this one`, { hint: `Looked in ${root}. Use the managed launcher, or run the install from that checkout.` });
12122
12582
  }
12123
12583
  /**
12124
12584
  * The product the shared installer registers, for one channel: at this core's version unless another is named.
@@ -12128,11 +12588,11 @@ async function localModuleUrl(channel) {
12128
12588
  * newer release here is all it takes to register one this core is older than — which is what `agentcomms update` does.
12129
12589
  * `comms_server_install` and every `mcp install` name none, and register their own release as they always have.
12130
12590
  */
12131
- async function channelProduct(channel, launcher, version = VERSION) {
12591
+ async function serverProduct(server, launcher, version = VERSION) {
12132
12592
  return {
12133
- ...channelServer(channel),
12593
+ ...serverFacts(server),
12134
12594
  version,
12135
- moduleUrl: launcher === "local" ? await localModuleUrl(channel) : ""
12595
+ moduleUrl: launcher === "local" ? await localModuleUrl(server) : ""
12136
12596
  };
12137
12597
  }
12138
12598
  /**
@@ -12145,8 +12605,8 @@ async function channelProduct(channel, launcher, version = VERSION) {
12145
12605
  * registered, from the channel; a product for another package would register something else under those words, and
12146
12606
  * the person would have approved a sentence that was not true.
12147
12607
  */
12148
- function ownProduct(channel, own) {
12149
- if (own !== void 0 && own.packageName !== channelServer(channel).packageName) throw new CommsError("UNEXPECTED", `${own.packageName} is not the ${channelLabel(channel)} server's package, so it cannot register that server`);
12608
+ function ownProduct(server, own) {
12609
+ if (own !== void 0 && own.packageName !== serverFacts(server).packageName) throw new CommsError("UNEXPECTED", `${own.packageName} is not the ${serverLabel(server)} server's package, so it cannot register that server`);
12150
12610
  return own;
12151
12611
  }
12152
12612
  /**
@@ -12156,11 +12616,11 @@ function ownProduct(channel, own) {
12156
12616
  * register a server reaching every workspace for a caller who believed they had pinned it.
12157
12617
  */
12158
12618
  function checkRequest(request) {
12159
- if (!isChannel(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
12619
+ if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
12160
12620
  if (!CLIENTS.includes(request.client)) throw new CommsError("USAGE", `"${String(request.client)}" is not a client this can register with`, { hint: `One of: ${CLIENTS.join(", ")}.` });
12161
12621
  if (request.launcher !== void 0 && !LAUNCHERS.includes(request.launcher)) throw new CommsError("USAGE", `"${String(request.launcher)}" is not a launcher`, { hint: `One of: ${LAUNCHERS.join(", ")}.` });
12162
12622
  if (request.name !== void 0) checkServerName(request.name);
12163
- const label = channelLabel(request.channel);
12623
+ const label = serverLabel(request.channel);
12164
12624
  const refuse = (option) => {
12165
12625
  const owner = narrowingOwner(option);
12166
12626
  throw new CommsError("USAGE", owner === void 0 ? `\`${option}\` is not an option of the ${label} server` : `\`${option}\` is an option of the ${owner.label} server; the ${label} server has no such option`);
@@ -12196,7 +12656,7 @@ function checkPin(config, request) {
12196
12656
  return;
12197
12657
  }
12198
12658
  const { account } = resolveName(config, "account", pin);
12199
- if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${channelLabel(request.channel)} ${accountNoun(request.channel)}`);
12659
+ if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${serverLabel(request.channel)} ${accountNoun(request.channel)}`);
12200
12660
  }
12201
12661
  function installOptions(request) {
12202
12662
  const own = pinOption(request.channel);
@@ -12215,7 +12675,7 @@ function installOptions(request) {
12215
12675
  };
12216
12676
  }
12217
12677
  /**
12218
- * Registering one channel's server with one client, as a change.
12678
+ * Registering one declared server with one client, as a change.
12219
12679
  *
12220
12680
  * The effects are what a person is agreeing to, one sentence each, and everything that decides what the server may
12221
12681
  * reach is in them — which server, which client, under which name, pinned to what, replacing what, started how. The
@@ -12230,21 +12690,22 @@ function installOptions(request) {
12230
12690
  * resolves another — `CLAUDE_CONFIG_DIR` set to another account's, a PATH that finds another node, a core running
12231
12691
  * from another build of the checkout — is another registration, refused as one.
12232
12692
  */
12233
- function serverInstallChange(core, env, request, own) {
12693
+ function serverInstallChange(core, env, request, own, deps = {}) {
12234
12694
  checkRequest(request);
12235
12695
  ownProduct(request.channel, own);
12236
12696
  const context = {
12237
12697
  env,
12238
- core
12698
+ core,
12699
+ ...deps.platform === void 0 ? {} : { platform: deps.platform }
12239
12700
  };
12240
- const facts = channelServer(request.channel);
12241
- const label = channelLabel(request.channel);
12701
+ const facts = serverFacts(request.channel);
12702
+ const label = serverLabel(request.channel);
12242
12703
  const noun = accountNoun(request.channel);
12243
12704
  const pinName = pinOption(request.channel);
12244
12705
  const switchable = hasNarrowing(request.channel, "readOnly");
12245
12706
  const name = request.name ?? facts.defaultServerName;
12246
12707
  const launcher = request.launcher ?? "managed";
12247
- const productNow = async () => own ?? await channelProduct(request.channel, request.launcher);
12708
+ const productNow = async () => own ?? await serverProduct(request.channel, request.launcher);
12248
12709
  let planned;
12249
12710
  return {
12250
12711
  plan: async (config) => {
@@ -12275,6 +12736,14 @@ function serverInstallChange(core, env, request, own) {
12275
12736
  else if (launcher === "local") effects.push(`${request.client} will start it from ${await localCliEntry(product.moduleUrl)}`);
12276
12737
  }
12277
12738
  if (request.print !== true && launcher === "managed" && await reusableRuntime(core.paths.dataDir, facts.packageName, version) === null) effects.push(`installs ${facts.packageName}@${version} from npm into ${managedRuntimeDir(core.paths.dataDir, facts.packageName, version)}`);
12739
+ if (request.print !== true && launcher === "managed" && managedShimsSupported(context)) {
12740
+ const path = managedShimPath({
12741
+ env,
12742
+ dataDir: core.paths.dataDir,
12743
+ platform: context.platform
12744
+ }, facts.binary);
12745
+ effects.push(`writes or repoints the owned CLI shim at ${path}; a foreign file there is left unchanged`);
12746
+ }
12278
12747
  return {
12279
12748
  before: config,
12280
12749
  after: config,
@@ -12284,9 +12753,10 @@ function serverInstallChange(core, env, request, own) {
12284
12753
  },
12285
12754
  apply: async () => {
12286
12755
  if (planned === void 0) throw new CommsError("UNEXPECTED", "the registration was applied before it was planned");
12287
- const result = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
12756
+ const installed = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
12288
12757
  throw await remedyLocated(error, core);
12289
12758
  });
12759
+ const result = request.print === true ? installed : await withManagedShim(context, planned.product, installed);
12290
12760
  return {
12291
12761
  ...result,
12292
12762
  restart: result.applied ? `Restart ${request.client} to load "${result.name}": no MCP client loads a new server into a session that is already running.` : null
@@ -12295,6 +12765,68 @@ function serverInstallChange(core, env, request, own) {
12295
12765
  };
12296
12766
  }
12297
12767
  /**
12768
+ * A managed install's CLI shim (CUE-406), once the registration's outcome is known: the command the locator finds in
12769
+ * the runtime the entry starts, written where nothing is or over this installation's own unchanged shim, and never
12770
+ * over anything else. A shim that cannot be written leaves the registration as it went, and says how to repair both.
12771
+ * The installer may not import the locator, so the operation writes it — for `mcp install` and `update` alike.
12772
+ */
12773
+ async function withManagedShim(context, product, installed) {
12774
+ if (installed.launcher !== "managed") return installed;
12775
+ const cliEntry = installed.entry.args[0];
12776
+ if (cliEntry === void 0) throw new CommsError("UNEXPECTED", "the managed entry has no CLI path");
12777
+ const shimContext = {
12778
+ env: context.env,
12779
+ dataDir: context.core.paths.dataDir,
12780
+ platform: context.platform
12781
+ };
12782
+ if (!managedShimsSupported(shimContext)) return {
12783
+ ...installed,
12784
+ shimNotice: managedShimUnsupportedNotice(shimContext)
12785
+ };
12786
+ const warnings = [...installed.warnings];
12787
+ let shim;
12788
+ let shimFailure;
12789
+ try {
12790
+ const located = locateCliCommand({
12791
+ caller: {
12792
+ url: pathToFileURL(cliEntry).href,
12793
+ packageName: product.packageName
12794
+ },
12795
+ target: {
12796
+ packageName: product.packageName,
12797
+ manifest: {
12798
+ label: product.defaultServerName,
12799
+ binary: product.binary
12800
+ }
12801
+ },
12802
+ words: [],
12803
+ uses: [],
12804
+ paths: {
12805
+ ...context.core.paths,
12806
+ downloadsDir: context.core.paths.dataDir
12807
+ },
12808
+ platform: context.platform
12809
+ });
12810
+ if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
12811
+ shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
12812
+ binary: product.binary,
12813
+ command: located.command.words,
12814
+ entry: located.command.entry
12815
+ });
12816
+ } catch (error) {
12817
+ shimFailure = error instanceof Error ? error.message : String(error);
12818
+ const path = join(homeDirectory(context.env), ".local", "bin", product.binary);
12819
+ warnings.push(`The MCP registration ${installed.applied ? "succeeded" : "was not changed"}, but the CLI shim at ${path} could not be written (${shimFailure}). Fix that shim directory, then run this managed install again with \`--force\`; the owned registration is replaced without widening it and the shim is repaired.`);
12820
+ }
12821
+ if (shim?.status === "foreign") warnings.push(`${shim.path} already exists but was not written by this installation, so it was left unchanged; move it aside and run this managed install again with \`--force\` to create the CLI shim at that path.`);
12822
+ return {
12823
+ ...installed,
12824
+ warnings,
12825
+ ...shim === void 0 ? {} : { shim },
12826
+ ...shimFailure === void 0 ? {} : { shimFailure }
12827
+ };
12828
+ }
12829
+ /**
12298
12830
  * A refusal of `mcp install` with its remedy — another install of the same product — said with the command located:
12299
12831
  * the printing package's own, core's, or another product's found among this machine's registrations (CUE-403). The
12300
12832
  * installer says only the words; it may not import the locator.
@@ -12302,17 +12834,24 @@ function serverInstallChange(core, env, request, own) {
12302
12834
  async function remedyLocated(error, core) {
12303
12835
  const remedy = installRemedyOf(error);
12304
12836
  if (remedy === void 0 || !(error instanceof CommsError)) return error;
12305
- const channel = CHANNELS.find((each) => channelServer(each).binary === remedy.binary);
12306
- if (channel === void 0) return error;
12837
+ const server = SERVERS.find((each) => serverFacts(each).binary === remedy.binary);
12838
+ if (server === void 0) return error;
12307
12839
  const maker = await requireHandoffs(core).registered();
12840
+ const handoff = isChannel(server) ? maker.of(server, remedy.words) : maker.core([
12841
+ "mcp",
12842
+ "install",
12843
+ "--server",
12844
+ server,
12845
+ ...remedy.words.slice(2)
12846
+ ]);
12308
12847
  return new CommsError(error.code, error.message, {
12309
- hint: handoffSentence(maker.of(channel, remedy.words), remedy.say),
12848
+ hint: handoffSentence(handoff, remedy.say),
12310
12849
  ...error.details === void 0 ? {} : { details: error.details },
12311
12850
  ...error.cause === void 0 ? {} : { cause: error.cause }
12312
12851
  });
12313
12852
  }
12314
12853
  /**
12315
- * Removing a channel's unused managed runtimes, as a change.
12854
+ * Removing a declared server's unused managed runtimes, as a change.
12316
12855
  *
12317
12856
  * A dry run is free: it removes nothing. Otherwise the plan is that same dry run, and each runtime it would remove is
12318
12857
  * one effect — the removal cannot be taken back — so the approval is bound to exactly that list; and the removal is
@@ -12322,20 +12861,20 @@ async function remedyLocated(error, core) {
12322
12861
  * release of the command that was run; checked to be that channel's, as for an install.
12323
12862
  */
12324
12863
  function serverPruneChange(core, env, request, own) {
12325
- if (!isChannel(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
12864
+ if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
12326
12865
  const context = {
12327
12866
  env,
12328
12867
  core
12329
12868
  };
12330
12869
  const { packageName, version } = ownProduct(request.channel, own) ?? {
12331
- packageName: channelServer(request.channel).packageName,
12332
- version: "0.16.0"
12870
+ packageName: serverFacts(request.channel).packageName,
12871
+ version: "0.18.0"
12333
12872
  };
12334
12873
  const product = {
12335
12874
  packageName,
12336
12875
  version
12337
12876
  };
12338
- const label = channelLabel(request.channel);
12877
+ const label = serverLabel(request.channel);
12339
12878
  const base = {
12340
12879
  includePrinted: request.includePrinted === true,
12341
12880
  ...request.processes ? { processes: request.processes } : {}
@@ -12356,7 +12895,7 @@ function serverPruneChange(core, env, request, own) {
12356
12895
  return {
12357
12896
  before: config,
12358
12897
  after: config,
12359
- effects: preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`),
12898
+ effects: [...preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`), ...preview.shims.removed.map((path) => `deletes the owned ${label} CLI shim at ${path}`)],
12360
12899
  summary: `Remove ${approved.length} unused ${label} runtime${approved.length === 1 ? "" : "s"}`
12361
12900
  };
12362
12901
  },
@@ -12635,21 +13174,22 @@ function updateStopMessage(pending, where, commands) {
12635
13174
  * the yes is a different change, and the claim is refused. Nothing behind, nothing is prepared.
12636
13175
  */
12637
13176
  /**
12638
- * The packages this suite publishes, whose latest releases an update reads: every channel's, from the manifests, and
12639
- * every package a channel's server is run through by `npx` when that is another one — Gmail's `gmail-mcp`.
13177
+ * The packages this suite publishes, whose latest releases an update reads: every declared server owner's, from the
13178
+ * channel and service declarations, and every package a server is run through by `npx` when that is another one —
13179
+ * Gmail's `gmail-mcp`.
12640
13180
  */
12641
- const CHANNEL_PACKAGES = Object.freeze(CHANNELS.map((channel) => channelServer(channel).packageName));
12642
- const SERVER_PACKAGES = Object.freeze(CHANNELS.map((channel) => channelServer(channel).npxPackage).filter((name) => !CHANNEL_PACKAGES.includes(name)));
12643
- const PUBLISHED_NAMES = Object.freeze([.../* @__PURE__ */ new Set([...CHANNEL_PACKAGES, ...SERVER_PACKAGES])].sort());
12644
- function narrowingArgs(channel, narrowing) {
12645
- return channelServer(channel).serverArgs({
13181
+ const OWNER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).packageName));
13182
+ const SERVER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).npxPackage).filter((name) => !OWNER_PACKAGES.includes(name)));
13183
+ const PUBLISHED_NAMES = Object.freeze([.../* @__PURE__ */ new Set([...OWNER_PACKAGES, ...SERVER_PACKAGES])].sort());
13184
+ function narrowingArgs(server, narrowing) {
13185
+ return serverFacts(server).serverArgs({
12646
13186
  client: "json",
12647
13187
  ...narrowing
12648
13188
  });
12649
13189
  }
12650
- /** Every registration of every channel's server, as the core's `channels` finds them — nothing an `env` holds. */
13190
+ /** Every registration of every declared server, as the core's `channels` finds them — nothing an `env` holds. */
12651
13191
  function registrations(servers) {
12652
- return CHANNELS.flatMap((channel) => servers.filter((server) => isProductServer(server, channelServer(channel))).map((server) => ({
13192
+ return SERVERS.flatMap((channel) => servers.filter((server) => isProductServer(server, serverFacts(channel))).map((server) => ({
12653
13193
  channel,
12654
13194
  server
12655
13195
  })));
@@ -12687,7 +13227,7 @@ async function latestReleases(names, latestVersion) {
12687
13227
  * have no line to paste: the reasons below give it with `handoffSentence`, which shows it as words then.
12688
13228
  */
12689
13229
  function installHandoff(item, handoffs) {
12690
- const facts = channelServer(item.channel);
13230
+ const facts = serverFacts(item.channel);
12691
13231
  const words = [
12692
13232
  "mcp",
12693
13233
  "install",
@@ -12696,7 +13236,13 @@ function installHandoff(item, handoffs) {
12696
13236
  ];
12697
13237
  if (item.name !== facts.defaultServerName) words.push("--name", item.name);
12698
13238
  words.push(...item.narrowing, "--force");
12699
- return handoffs.of(item.channel, words);
13239
+ return isChannel(item.channel) ? handoffs.of(item.channel, words) : handoffs.core([
13240
+ "mcp",
13241
+ "install",
13242
+ "--server",
13243
+ item.channel,
13244
+ ...words.slice(2)
13245
+ ]);
12700
13246
  }
12701
13247
  /**
12702
13248
  * Why a registration that is behind cannot be registered again from here, or undefined when it can.
@@ -12736,8 +13282,8 @@ function newest(versions) {
12736
13282
  */
12737
13283
  async function runtimesNeeded(dataDir, latest, managed) {
12738
13284
  const needed = [];
12739
- for (const channel of CHANNELS) {
12740
- const packageName = channelServer(channel).packageName;
13285
+ for (const channel of SERVERS) {
13286
+ const packageName = serverFacts(channel).packageName;
12741
13287
  const version = latest[packageName];
12742
13288
  if (version === void 0) continue;
12743
13289
  if (!managed.some((item) => item.channel === channel && item.launcher === "managed")) continue;
@@ -12764,7 +13310,7 @@ async function inspect(core, env, deps) {
12764
13310
  const handoffs = requireHandoffs(core, deps.platform).withRegistrations(scan.servers);
12765
13311
  const unreadable = [...scan.unreadable];
12766
13312
  const found = registrations(scan.servers).map(({ channel, server }) => {
12767
- const facts = channelServer(channel);
13313
+ const facts = serverFacts(channel);
12768
13314
  return {
12769
13315
  channel,
12770
13316
  server,
@@ -12784,9 +13330,9 @@ async function inspect(core, env, deps) {
12784
13330
  reason: error instanceof Error ? error.message : String(error)
12785
13331
  });
12786
13332
  }
12787
- const wanted = /* @__PURE__ */ new Set([channelServer("core").packageName]);
13333
+ const wanted = /* @__PURE__ */ new Set([serverFacts("core").packageName]);
12788
13334
  for (const entry of found) {
12789
- wanted.add(channelServer(entry.channel).packageName);
13335
+ wanted.add(serverFacts(entry.channel).packageName);
12790
13336
  wanted.add(entry.package);
12791
13337
  }
12792
13338
  for (const name of Object.keys(installed)) wanted.add(name);
@@ -12809,7 +13355,7 @@ async function inspect(core, env, deps) {
12809
13355
  name: entry.server.name,
12810
13356
  scope: entry.server.scope ?? "user",
12811
13357
  path: entry.server.path,
12812
- launcher: launcherOf(entry.server, channelServer(entry.channel)),
13358
+ launcher: launcherOf(entry.server, serverFacts(entry.channel)),
12813
13359
  version: entry.version,
12814
13360
  latest: latest[entry.package] ?? "",
12815
13361
  narrowing: narrowingArgs(entry.channel, entry.narrowing)
@@ -12840,8 +13386,8 @@ async function inspect(core, env, deps) {
12840
13386
  }
12841
13387
  const needed = await runtimesNeeded(core.paths.dataDir, latest, candidates.map((candidate) => candidate.item));
12842
13388
  behind.push(...needed);
12843
- for (const channel of CHANNELS) {
12844
- const packageName = channelServer(channel).packageName;
13389
+ for (const channel of SERVERS) {
13390
+ const packageName = serverFacts(channel).packageName;
12845
13391
  const version = latest[packageName];
12846
13392
  if (version === void 0 || await reusableRuntime(core.paths.dataDir, packageName, version) === null) continue;
12847
13393
  upToDate.push({
@@ -12899,7 +13445,7 @@ async function updateCheck(core, env, deps = {}, claim) {
12899
13445
  await recordFound(core, env, report, deps, null, claim);
12900
13446
  return report;
12901
13447
  }
12902
- const CORE_PACKAGE$1 = channelServer("core").packageName;
13448
+ const CORE_PACKAGE$1 = serverFacts("core").packageName;
12903
13449
  /**
12904
13450
  * One registration: its channel, its client, its scope, the file it is in and its name. Two copies of it in one file —
12905
13451
  * `servers` and an old `mcpServers` — are one registration, and an update registers them again as one.
@@ -12978,7 +13524,7 @@ function updateCheckFindings(report, latest, after = null) {
12978
13524
  });
12979
13525
  const globals = items.filter((item) => item.kind === "global");
12980
13526
  const global = CHANNELS.filter((channel) => {
12981
- const name = channelServer(channel).packageName;
13527
+ const name = serverFacts(channel).packageName;
12982
13528
  return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.version));
12983
13529
  });
12984
13530
  const stillBehind = after === null ? report.behind.length > 0 : !(after.ok && after.manual.length === 0 && after.status !== "manual");
@@ -13009,13 +13555,11 @@ function reachOf(channel, narrowing) {
13009
13555
  */
13010
13556
  function registrationEffects(step) {
13011
13557
  const { item } = step;
13012
- const reach = reachOf(item.channel, channelServer(item.channel).narrowingOf(item.narrowing));
13013
- const entry = `registers the ${channelLabel(item.channel)} MCP server with ${item.client} as "${item.name}" again (${item.scope} scope, ${item.launcher} launcher), at ${item.latest} in place of ${item.version}${reach ? ` — ${reach}, as now` : ""}`;
13014
- return item.launcher === "npx" ? [
13015
- entry,
13016
- step.destination,
13017
- `${item.client} will fetch ${item.package}@${item.latest} from npm each time it starts "${item.name}"`
13018
- ] : [entry, step.destination];
13558
+ const reach = reachOf(item.channel, serverFacts(item.channel).narrowingOf(item.narrowing));
13559
+ const effects = [`registers the ${serverLabel(item.channel)} MCP server with ${item.client} as "${item.name}" again (${item.scope} scope, ${item.launcher} launcher), at ${item.latest} in place of ${item.version}${reach ? ` — ${reach}, as now` : ""}`, step.destination];
13560
+ if (step.shim) effects.push(`writes or repoints the owned CLI shim at ${step.shim}; a foreign file there is left unchanged`);
13561
+ if (item.launcher === "npx") effects.push(`${item.client} will fetch ${item.package}@${item.latest} from npm each time it starts "${item.name}"`);
13562
+ return effects;
13019
13563
  }
13020
13564
  /** Every step as the sentences a person reads, in the order the steps are taken. */
13021
13565
  function effectsOf(planned) {
@@ -13051,7 +13595,7 @@ async function planRegistrations(context, candidates, request, handoffs) {
13051
13595
  if (seen.has(key)) continue;
13052
13596
  seen.add(key);
13053
13597
  const launcher = item.launcher;
13054
- const product = await channelProduct(item.channel, launcher, item.latest);
13598
+ const product = await serverProduct(item.channel, launcher, item.latest);
13055
13599
  const options = {
13056
13600
  client: item.client,
13057
13601
  name: item.name,
@@ -13077,7 +13621,12 @@ async function planRegistrations(context, candidates, request, handoffs) {
13077
13621
  product,
13078
13622
  options,
13079
13623
  install: plannedInstall(preflight),
13080
- destination: entryDestination(item.client, preflight.target, preflight.command, context.env)
13624
+ destination: entryDestination(item.client, preflight.target, preflight.command, context.env),
13625
+ ...launcher === "managed" && managedShimsSupported(context) ? { shim: managedShimPath({
13626
+ env: context.env,
13627
+ dataDir: context.core.paths.dataDir,
13628
+ platform: context.platform
13629
+ }, product.binary) } : {}
13081
13630
  });
13082
13631
  }
13083
13632
  return {
@@ -13199,18 +13748,23 @@ function whatBecameOf(stored) {
13199
13748
  revoked: "it had been revoked"
13200
13749
  }[record.state] ?? `it is ${JSON.stringify(record.state)}, a state this version does not know`;
13201
13750
  }
13202
- /** The product a runtime of `packageName` is installed as: the channel whose package it is. */
13751
+ /** The product a runtime of `packageName` is installed as: the declared server whose package it is. */
13203
13752
  function runtimeProduct(packageName, version) {
13204
- const channel = CHANNELS.find((each) => channelServer(each).packageName === packageName);
13205
- if (channel === void 0) throw new CommsError("UNEXPECTED", `${packageName} is not a channel's package`);
13206
- return channelProduct(channel, "managed", version);
13753
+ const channel = SERVERS.find((each) => serverFacts(each).packageName === packageName);
13754
+ if (channel === void 0) throw new CommsError("UNEXPECTED", `${packageName} is not a declared server package`);
13755
+ return serverProduct(channel, "managed", version);
13207
13756
  }
13208
13757
  /**
13209
- * Every channel's `mcp prune`, as a person runs it at a terminal: each located among this machine's registrations, and
13210
- * for one that is not, why — never a bare name (CUE-403).
13758
+ * Every declared server's `mcp prune`, as a person runs it at a terminal: each channel command is located among this
13759
+ * machine's registrations, and each service uses core — never a bare name (CUE-403).
13211
13760
  */
13212
13761
  function pruneCommands(handoffs) {
13213
- return handoffChoices(CHANNELS.map((channel) => handoffs.of(channel, ["mcp", "prune"])), "none of their commands is locatable here:", { conjunction: "and" });
13762
+ return handoffChoices(SERVERS.map((channel) => isChannel(channel) ? handoffs.of(channel, ["mcp", "prune"]) : handoffs.core([
13763
+ "mcp",
13764
+ "prune",
13765
+ "--server",
13766
+ channel
13767
+ ])), "none of their commands is locatable here:", { conjunction: "and" });
13214
13768
  }
13215
13769
  function message(error) {
13216
13770
  return error instanceof CommsError ? error.message : error instanceof Error ? error.message : String(error);
@@ -13267,26 +13821,28 @@ async function applyUpdate(context, planned, deps) {
13267
13821
  continue;
13268
13822
  }
13269
13823
  try {
13270
- const pins = channelServer(item.channel).narrowingOf(item.narrowing);
13271
- const result = await mcpInstall(context, product, {
13824
+ const pins = serverFacts(item.channel).narrowingOf(item.narrowing);
13825
+ const installed = await mcpInstall(context, product, {
13272
13826
  ...options,
13273
13827
  ...pins
13274
13828
  }, install);
13275
- if (!result.applied) {
13829
+ if (!installed.applied) {
13276
13830
  steps.push({
13277
13831
  ...base,
13278
13832
  outcome: "failed",
13279
- detail: result.notApplied ?? "nothing was registered"
13833
+ detail: installed.notApplied ?? "nothing was registered"
13280
13834
  });
13281
13835
  continue;
13282
13836
  }
13837
+ const result = await withManagedShim(context, product, installed);
13283
13838
  steps.push({
13284
13839
  ...base,
13285
13840
  outcome: "registered",
13286
13841
  verification: result.verification,
13287
13842
  ...result.verifyDetail !== void 0 ? { detail: result.verifyDetail } : {},
13288
13843
  ...result.backupPath !== void 0 ? { backupPath: result.backupPath } : {},
13289
- ...result.warnings.length > 0 ? { warnings: result.warnings } : {}
13844
+ ...result.warnings.length > 0 ? { warnings: result.warnings } : {},
13845
+ ...result.shim !== void 0 ? { shim: result.shim } : {}
13290
13846
  });
13291
13847
  } catch (error) {
13292
13848
  steps.push({
@@ -14046,7 +14602,7 @@ function terminalUpdateHooks(core, env, options) {
14046
14602
  };
14047
14603
  }
14048
14604
  //#endregion
14049
- //#region ../core/dist/tool-arguments-DA5ZqgoB.mjs
14605
+ //#region ../core/dist/tool-arguments-C_RA7n3A.mjs
14050
14606
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
14051
14607
  const TARGET = "draft-2020-12";
14052
14608
  /**
@@ -14351,7 +14907,7 @@ function spoken(words, joiner) {
14351
14907
  return `${words.slice(0, -1).join(", ")} ${joiner} ${words.at(-1)}`;
14352
14908
  }
14353
14909
  //#endregion
14354
- //#region ../../node_modules/.pnpm/postal-mime@3.0.0/node_modules/postal-mime/src/decode-strings.js
14910
+ //#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/decode-strings.js
14355
14911
  const textEncoder = new TextEncoder();
14356
14912
  const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
14357
14913
  const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
@@ -14406,26 +14962,143 @@ const codePagePattern = /^(?:cp|windows|ms|ibm)(\d+)$/;
14406
14962
  function normalizeCharset(charset) {
14407
14963
  return charset.replace(/^(?:x-ms-|x-|cs)/, "").replace(/[\s._-]+/g, "");
14408
14964
  }
14965
+ const ENCODING_LABELS = new Set([
14966
+ "unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8",
14967
+ "866 cp866 csibm866 ibm866",
14968
+ "csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2",
14969
+ "csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3",
14970
+ "csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4",
14971
+ "csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988",
14972
+ "arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e",
14973
+ "iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987",
14974
+ "csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7",
14975
+ "iso_8859-7:1987 sun_eu_greek",
14976
+ "csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8",
14977
+ "iso_8859-8:1988 visual",
14978
+ "csiso88598i iso-8859-8-i logical",
14979
+ "csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6",
14980
+ "iso-8859-13 iso8859-13 iso885913",
14981
+ "iso-8859-14 iso8859-14 iso885914",
14982
+ "csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9",
14983
+ "iso-8859-16",
14984
+ "cskoi8r koi koi8 koi8-r koi8_r",
14985
+ "koi8-ru koi8-u",
14986
+ "csmacintosh mac macintosh x-mac-roman",
14987
+ "dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874",
14988
+ "cp1250 windows-1250 x-cp1250",
14989
+ "cp1251 windows-1251 x-cp1251",
14990
+ "ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591",
14991
+ "iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252",
14992
+ "cp1253 windows-1253 x-cp1253",
14993
+ "cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5",
14994
+ "windows-1254 x-cp1254",
14995
+ "cp1255 windows-1255 x-cp1255",
14996
+ "cp1256 windows-1256 x-cp1256",
14997
+ "cp1257 windows-1257 x-cp1257",
14998
+ "cp1258 windows-1258 x-cp1258",
14999
+ "x-mac-cyrillic x-mac-ukrainian",
15000
+ "chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk",
15001
+ "gb18030",
15002
+ "big5 big5-hkscs cn-big5 csbig5 x-x-big5",
15003
+ "cseucpkdfmtjapanese euc-jp x-euc-jp",
15004
+ "csiso2022jp iso-2022-jp",
15005
+ "csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis",
15006
+ "cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
15007
+ "windows-949",
15008
+ "csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
15009
+ "unicodefffe utf-16be",
15010
+ "csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
15011
+ "x-user-defined"
15012
+ ].join(" ").split(" "));
15013
+ const decoders = /* @__PURE__ */ new Map();
14409
15014
  function tryDecoder(charset) {
14410
- try {
14411
- return new TextDecoder(charset);
14412
- } catch (err) {
14413
- return null;
15015
+ if (!ENCODING_LABELS.has(charset)) return null;
15016
+ let decoder = decoders.get(charset);
15017
+ if (decoder === void 0) {
15018
+ decoder = null;
15019
+ try {
15020
+ decoder = new TextDecoder(charset);
15021
+ } catch {}
15022
+ decoders.set(charset, decoder);
14414
15023
  }
15024
+ return decoder;
14415
15025
  }
14416
- function getDecoder(charset) {
15026
+ const utf8Decoder = new TextDecoder();
15027
+ /**
15028
+ * Finds the decoder for a charset label, or null when the runtime has no encoding for it
15029
+ */
15030
+ function findDecoder(charset) {
14417
15031
  charset = (charset || "utf8").trim().toLowerCase();
14418
15032
  const decoder = tryDecoder(charset);
14419
15033
  if (decoder) return decoder;
14420
15034
  const normalized = normalizeCharset(charset);
14421
15035
  const codePage = normalized.match(codePagePattern);
14422
- return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized) || new TextDecoder("windows-1252");
15036
+ return tryDecoder(codePage && codePageAliases.get(codePage[1]) || charsetAliases.get(normalized) || normalized);
15037
+ }
15038
+ function getDecoder(charset) {
15039
+ return findDecoder(charset) || tryDecoder("windows-1252") || utf8Decoder;
15040
+ }
15041
+ /**
15042
+ * Whether the bytes are well formed UTF-8. A scan rather than a fatal TextDecoder, since
15043
+ * the exception one throws costs more than the decode itself and a message decides how
15044
+ * many lines throw it.
15045
+ */
15046
+ function isUtf8(bytes) {
15047
+ for (let i = 0; i < bytes.length;) {
15048
+ const lead = bytes[i];
15049
+ if (lead < 128) {
15050
+ i++;
15051
+ continue;
15052
+ }
15053
+ let count;
15054
+ if (lead >= 194 && lead <= 223) count = 1;
15055
+ else if (lead >= 224 && lead <= 239) count = 2;
15056
+ else if (lead >= 240 && lead <= 244) count = 3;
15057
+ else return false;
15058
+ if (i + count >= bytes.length) return false;
15059
+ const second = bytes[i + 1];
15060
+ if (second < (lead === 224 ? 160 : lead === 240 ? 144 : 128) || second > (lead === 237 ? 159 : lead === 244 ? 143 : 191)) return false;
15061
+ for (let k = 2; k <= count; k++) if ((bytes[i + k] & 192) !== 128) return false;
15062
+ i += count + 1;
15063
+ }
15064
+ return true;
15065
+ }
15066
+ /**
15067
+ * Decodes bytes whose encoding is not known: as UTF-8 when they are well formed UTF-8, as
15068
+ * windows-1252 otherwise.
15069
+ *
15070
+ * A message that declares no charset, or one the runtime has never heard of, usually
15071
+ * predates RFC 6532 or comes from a generator that never heard of it either, and its 8-bit
15072
+ * bytes are a legacy single byte encoding far more often than broken UTF-8. windows-1252
15073
+ * is the fallback the WHATWG Encoding Standard prescribes and what a mail client's default
15074
+ * charset amounts to, and it reads such text rather than filling it with replacement
15075
+ * characters.
15076
+ *
15077
+ * @param bytes Bytes to decode
15078
+ * @param utf8 The UTF-8 decoder to use, header lines bring one that keeps a BOM
15079
+ * @return Decoded text
15080
+ */
15081
+ function decodeUtf8OrWindows1252(bytes, utf8 = utf8Decoder) {
15082
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
15083
+ return isUtf8(view) ? utf8.decode(view) : getDecoder("windows-1252").decode(view);
15084
+ }
15085
+ /**
15086
+ * Decodes bytes in a named charset. A label the runtime does not know is no charset at
15087
+ * all, see decodeUtf8OrWindows1252.
15088
+ *
15089
+ * @param bytes Bytes to decode
15090
+ * @param charset Charset label
15091
+ * @return Decoded text
15092
+ */
15093
+ function decodeCharset(bytes, charset) {
15094
+ const decoder = findDecoder(charset);
15095
+ return decoder ? decoder.decode(bytes) : decodeUtf8OrWindows1252(bytes);
14423
15096
  }
14424
15097
  /**
14425
15098
  * Numeric value of an ASCII hex digit
14426
15099
  *
14427
- * @param {Number} c Byte to read
14428
- * @return {Number} Value 0-15, or -1 if the byte is not a hex digit
15100
+ * @param c Byte to read
15101
+ * @return Value 0-15, or -1 if the byte is not a hex digit
14429
15102
  */
14430
15103
  function hexNibble(c) {
14431
15104
  if (c >= 48 && c <= 57) return c - 48;
@@ -14434,14 +15107,35 @@ function hexNibble(c) {
14434
15107
  return -1;
14435
15108
  }
14436
15109
  /**
15110
+ * Folds the line breaks of a header value into spaces.
15111
+ *
15112
+ * A header value holds no line breaks. The raw value is folded when the headers are
15113
+ * processed, see processHeaders in mime-node.ts, an RFC 2047 encoded word is folded as it
15114
+ * is decoded, and an RFC 2231 filename is folded where the attachment is built. Otherwise a
15115
+ * decoded name, subject or filename written into a header of an outgoing message by the
15116
+ * consumer ends the header early and starts another one with the rest of the value.
15117
+ *
15118
+ * The RFC 2231 percent decoder itself does not fold: it also produces the boundary, and a
15119
+ * boundary holding a line break can never match a line. That has to stay so, or a message
15120
+ * that has no parts for every other parser would have them here.
15121
+ *
15122
+ * @param str Header value
15123
+ * @return The value with every run of CR and LF replaced by a space
15124
+ */
15125
+ function foldLineBreaks(str) {
15126
+ return str.replace(/[\r\n]+/g, " ");
15127
+ }
15128
+ /**
14437
15129
  * Decode a complete mime word encoded string
14438
15130
  *
14439
- * @param {String} str Mime word encoded string
14440
- * @return {String} Decoded unicode string
15131
+ * @param charset Character set of the encoded word
15132
+ * @param encoding `Q` or `B`
15133
+ * @param str Mime word encoded string
15134
+ * @return Decoded unicode string
14441
15135
  */
14442
15136
  function decodeWord(charset, encoding, str) {
14443
15137
  let splitPos = charset.indexOf("*");
14444
- if (splitPos >= 0) charset = charset.substr(0, splitPos);
15138
+ if (splitPos >= 0) charset = charset.slice(0, splitPos);
14445
15139
  encoding = encoding.toUpperCase();
14446
15140
  let byteStr;
14447
15141
  if (encoding === "Q") {
@@ -14466,7 +15160,7 @@ function decodeWord(charset, encoding, str) {
14466
15160
  for (let i = 0, len = encodedBytes.length; i < len; i++) dataView.setUint8(i, encodedBytes[i]);
14467
15161
  } else if (encoding === "B") byteStr = decodeBase64$1(str.replace(/[^a-zA-Z0-9\+\/=]+/g, ""));
14468
15162
  else byteStr = textEncoder.encode(str);
14469
- return getDecoder(charset).decode(byteStr);
15163
+ return foldLineBreaks(decodeCharset(byteStr, charset));
14470
15164
  }
14471
15165
  const ENCODED_WORD_PATTERN = "=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=";
14472
15166
  const ENCODED_WORD_REGEX = new RegExp(ENCODED_WORD_PATTERN, "g");
@@ -14476,8 +15170,8 @@ const ENCODED_WORDS_ONLY_REGEX = new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`
14476
15170
  * Checks whether a string is nothing but RFC 2047 encoded words. Kept next to the grammar
14477
15171
  * it depends on, so the pattern has a single definition.
14478
15172
  *
14479
- * @param {String} str String to check
14480
- * @return {Boolean} true if the string holds encoded words and nothing else
15173
+ * @param str String to check
15174
+ * @return true if the string holds encoded words and nothing else
14481
15175
  */
14482
15176
  function isEncodedWordsOnly(str) {
14483
15177
  return ENCODED_WORDS_ONLY_REGEX.test(str);
@@ -14489,8 +15183,8 @@ function isEncodedWordsOnly(str) {
14489
15183
  * means the input can not contain the marker, which previously let a sender delete text
14490
15184
  * from a subject or a display name by writing the marker into the header themselves.
14491
15185
  *
14492
- * @param {String} str String to split
14493
- * @return {Array} Array of `{text}` and `{charset, encoding, encodedText}` tokens
15186
+ * @param str String to split
15187
+ * @return Array of `{text}` and `{charset, encoding, encodedText}` tokens
14494
15188
  */
14495
15189
  function splitEncodedWords(str) {
14496
15190
  const tokens = [];
@@ -14524,9 +15218,9 @@ function canJoinWords(left, right) {
14524
15218
  /**
14525
15219
  * Decodes a token list into a string, optionally merging adjacent encoded words.
14526
15220
  *
14527
- * @param {Array} tokens Tokens from splitEncodedWords
14528
- * @param {Boolean} joinWords Whether adjacent encoded words may be decoded as one unit
14529
- * @return {String} Decoded string
15221
+ * @param tokens Tokens from splitEncodedWords
15222
+ * @param joinWords Whether adjacent encoded words may be decoded as one unit
15223
+ * @return Decoded string
14530
15224
  */
14531
15225
  function renderTokens(tokens, joinWords) {
14532
15226
  let result = "";
@@ -14557,6 +15251,12 @@ function renderTokens(tokens, joinWords) {
14557
15251
  if (pending) result += decodeWord(pending.charset, pending.encoding, pending.encodedText);
14558
15252
  return result;
14559
15253
  }
15254
+ /**
15255
+ * Decodes RFC 2047 encoded words in a string
15256
+ *
15257
+ * @param str String that may contain encoded words
15258
+ * @return Unicode string with every encoded word decoded
15259
+ */
14560
15260
  function decodeWords(str) {
14561
15261
  const tokens = splitEncodedWords((str || "").toString());
14562
15262
  const result = renderTokens(tokens, true);
@@ -14564,55 +15264,279 @@ function decodeWords(str) {
14564
15264
  }
14565
15265
  new TextDecoder("utf-8", { ignoreBOM: true });
14566
15266
  //#endregion
14567
- //#region ../../node_modules/.pnpm/postal-mime@3.0.0/node_modules/postal-mime/src/address-parser.js
14568
- const WORD_CHAR_REGEX = /\w/;
14569
- const NON_SPACE_TOKEN_REGEX = /[^\s]+/g;
15267
+ //#region ../../node_modules/.pnpm/postal-mime@4.0.5/node_modules/postal-mime/dist/esm/address-parser.js
15268
+ /**
15269
+ * Any whitespace, the test for an angle-addr that needs recovering
15270
+ */
15271
+ const HAS_WHITESPACE = /\s/;
14570
15272
  /**
14571
- * Finds the first address looking token in a run of text.
15273
+ * An addr-spec that carries its whitespace legally, inside a quoted local part. The
15274
+ * optional tail is the malformed shape: a real mailbox with wreckage trailing it.
15275
+ */
15276
+ const QUOTED_LOCAL_ADDR = /^("(?:[^"\\]|\\[\s\S])*"@\S+)(?:\s+([\s\S]+))?$/;
15277
+ /**
15278
+ * One run holding a single '@' and no whitespace, the shape an addr-spec has to have
15279
+ */
15280
+ const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
15281
+ /**
15282
+ * The looser reading applied once the strict one finds nothing, which tolerates the
15283
+ * further '@' that a domain should not have but malformed headers carry anyway
15284
+ */
15285
+ const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
15286
+ /**
15287
+ * An addr-spec sitting inside free text, together with the whitespace around it. Sticky
15288
+ * on purpose: it is only run at the offset looseAddressStart picks, see there.
15289
+ */
15290
+ const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
15291
+ /**
15292
+ * Restores the quoting of a local part that was read out of a quoted string.
14572
15293
  *
14573
- * This replaces a `\s*\b[^@\s]+@[^\s]+\b\s*` scan over the whole string, which backtracks
14574
- * quadratically: the leading `\s*` makes every position inside a whitespace run a viable
14575
- * start, and `[^@\s]+` then gives back one character at a time looking for an '@'. A
14576
- * single header well inside the default size limit could hold a core busy for minutes.
15294
+ * RFC 5321 allows '@' inside a quoted local part, so handing '"user@evil.com"@good.com'
15295
+ * on as the bare 'user@evil.com@good.com' leaves it to the consumer which '@' splits the
15296
+ * domain off. Getting that wrong is a misrouting vector, so the quotes go back on. The
15297
+ * same holds for the other specials: a ',' or a ';' that loses its quotes reads as a
15298
+ * recipient separator once the consumer puts the address back into a header.
15299
+ *
15300
+ * @param address Address with an unquoted local part
15301
+ * @return Address with the local part as a quoted-string
15302
+ */
15303
+ function quoteLocalPart(address) {
15304
+ const lastAt = address.lastIndexOf("@");
15305
+ if (lastAt < 0) return address;
15306
+ const user = address.substring(0, lastAt);
15307
+ if (/^[^\s"(),:;<>@[\\\]]+$/.test(user) || /^"(?:[^"\\]|\\[\s\S])*"$/.test(user)) return address;
15308
+ return "\"" + user.replace(/["\\]/g, "\\$&") + "\"@" + address.substring(lastAt + 1);
15309
+ }
15310
+ /**
15311
+ * The characters JS `\s` matches, which the scan below has to agree with to land on the
15312
+ * same match LOOSE_TEXT_ADDR would.
15313
+ */
15314
+ function isSpaceCode(code) {
15315
+ return code === 32 || code >= 9 && code <= 13 || code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
15316
+ }
15317
+ /**
15318
+ * The characters JS `\w` matches without the unicode flag, the set the `\b` in
15319
+ * LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
15320
+ * which compares false throughout, so out of range reads as a non-word character.
15321
+ */
15322
+ function isWordCode(code) {
15323
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
15324
+ }
15325
+ /**
15326
+ * Whether `\b` holds at an offset
15327
+ */
15328
+ function isBoundary(text, at) {
15329
+ return isWordCode(text.charCodeAt(at - 1)) !== isWordCode(text.charCodeAt(at));
15330
+ }
15331
+ /**
15332
+ * Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
15333
+ * Bounded rather than indexOf, which would scan on to the end of the header once for every
15334
+ * whitespace delimited run and make the walk below quadratic.
15335
+ */
15336
+ function indexOfAt(text, from, to) {
15337
+ for (let i = from; i < to; i++) if (text.charCodeAt(i) === 64) return i;
15338
+ return -1;
15339
+ }
15340
+ /**
15341
+ * Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
14577
15342
  *
14578
- * Scanning whitespace delimited tokens instead is linear and keeps the word boundary
14579
- * semantics of the regex: the local part has to open on a word character and the domain
14580
- * has to end on one.
15343
+ * Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
15344
+ * offset and rescans the run to the next '@' each time, so a single header well inside
15345
+ * the default size limit could hold a core busy for minutes. The search is also
15346
+ * unnecessary. '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin
15347
+ * at the head of a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b'
15348
+ * gives characters back until it lands on a boundary, so the only end it can take in that
15349
+ * run is the last boundary in it. Both are found in one pass, and the pattern is then run
15350
+ * at that single offset.
14581
15351
  *
14582
- * @param {String} text Text to search
14583
- * @return {Object|null} `{index, length, value}` of the address, or null if there is none
15352
+ * @param text Free text to look in
15353
+ * @return Offset to match at, or -1
14584
15354
  */
14585
- function findAddressInText(text) {
14586
- NON_SPACE_TOKEN_REGEX.lastIndex = 0;
14587
- let match;
14588
- while (match = NON_SPACE_TOKEN_REGEX.exec(text)) {
14589
- const token = match[0];
14590
- const at = token.indexOf("@");
14591
- let start = 0;
14592
- while (start < at && !WORD_CHAR_REGEX.test(token.charAt(start))) start++;
14593
- if (start >= at) continue;
14594
- let end = token.length;
14595
- while (end > at + 1 && !WORD_CHAR_REGEX.test(token.charAt(end - 1))) end--;
14596
- if (end <= at + 1) continue;
14597
- return {
14598
- index: match.index + start,
14599
- length: end - start,
14600
- value: token.substring(start, end)
15355
+ function looseAddressStart(text) {
15356
+ const len = text.length;
15357
+ let pos = 0;
15358
+ while (pos < len) {
15359
+ while (pos < len && isSpaceCode(text.charCodeAt(pos))) pos++;
15360
+ if (pos >= len) break;
15361
+ const runStart = pos;
15362
+ let runEnd = pos;
15363
+ while (runEnd < len && !isSpaceCode(text.charCodeAt(runEnd))) runEnd++;
15364
+ let at = indexOfAt(text, runStart, runEnd);
15365
+ if (at >= 0) {
15366
+ let lastBoundary = -1;
15367
+ for (let k = runEnd; k > runStart; k--) if (isBoundary(text, k)) {
15368
+ lastBoundary = k;
15369
+ break;
15370
+ }
15371
+ let atomStart = runStart;
15372
+ while (lastBoundary >= 0 && at >= 0) {
15373
+ if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
15374
+ for (let start = atomStart; start < at; start++) if (isBoundary(text, start)) {
15375
+ if (start > runStart) return start;
15376
+ let padded = runStart;
15377
+ while (padded > 0 && isSpaceCode(text.charCodeAt(padded - 1))) padded--;
15378
+ return padded;
15379
+ }
15380
+ }
15381
+ atomStart = at + 1;
15382
+ at = indexOfAt(text, atomStart, runEnd);
15383
+ }
15384
+ }
15385
+ pos = runEnd;
15386
+ }
15387
+ return -1;
15388
+ }
15389
+ /**
15390
+ * Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
15391
+ *
15392
+ * A malformed header can put more than a mailbox between the angle brackets, most often
15393
+ * because the generator wrote the recipient twice: '<user@example.com user@example.com>'
15394
+ * or '<example.com user@example.com>'. Whitespace is not addr-spec, so the whole run can
15395
+ * never be a mailbox, and passing it on as the address loses the recipient that is
15396
+ * sitting right there in the header.
15397
+ *
15398
+ * The run that still reads as an addr-spec is kept and whatever is left over becomes
15399
+ * display text. Candidates are read strictly first and then under the looser grammar,
15400
+ * the same two tiers the unquoted text fallback applies, so that '<a@b@c.com junk>' and a
15401
+ * bare 'a@b@c.com junk' agree on the recipient.
15402
+ *
15403
+ * A quoted local part is left alone: RFC 5321 allows whitespace inside it, so
15404
+ * '<"user name"@example.com>' is well formed and means exactly what it says.
15405
+ *
15406
+ * @param address Collected address
15407
+ * @param text Collected display text
15408
+ * @return The address and the display text, with any leftovers moved to the text
15409
+ */
15410
+ function recoverAddrSpec(address, text) {
15411
+ if (!HAS_WHITESPACE.test(address)) return {
15412
+ address,
15413
+ text
15414
+ };
15415
+ let rest;
15416
+ const quoted = address.match(QUOTED_LOCAL_ADDR);
15417
+ if (quoted) {
15418
+ if (!quoted[2]) return {
15419
+ address,
15420
+ text
15421
+ };
15422
+ address = quoted[1];
15423
+ rest = [quoted[2]];
15424
+ } else {
15425
+ if (address.indexOf("\"") >= 0) return {
15426
+ address,
15427
+ text
15428
+ };
15429
+ const parts = address.split(/\s+/);
15430
+ let addrIndex = parts.findIndex((part) => ADDR_SPEC.test(part));
15431
+ if (addrIndex < 0) addrIndex = parts.findIndex((part) => LOOSE_ADDR_SPEC.test(part));
15432
+ if (addrIndex < 0) return {
15433
+ address,
15434
+ text
14601
15435
  };
15436
+ address = parts.splice(addrIndex, 1)[0];
15437
+ rest = parts;
14602
15438
  }
14603
- return null;
15439
+ return {
15440
+ address,
15441
+ text: [text, ...rest].filter((part) => part).join(" ")
15442
+ };
15443
+ }
15444
+ /**
15445
+ * Takes the RFC 5322 comments out of the contents of an angle-addr.
15446
+ *
15447
+ * The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
15448
+ * a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
15449
+ * put 'user@example.com(x)evil.com' into the address, a value that is no mailbox and that a
15450
+ * consumer stripping the comment reads as 'user@example.comevil.com'. A comment is folding
15451
+ * whitespace, so it is read as a space that splits two atoms and as nothing next to an '@',
15452
+ * the same rule the token walk applies outside the brackets. Quoted strings and domain
15453
+ * literals are copied through untouched, a '(' in there is not a comment.
15454
+ *
15455
+ * @param address Contents of the angle brackets
15456
+ * @return The address with the comments removed, and the text of the comments
15457
+ */
15458
+ function stripAddressComments(address) {
15459
+ const comments = [];
15460
+ let result = "";
15461
+ let comment = "";
15462
+ let depth = 0;
15463
+ let closer = "";
15464
+ let lastChar = "";
15465
+ for (let i = 0, len = address.length; i < len; i++) {
15466
+ const chr = address.charAt(i);
15467
+ if (depth) {
15468
+ if (chr === "\\" && i < len - 1) comment += address.charAt(++i);
15469
+ else if (chr === "(") {
15470
+ depth++;
15471
+ comment += chr;
15472
+ } else if (chr === ")" && !--depth) {
15473
+ comments.push(comment.trim());
15474
+ comment = "";
15475
+ if (lastChar !== "@" && address.charAt(i + 1) !== "@") {
15476
+ result += " ";
15477
+ lastChar = " ";
15478
+ }
15479
+ } else comment += chr;
15480
+ continue;
15481
+ }
15482
+ if (closer) {
15483
+ if (chr === "\\" && closer === "\"" && i < len - 1) {
15484
+ result += chr + address.charAt(++i);
15485
+ lastChar = address.charAt(i);
15486
+ continue;
15487
+ }
15488
+ if (chr === closer) closer = "";
15489
+ } else if (chr === "\"") closer = "\"";
15490
+ else if (chr === "[") closer = "]";
15491
+ else if (chr === "(") {
15492
+ depth = 1;
15493
+ continue;
15494
+ }
15495
+ result += chr;
15496
+ lastChar = chr;
15497
+ }
15498
+ if (depth) comments.push(comment.trim());
15499
+ return {
15500
+ address: result.trim(),
15501
+ comments: comments.filter((text) => text)
15502
+ };
15503
+ }
15504
+ /**
15505
+ * Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
15506
+ * without other angle brackets that has an '@' with at least one character on each side.
15507
+ *
15508
+ * Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
15509
+ * followed by a long run of '@' and no closing `>`.
15510
+ *
15511
+ * @param text Text to check
15512
+ * @return true if an angle bracket address is present
15513
+ */
15514
+ function hasAngleAddress(text) {
15515
+ let start = -1;
15516
+ let at = -1;
15517
+ for (let i = 0; i < text.length; i++) {
15518
+ const chr = text.charAt(i);
15519
+ if (chr === "<") {
15520
+ start = i + 1;
15521
+ at = -1;
15522
+ } else if (chr === ">") {
15523
+ if (at >= 0 && at < i - 1) return true;
15524
+ start = -1;
15525
+ at = -1;
15526
+ } else if (chr === "@" && start >= 0 && at < 0 && i > start) at = i;
15527
+ }
15528
+ return false;
14604
15529
  }
14605
15530
  /**
14606
15531
  * Converts tokens for a single address into an address object
14607
15532
  *
14608
- * @param {Array} tokens Tokens object
14609
- * @param {Number} depth Current recursion depth for nested group protection
14610
- * @return {Object} Address object
15533
+ * @param tokens Tokens object
15534
+ * @param depth Current recursion depth for nested group protection
15535
+ * @return Address objects
14611
15536
  */
14612
15537
  function _handleAddress(tokens, depth) {
14613
15538
  let isGroup = false;
14614
15539
  let state = "text";
14615
- let address;
14616
15540
  let addresses = [];
14617
15541
  let data = {
14618
15542
  address: [],
@@ -14624,6 +15548,12 @@ function _handleAddress(tokens, depth) {
14624
15548
  let i;
14625
15549
  let len;
14626
15550
  let insideQuotes = false;
15551
+ const lastChars = {
15552
+ address: "",
15553
+ comment: "",
15554
+ group: "",
15555
+ text: ""
15556
+ };
14627
15557
  for (i = 0, len = tokens.length; i < len; i++) {
14628
15558
  let token = tokens[i];
14629
15559
  let prevToken = i ? tokens[i - 1] : null;
@@ -14650,14 +15580,17 @@ function _handleAddress(tokens, depth) {
14650
15580
  insideQuotes = false;
14651
15581
  }
14652
15582
  else if (token.value) {
15583
+ const prevPrevToken = i > 1 ? tokens[i - 2] : null;
15584
+ const opensAfterEmptyQuotedString = prevToken?.type === "operator" && prevToken.value === "\"" && !!prevToken.noBreak && prevPrevToken?.type === "operator" && prevPrevToken.value === "\"";
14653
15585
  if (state === "address") token.value = token.value.replace(/^[^<]*<\s*/, "");
14654
- if (prevToken && prevToken.noBreak && data[state].length) {
15586
+ if (prevToken && prevToken.noBreak && data[state].length && (prevToken.value !== ")" || lastChars[state] === "@" || token.value.charAt(0) === "@")) {
14655
15587
  data[state][data[state].length - 1] += token.value;
14656
15588
  if (state === "text" && insideQuotes) data.textWasQuoted[data.textWasQuoted.length - 1] = true;
14657
15589
  } else {
14658
15590
  data[state].push(token.value);
14659
- if (state === "text") data.textWasQuoted.push(insideQuotes);
15591
+ if (state === "text") data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
14660
15592
  }
15593
+ lastChars[state] = token.value.charAt(token.value.length - 1);
14661
15594
  }
14662
15595
  }
14663
15596
  if (!data.text.length && data.comment.length) {
@@ -14665,32 +15598,49 @@ function _handleAddress(tokens, depth) {
14665
15598
  data.comment = [];
14666
15599
  }
14667
15600
  if (isGroup) {
14668
- data.text = data.text.join(" ");
15601
+ const text = data.text.join(" ");
14669
15602
  let groupMembers = [];
14670
- if (data.group.length) addressParser(data.group.join(","), { _depth: depth + 1 }).forEach((member) => {
14671
- if (member.group) groupMembers = groupMembers.concat(member.group);
15603
+ if (data.group.length) parseAddressList$1(data.group.join(","), depth + 1).forEach((member) => {
15604
+ if (member.group) for (const nestedMember of member.group) groupMembers.push(nestedMember);
14672
15605
  else groupMembers.push(member);
14673
15606
  });
14674
15607
  addresses.push({
14675
- name: decodeWords(data.text || address && address.name),
15608
+ name: decodeWords(text),
14676
15609
  group: groupMembers
14677
15610
  });
14678
15611
  } else {
15612
+ const addressComments = [];
15613
+ const addressParts = [];
15614
+ for (const part of data.address) {
15615
+ if (part.indexOf("(") < 0) {
15616
+ addressParts.push(part);
15617
+ continue;
15618
+ }
15619
+ const stripped = stripAddressComments(part);
15620
+ for (const comment of stripped.comments) addressComments.push(comment);
15621
+ if (stripped.address) addressParts.push(stripped.address);
15622
+ }
15623
+ data.address = addressParts;
14679
15624
  if (!data.address.length && data.text.length) {
14680
- for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
15625
+ for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i] && ADDR_SPEC.test(data.text[i])) {
14681
15626
  data.address = data.text.splice(i, 1);
14682
15627
  data.textWasQuoted.splice(i, 1);
14683
15628
  break;
14684
15629
  }
14685
15630
  if (!data.address.length) {
14686
15631
  for (i = data.text.length - 1; i >= 0; i--) if (!data.textWasQuoted[i]) {
14687
- const found = findAddressInText(data.text[i]);
14688
- if (found) {
14689
- data.address = [found.value];
14690
- data.text[i] = (data.text[i].substring(0, found.index).replace(/\s+$/, "") + " " + data.text[i].substring(found.index + found.length).replace(/^\s+/, "")).trim();
14691
- break;
15632
+ const part = data.text[i];
15633
+ const start = looseAddressStart(part);
15634
+ if (start >= 0) {
15635
+ LOOSE_TEXT_ADDR.lastIndex = start;
15636
+ const match = LOOSE_TEXT_ADDR.exec(part);
15637
+ if (match) {
15638
+ data.address = [match[0].trim()];
15639
+ data.text[i] = (part.slice(0, start) + " " + part.slice(start + match[0].length)).trim();
15640
+ break;
15641
+ }
14692
15642
  }
14693
- data.text[i] = data.text[i].trim();
15643
+ data.text[i] = part.trim();
14694
15644
  }
14695
15645
  }
14696
15646
  }
@@ -14699,25 +15649,33 @@ function _handleAddress(tokens, depth) {
14699
15649
  data.comment = [];
14700
15650
  }
14701
15651
  if (data.address.length > 1) data.text = data.text.concat(data.address.splice(1));
14702
- data.text = data.text.join(" ");
14703
- data.address = data.address.join(" ");
14704
- if (!data.address && isEncodedWordsOnly(data.text.trim())) {
14705
- const decodedText = decodeWords(data.text);
14706
- if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
14707
- const parsedSubAddresses = addressParser(decodedText);
14708
- if (parsedSubAddresses && parsedSubAddresses.length) return parsedSubAddresses;
15652
+ const addressFromQuotedText = !data.address.length && data.textWasQuoted.some((wasQuoted) => wasQuoted);
15653
+ let text = data.text.join(" ");
15654
+ let addressValue = data.address.join(" ");
15655
+ if (!addressValue && isEncodedWordsOnly(text.trim())) {
15656
+ const decodedText = decodeWords(text);
15657
+ if (!addressFromQuotedText && hasAngleAddress(decodedText)) {
15658
+ const parsedSubAddresses = parseAddressList$1(decodedText, depth + 1);
15659
+ if (parsedSubAddresses.length) return parsedSubAddresses;
14709
15660
  }
14710
15661
  return [{
14711
15662
  address: "",
14712
15663
  name: decodedText
14713
15664
  }];
14714
15665
  }
14715
- address = {
14716
- address: data.address || data.text || "",
14717
- name: decodeWords(data.text || data.address || "")
15666
+ if (addressFromQuotedText && text.indexOf("@") >= 0) {
15667
+ addressValue = quoteLocalPart(text);
15668
+ text = "";
15669
+ }
15670
+ ({address: addressValue, text} = recoverAddrSpec(addressValue, text));
15671
+ if (!text && addressComments.length) text = addressComments.join(" ");
15672
+ const rawName = text || addressValue || "";
15673
+ const address = {
15674
+ address: addressValue || text || "",
15675
+ name: decodeWords(rawName)
14718
15676
  };
14719
- if (address.address === address.name) {
14720
- if ((address.address || "").match(/@/)) address.name = "";
15677
+ if (address.address === rawName || address.address === address.name) {
15678
+ if (address.address.match(/@/)) address.name = "";
14721
15679
  else address.address = "";
14722
15680
  }
14723
15681
  addresses.push(address);
@@ -14725,22 +15683,21 @@ function _handleAddress(tokens, depth) {
14725
15683
  return addresses;
14726
15684
  }
14727
15685
  /**
14728
- * Creates a Tokenizer object for tokenizing address field strings
14729
- *
14730
- * @constructor
14731
- * @param {String} str Address field string
15686
+ * Tokenizer for address field strings
14732
15687
  */
14733
15688
  var Tokenizer$2 = class {
15689
+ /**
15690
+ * @param str Address field string
15691
+ */
14734
15692
  constructor(str) {
14735
15693
  this.str = (str || "").toString();
14736
15694
  this.operatorCurrent = "";
14737
15695
  this.operatorExpecting = "";
14738
15696
  this.node = null;
14739
15697
  this.escaped = false;
15698
+ this.inDomainLiteral = false;
15699
+ this.afterAt = false;
14740
15700
  this.list = [];
14741
- /**
14742
- * Operator tokens and which tokens are expected to end the sequence
14743
- */
14744
15701
  this.operators = {
14745
15702
  "\"": "\"",
14746
15703
  "(": ")",
@@ -14753,7 +15710,7 @@ var Tokenizer$2 = class {
14753
15710
  /**
14754
15711
  * Tokenizes the original input string
14755
15712
  *
14756
- * @return {Array} An array of operator|text tokens
15713
+ * @return An array of operator|text tokens
14757
15714
  */
14758
15715
  tokenize() {
14759
15716
  let list = [];
@@ -14771,9 +15728,14 @@ var Tokenizer$2 = class {
14771
15728
  /**
14772
15729
  * Checks if a character is an operator or text and acts accordingly
14773
15730
  *
14774
- * @param {String} chr Character from the address field
15731
+ * @param chr Character from the address field
15732
+ * @param nextChr The character that follows, or null at the end of the field
14775
15733
  */
14776
15734
  checkChar(chr, nextChr) {
15735
+ if (!this.escaped && !this.operatorExpecting) {
15736
+ if (!this.inDomainLiteral && chr === "[" && this.afterAt) this.inDomainLiteral = true;
15737
+ else if (this.inDomainLiteral && (chr === "]" || chr === "," || chr === ";")) this.inDomainLiteral = false;
15738
+ }
14777
15739
  if (this.escaped) {} else if (chr === this.operatorExpecting) {
14778
15740
  this.node = {
14779
15741
  type: "operator",
@@ -14789,20 +15751,22 @@ var Tokenizer$2 = class {
14789
15751
  ].includes(nextChr)) this.node.noBreak = true;
14790
15752
  this.list.push(this.node);
14791
15753
  this.node = null;
15754
+ if (chr !== ")") this.afterAt = false;
14792
15755
  this.operatorExpecting = "";
14793
15756
  this.escaped = false;
14794
15757
  return;
14795
- } else if (!this.operatorExpecting && chr in this.operators) {
15758
+ } else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ":") && chr in this.operators) {
14796
15759
  this.node = {
14797
15760
  type: "operator",
14798
15761
  value: chr
14799
15762
  };
14800
15763
  this.list.push(this.node);
14801
15764
  this.node = null;
15765
+ if (chr !== "(") this.afterAt = false;
14802
15766
  this.operatorExpecting = this.operators[chr];
14803
15767
  this.escaped = false;
14804
15768
  return;
14805
- } else if (this.operatorExpecting === "\"" && chr === "\\") {
15769
+ } else if ((this.operatorExpecting === "\"" || this.operatorExpecting === ")") && chr === "\\") {
14806
15770
  this.escaped = true;
14807
15771
  return;
14808
15772
  }
@@ -14814,7 +15778,10 @@ var Tokenizer$2 = class {
14814
15778
  this.list.push(this.node);
14815
15779
  }
14816
15780
  if (chr === "\n") chr = " ";
14817
- if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) this.node.value += chr;
15781
+ if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
15782
+ this.node.value += chr;
15783
+ if (!this.operatorExpecting && chr !== " " && chr !== " ") this.afterAt = chr === "@";
15784
+ }
14818
15785
  this.escaped = false;
14819
15786
  }
14820
15787
  };
@@ -14825,29 +15792,20 @@ var Tokenizer$2 = class {
14825
15792
  */
14826
15793
  const MAX_NESTED_GROUP_DEPTH = 50;
14827
15794
  /**
14828
- * Parses structured e-mail addresses from an address field
14829
- *
14830
- * Example:
14831
- *
14832
- * 'Name <address@domain>'
15795
+ * Parses an address list, recursing into groups. The depth is threaded through the
15796
+ * calls rather than taken from an option, so a caller supplied options object can not
15797
+ * seed it and lift the recursion limit.
14833
15798
  *
14834
- * will be converted to
14835
- *
14836
- * [{name: 'Name', address: 'address@domain'}]
14837
- *
14838
- * @param {String} str Address field
14839
- * @param {Object} options Optional options object
14840
- * @param {Number} options._depth Internal recursion depth counter (do not set manually)
14841
- * @return {Array} An array of address objects
15799
+ * @param str Address field
15800
+ * @param depth Current recursion depth for nested group protection
15801
+ * @return An array of address objects
14842
15802
  */
14843
- function addressParser(str, options) {
14844
- options = options || {};
14845
- let depth = options._depth || 0;
15803
+ function parseAddressList$1(str, depth) {
14846
15804
  if (depth > MAX_NESTED_GROUP_DEPTH) return [];
14847
15805
  let tokens = new Tokenizer$2(str).tokenize();
14848
15806
  let addresses = [];
14849
15807
  let address = [];
14850
- let parsedAddresses = [];
15808
+ const parsedAddresses = [];
14851
15809
  tokens.forEach((token) => {
14852
15810
  if (token.type === "operator" && (token.value === "," || token.value === ";")) {
14853
15811
  if (address.length) addresses.push(address);
@@ -14855,11 +15813,34 @@ function addressParser(str, options) {
14855
15813
  } else address.push(token);
14856
15814
  });
14857
15815
  if (address.length) addresses.push(address);
14858
- addresses.forEach((address) => {
14859
- address = _handleAddress(address, depth);
14860
- if (address.length) parsedAddresses = parsedAddresses.concat(address);
14861
- });
14862
- if (options.flatten) {
15816
+ for (const tokens of addresses) for (const address of _handleAddress(tokens, depth)) parsedAddresses.push(address);
15817
+ const mergedAddresses = [];
15818
+ for (let i = parsedAddresses.length - 1; i >= 0; i--) {
15819
+ const current = parsedAddresses[i];
15820
+ const next = mergedAddresses.length ? mergedAddresses[mergedAddresses.length - 1] : null;
15821
+ if (next && current.address === "" && current.name && !current.group && next.address && next.name) next.name = current.name + ", " + next.name;
15822
+ else mergedAddresses.push(current);
15823
+ }
15824
+ return mergedAddresses.reverse();
15825
+ }
15826
+ /**
15827
+ * Parses structured e-mail addresses from an address field
15828
+ *
15829
+ * Example:
15830
+ *
15831
+ * 'Name <address@domain>'
15832
+ *
15833
+ * will be converted to
15834
+ *
15835
+ * [{name: 'Name', address: 'address@domain'}]
15836
+ *
15837
+ * @param str Address field
15838
+ * @param options Optional options object
15839
+ * @return An array of address objects
15840
+ */
15841
+ function addressParser(str, options) {
15842
+ const parsedAddresses = parseAddressList$1(str, 0);
15843
+ if (options && options.flatten) {
14863
15844
  let addresses = [];
14864
15845
  let walkAddressList = (list) => {
14865
15846
  list.forEach((address) => {
@@ -14872,6 +15853,7 @@ function addressParser(str, options) {
14872
15853
  }
14873
15854
  return parsedAddresses;
14874
15855
  }
15856
+ new Uint8Array([10]);
14875
15857
  //#endregion
14876
15858
  //#region ../../node_modules/.pnpm/domelementtype@3.0.0/node_modules/domelementtype/dist/index.js
14877
15859
  /** Types of elements found in htmlparser2's DOM */
@@ -25992,7 +26974,7 @@ function bad(message, hint) {
25992
26974
  /** A bare address, checked and canonicalised. Display names are only accepted on From. */
25993
26975
  function address(value, field) {
25994
26976
  const trimmed = value.trim();
25995
- if (!ADDRESS.test(trimmed)) throw bad(`${field}: "${[...trimmed].slice(0, 80).join("")}" is not an email address`, "Give bare addresses.");
26977
+ if (!ADDRESS.test(trimmed)) throw bad(`${field} contains a value that is not a bare email address`, "Give bare addresses.");
25996
26978
  return canonicalAddress(trimmed);
25997
26979
  }
25998
26980
  function list(values, field) {
@@ -27778,11 +28760,37 @@ function outcomeUnavailable(why) {
27778
28760
  //#endregion
27779
28761
  //#region src/operations/send.ts
27780
28762
  /**
28763
+ * A caller may hand back the canonical expectation it kept, or the display-safe `expect` that prepare returned. The
28764
+ * latter is accepted only when it is byte-for-byte this approval's field rendering under the boundary it carries, the
28765
+ * way Gmail accepts its own; then the claim compares the raw binding. Anything else is passed on as given, so an
28766
+ * invented or edited envelope still mismatches and the send is refused.
28767
+ */
28768
+ function canonicalSuppliedExpectation(supplied, bound, account, id) {
28769
+ const boundary = /^<untrusted-content boundary="([A-Za-z0-9_-]+)" field="subject"/.exec(supplied.subject)?.[1];
28770
+ if (boundary === void 0) return supplied;
28771
+ const displayed = expectationFields(bound, {
28772
+ boundary,
28773
+ account,
28774
+ id
28775
+ });
28776
+ const same = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
28777
+ return displayed.subject === supplied.subject && same(displayed.to, supplied.to) && same(displayed.cc, supplied.cc) && same(displayed.bcc, supplied.bcc) ? bound : supplied;
28778
+ }
28779
+ /**
27781
28780
  * A used approval, as Resend's own surfaces refuse it (design 2026-10-05 §D2, the used-send row): Resend accepted the
27782
28781
  * email — which is not that it went, since one may be scheduled, bounce or be cancelled — and never "sent". The words
27783
28782
  * `resend_send_wait` shows for it too, core's for every provider.
27784
28783
  */
27785
28784
  const ACCEPTED_BY_RESEND = (usedAt) => `accepted by Resend at ${usedAt}`;
28785
+ /** A send's recipients and subject as Resend may return them: display-safe, never the raw approval binding. */
28786
+ function expectationFields(expect, envelope) {
28787
+ return {
28788
+ to: expect.to.map((address) => addressField(address, "to", envelope)),
28789
+ cc: expect.cc.map((address) => addressField(address, "cc", envelope)),
28790
+ bcc: expect.bcc.map((address) => addressField(address, "bcc", envelope)),
28791
+ subject: wrapField(expect.subject, "subject", envelope)
28792
+ };
28793
+ }
27786
28794
  /**
27787
28795
  * A send's approval as the one account that may use it, classified under its lock as the claim would classify it
27788
28796
  * (design 2026-10-05 §D2), or a refusal that does not touch it.
@@ -28043,9 +29051,13 @@ async function prepareSend(context, name, input) {
28043
29051
  effectivePolicy: effective,
28044
29052
  riskFlags: study.flags,
28045
29053
  reach,
28046
- expect: record.expect,
29054
+ expect: expectationFields(record.expect, {
29055
+ boundary: newBoundary(),
29056
+ account: name,
29057
+ id: record.approvalId
29058
+ }),
28047
29059
  expiresAt: record.expiresAt,
28048
- nextStep: effective === "confirm" ? `${approveAndWaitSentence(context.handoffs, context.surface, record.approvalId, (command, wait) => `Show the preview to the user, then have them run ${command} in their own terminal${wait === void 0 ? "" : `; learn when they have with ${wait}`}.`, { instead: "Show the preview to the user; they approve it at their own terminal." })} You cannot approve this yourself. Then execute it with the same approval id and the recipients and subject shown.` : "Show the preview to the user verbatim and wait for an explicit yes. Then execute it with the same approval id and the recipients and subject shown above.",
29060
+ nextStep: effective === "confirm" ? `${approveAndWaitSentence(context.handoffs, context.surface, record.approvalId, (command, wait) => `Show the preview to the user, then have them run ${command} in their own terminal${wait === void 0 ? "" : `; learn when they have with ${wait}`}.`, { instead: "Show the preview to the user; they approve it at their own terminal." })} You cannot approve this yourself. Then execute it with the same approval id.` : "Show the preview to the user verbatim and wait for an explicit yes. Then execute it with the same approval id.",
28049
29061
  approval: await context.core.approvals.approvalOf(record),
28050
29062
  ...legacyDrain === void 0 ? {} : { legacyDrain }
28051
29063
  };
@@ -28236,7 +29248,7 @@ async function claimAndSend(context, named, name, options, known, _seen, legacyD
28236
29248
  contentDigest: digest,
28237
29249
  inboxId: named.account.id,
28238
29250
  inboxSub: named.account.userId,
28239
- expect: options.expect
29251
+ expect: options.expect === void 0 ? known.expect : canonicalSuppliedExpectation(options.expect, known.expect, name, known.approvalId)
28240
29252
  }, {
28241
29253
  pendingHint: `${approveAndWaitSentence(context.handoffs, context.surface, options.approvalId, (command, wait) => `Ask the user to run ${command} in their own terminal${wait === void 0 ? "," : `; learn when they have with ${wait},`} then execute it again with the same approval.`, { instead: "Ask the user to approve it at their own terminal, then execute it again with the same approval." })} You cannot approve it yourself.`,
28242
29254
  usedSaid: ACCEPTED_BY_RESEND,
@@ -28396,16 +29408,26 @@ async function claimAndSend(context, named, name, options, known, _seen, legacyD
28396
29408
  state: message.scheduledAt ? "scheduled" : "sent",
28397
29409
  scheduledAt: message.scheduledAt,
28398
29410
  said: acceptedSaid(resendId !== void 0, message.scheduledAt),
28399
- to: claimed.expect.to,
28400
- cc: claimed.expect.cc,
28401
- bcc: claimed.expect.bcc,
28402
- subject: claimed.expect.subject,
29411
+ ...expectationFields(claimed.expect, {
29412
+ boundary: newBoundary(),
29413
+ account: name,
29414
+ id: options.approvalId
29415
+ }),
28403
29416
  ...unrecorded.length > 0 ? { note: unrecorded.join("; ") } : {},
28404
29417
  approval: approval ?? await approvalNow(context, options.approvalId, named.account.id, claimedApproval),
28405
29418
  ...legacyDrain === void 0 ? {} : { legacyDrain }
28406
29419
  };
28407
29420
  });
28408
29421
  }
29422
+ /** A stored send as a result may show it: canonical recipients stay addresses; its subject is always prose. */
29423
+ function sendSummaryFields(summary, envelope) {
29424
+ if (summary === null) return null;
29425
+ return {
29426
+ ...summary,
29427
+ recipients: summary.recipients.map((address) => addressField(address, "recipient", envelope)),
29428
+ ...summary.subject === void 0 ? {} : { subject: wrapField(summary.subject, "subject", envelope) }
29429
+ };
29430
+ }
28409
29431
  const CANNOT_READ = "this key can only send, so it cannot read what Resend did with the email";
28410
29432
  /**
28411
29433
  * What this machine knows Resend accepted, from its own send record: a scheduled send is accepted for the time the
@@ -28439,11 +29461,16 @@ async function sendStatus(context, name, approvalId) {
28439
29461
  throw error;
28440
29462
  });
28441
29463
  const stored = seen?.stored ?? null;
29464
+ const envelope = {
29465
+ boundary: newBoundary(),
29466
+ account: name,
29467
+ id: approvalId
29468
+ };
28442
29469
  const base = {
28443
29470
  account: name,
28444
29471
  approvalId,
28445
- approval: seen ? publicApproval(seen.stored, seen.outcome) : null,
28446
- local
29472
+ approval: seen ? publicApproval(seen.stored, seen.outcome, { wrap: (text, field) => field === "subject" || field === "filename" ? wrapField(text, field, envelope) : addressField(text, field, envelope) }) : null,
29473
+ local: sendSummaryFields(local, envelope)
28447
29474
  };
28448
29475
  if (!local) return {
28449
29476
  ...base,