@agentcomms/whatsapp 0.17.0 → 0.18.1

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.
@@ -11,7 +11,7 @@ import { execFile, spawn } from "node:child_process";
11
11
  //#endregion
12
12
  //#region src/version.ts
13
13
  /** The package version, read from package.json at build time. */
14
- const VERSION$1 = "0.17.0";
14
+ const VERSION$1 = "0.18.1";
15
15
  //#endregion
16
16
  //#region src/caller.ts
17
17
  /** This package's name, as its manifest has it. */
@@ -28,7 +28,7 @@ const CALLER = Object.freeze({
28
28
  packageName: PACKAGE_NAME
29
29
  });
30
30
  //#endregion
31
- //#region ../core/dist/update-check-XMsGuXRl.mjs
31
+ //#region ../core/dist/update-check-DvZxdFU_.mjs
32
32
  /**
33
33
  * Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
34
34
  * optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
@@ -1139,6 +1139,24 @@ const CHANNEL_SNAPSHOT = [
1139
1139
  }
1140
1140
  }
1141
1141
  ];
1142
+ /** Every declared service server: what core knows about it without installing it. */
1143
+ const SERVICE_SNAPSHOT = [{
1144
+ packageName: "@agentcomms/events-daemon",
1145
+ declaration: {
1146
+ kind: "service",
1147
+ binary: "agent-events",
1148
+ server: {
1149
+ defaultName: "events",
1150
+ entry: "src/mcp/server.ts",
1151
+ factory: "createEventsMcpServer"
1152
+ },
1153
+ operations: "src/operations",
1154
+ skills: {
1155
+ prefix: "agentcomms-",
1156
+ contract: "skills/_shared/contract-events.md"
1157
+ }
1158
+ }
1159
+ }];
1142
1160
  const DISCLOSURE_VERSION_KINDS = [
1143
1161
  "rule",
1144
1162
  "target",
@@ -1326,8 +1344,8 @@ function bindingDigestOf(record) {
1326
1344
  * Everywhere but Windows that shell is a POSIX one, and a word goes in single quotes, inside which nothing is special
1327
1345
  * but the quote itself. Windows has two shells, and neither reads single quotes that way: cmd.exe does not take them
1328
1346
  * as quotes at all, so `'C:\Profiles\First Last\outgoing'` reached the command as two words, quote marks and all, and
1329
- * PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A command printed on Windows
1330
- * has to be safe in both, because nothing says which one it will be pasted into (CUE-306) — and safe is not enough:
1347
+ * PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A common line printed on
1348
+ * Windows has to be safe in both — and safe is not enough:
1331
1349
  * the program has to receive the same words from either. Three readers stand between the line and the program. cmd.exe
1332
1350
  * hands the program the line as it is, and the program's own parser (the C runtime's, Node's) splits it. PowerShell
1333
1351
  * reads the line itself, a double-quoted word with its backslashes as plain characters, and then writes a new command
@@ -1354,15 +1372,17 @@ function bindingDigestOf(record) {
1354
1372
  * backslash, which the C runtime reads with the closing quote as `\"` — and doubled for it, PowerShell 7 passes both
1355
1373
  * backslashes on where cmd.exe and Windows PowerShell pass one; and a word with `& | < > ^ ( )` but no whitespace,
1356
1374
  * which PowerShell passes to a `.cmd` script unquoted, for cmd.exe to run `&whoami` from or to split at `|`.
1357
- * - With any other word in it, no line is printed at all (`line` is null), and the printers show the command's words
1375
+ * - With any other word in it, the common renderer has no line, and the printers show the command's words
1358
1376
  * as JSON instead, saying it has to be typed. Not a line with the word left out: a placeholder in its place was
1359
1377
  * still a command that ran — `claude mcp remove NAME` removed whatever entry was called `NAME`, and an install
1360
1378
  * hint's `--force` replaced it — and before that, a word quoted for PowerShell's single quotes, which cmd.exe reads
1361
1379
  * as characters, ran `whoami` from a server named `$x&whoami&`. See `commandAsJson` for why the JSON runs nothing.
1362
1380
  *
1363
- * - The program itself has to be left as it is. Quoted, it is a string to PowerShell rather than a command — PowerShell
1364
- * runs a quoted program only after `&`, which cmd.exe reads as the end of a command — so a program whose path needs
1365
- * quotes, as `C:\Program Files\nodejs\node.exe` does, leaves the command with no line either (CUE-403).
1381
+ * - The program itself has to be left as it is in the common form. Quoted, it is a string to PowerShell rather than a
1382
+ * command. When its path needs quoting, CUE-410 may emit a labelled PowerShell line: `&`, then every word in single
1383
+ * quotes, with an inner single quote doubled. Empty words, trailing backslashes, double quotes and control or
1384
+ * formatting characters retain the inert-JSON fallback because their native-program boundary cannot be proved
1385
+ * across the supported PowerShell versions.
1366
1386
  *
1367
1387
  * Everywhere else there is always a line: single quotes make any word safe.
1368
1388
  *
@@ -1378,9 +1398,23 @@ function quoteCommand(words, platform) {
1378
1398
  };
1379
1399
  const printed = words.map(windowsShellWord);
1380
1400
  const programBare = words.length === 0 || printed[0] === words[0];
1401
+ if (!printed.includes(null) && programBare) return {
1402
+ words,
1403
+ line: printed.join(" "),
1404
+ platform
1405
+ };
1406
+ if (!programBare && printed.slice(1).every((word) => word !== null)) {
1407
+ const powershell = words.map(powerShellWord);
1408
+ if (!powershell.includes(null)) return {
1409
+ words,
1410
+ line: `& ${powershell.join(" ")}`,
1411
+ platform,
1412
+ shell: "powershell"
1413
+ };
1414
+ }
1381
1415
  return {
1382
1416
  words,
1383
- line: printed.includes(null) || !programBare ? null : printed.join(" "),
1417
+ line: null,
1384
1418
  platform
1385
1419
  };
1386
1420
  }
@@ -1393,6 +1427,14 @@ function quotedLineWithWordsToFill(command, ...toFill) {
1393
1427
  if (command.line === null) return null;
1394
1428
  const sentinel = command.words.indexOf("--");
1395
1429
  if (sentinel < 0) return [command.line, ...toFill].join(" ");
1430
+ if (command.shell === "powershell") {
1431
+ const printed = command.words.map((each) => powerShellWord(each));
1432
+ return `& ${[
1433
+ ...printed.slice(0, sentinel),
1434
+ ...toFill,
1435
+ ...printed.slice(sentinel)
1436
+ ].join(" ")}`;
1437
+ }
1396
1438
  const word = command.platform === "win32" ? windowsShellWord : posixShellWord;
1397
1439
  const printed = command.words.map((each) => word(each));
1398
1440
  return [
@@ -1420,6 +1462,11 @@ function windowsShellWord(word) {
1420
1462
  if (/[&|<>^()]/.test(word) && !/\s/.test(word)) return null;
1421
1463
  return `"${word}"`;
1422
1464
  }
1465
+ /** One native-command word in a PowerShell single-quoted literal, or null when a one-line call is not provable. */
1466
+ function powerShellWord(word) {
1467
+ if (word === "" || word.endsWith("\\") || /["\u201C-\u201E]|[\p{C}\p{Zl}\p{Zp}]/u.test(word)) return null;
1468
+ return `'${word.replaceAll("'", "''")}'`;
1469
+ }
1423
1470
  /**
1424
1471
  * The words of a command as a JSON array, for a command that cannot be printed as a line: something to read, and to
1425
1472
  * parse, and nothing to run.
@@ -1450,17 +1497,19 @@ function commandAsJson(words) {
1450
1497
  /** What is said beside a command shown as JSON. */
1451
1498
  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";
1452
1499
  /** What is said beside a command shown as JSON because its program's path needs quotes. */
1453
- 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";
1500
+ 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";
1454
1501
  function toType(command) {
1455
1502
  return programNeedsQuotes(command) ? TO_TYPE_PROGRAM : TO_TYPE;
1456
1503
  }
1457
1504
  /** Quoted words in backticks, for a sentence — as words in JSON, saying it has to be typed, when there is no line. */
1458
1505
  function inlineQuoted(command) {
1459
- return command.line === null ? `\`${commandAsJson(command.words)}\` (${toType(command)})` : `\`${command.line}\``;
1506
+ if (command.line === null) return `\`${commandAsJson(command.words)}\` (${toType(command)})`;
1507
+ return `\`${command.line}\`${command.shell === "powershell" ? " (PowerShell)" : ""}`;
1460
1508
  }
1461
1509
  /** Quoted words as text of their own — a list's line, a field's value — or as words in JSON, saying to type them. */
1462
1510
  function quotedText(command) {
1463
- return command.line === null ? `${commandAsJson(command.words)} (${toType(command)})` : command.line;
1511
+ if (command.line === null) return `${commandAsJson(command.words)} (${toType(command)})`;
1512
+ return command.shell === "powershell" ? `PowerShell:\n${command.line}` : command.line;
1464
1513
  }
1465
1514
  /**
1466
1515
  * The same command with words the agent is to fill in — a placeholder such as `<folder>` — before its first `--`: for
@@ -1924,7 +1973,7 @@ function handoffSentence(handoff, say, options = {}) {
1924
1973
  function handoffSentenceToFill(handoff, toFill, say, options = {}) {
1925
1974
  if (!isCommand(handoff)) return noCommand(handoff, options);
1926
1975
  const line = lineWithWordsToFill(handoff, ...toFill);
1927
- if (line !== null) return say(`\`${line}\``);
1976
+ if (line !== null) return say(`\`${line}\`${handoff.shell === "powershell" ? " (PowerShell)" : ""}`);
1928
1977
  return say(inlineQuoted(quotedWithWordsToFill(handoff, toFill)));
1929
1978
  }
1930
1979
  /**
@@ -2124,7 +2173,7 @@ function nearestPackage(start) {
2124
2173
  * Whether `path` is `root` or inside it, by whole path segments: `/x/core-evil` is not inside `/x/core`, which a
2125
2174
  * comparison of strings would say it was. Both are taken as given; a caller compares real paths with real paths.
2126
2175
  */
2127
- function isWithin(root, path) {
2176
+ function isWithin$1(root, path) {
2128
2177
  const between = relative(resolve(root), resolve(path));
2129
2178
  return between === "" || !isAbsolute(between) && between !== ".." && !/^\.\.[\\/]/.test(between);
2130
2179
  }
@@ -2194,15 +2243,18 @@ var ExternalCommand = class {
2194
2243
  #reason;
2195
2244
  /** The words, as the program is to receive them. */
2196
2245
  words;
2197
- /** The line to paste; null when one of the words has no printing that every Windows shell passes on alike. */
2246
+ /** The line to paste; null when no supported common or labelled shell rendering is safe. */
2198
2247
  line;
2199
2248
  platform;
2249
+ shell;
2200
2250
  constructor(gate, words, platform, reason) {
2201
2251
  if (gate !== GATE$1) throw new TypeError("an external command is made by externalCommand, and only there");
2202
2252
  this.#reason = reason;
2203
2253
  this.words = Object.freeze([...words]);
2204
- this.line = quoteCommand(this.words, platform).line;
2254
+ const quoted = quoteCommand(this.words, platform);
2255
+ this.line = quoted.line;
2205
2256
  this.platform = platform;
2257
+ if (quoted.shell !== void 0) this.shell = quoted.shell;
2206
2258
  Object.freeze(this);
2207
2259
  }
2208
2260
  /** Why this program, which is not part of this suite, is one a person may be told to run. */
@@ -2916,6 +2968,343 @@ function commandLinePackage(command, args) {
2916
2968
  const line = [command, ...withoutPathOptions(args).words].join(" ");
2917
2969
  return /(@[\w.-]+\/[\w.-]+|(?<=\s)[\w.-]+-mcp)(?=@|\s|$)/.exec(line)?.[1];
2918
2970
  }
2971
+ const RECEIPT_VERSION = 1;
2972
+ const RECEIPT_FILE = "managed-shims.json";
2973
+ const MUTATION_LOCK_FILE = "managed-shims.lock";
2974
+ const POSIX_SHIM_MODE = 493;
2975
+ 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.";
2976
+ function platformOf(context) {
2977
+ return context.platform ?? process.platform;
2978
+ }
2979
+ /** Why the selected platform has no managed shim in this release. */
2980
+ function managedShimUnsupportedNotice(context = {}) {
2981
+ 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.";
2982
+ }
2983
+ /** The per-user directory managed installs put their command shims in. */
2984
+ function managedShimDirectory(context) {
2985
+ if (!managedShimsSupported(context)) throw new CommsError("CONFIG", managedShimUnsupportedNotice(context));
2986
+ return join(homeDirectory(context.env), ".local", "bin");
2987
+ }
2988
+ /** Managed CLI shims are POSIX-only until Windows has an argument-forwarding form that does not re-parse input. */
2989
+ function managedShimsSupported(context = {}) {
2990
+ const platform = platformOf(context);
2991
+ return platform === "darwin" || platform === "linux";
2992
+ }
2993
+ /** A managed shim's name: one plain command word, never a path, so joining it to the directory stays inside it. */
2994
+ function isSafeBinary(binary) {
2995
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(binary);
2996
+ }
2997
+ function checkedBinary(binary) {
2998
+ if (!isSafeBinary(binary)) throw new CommsError("CONFIG", `${JSON.stringify(binary)} is not a safe managed CLI binary name`);
2999
+ return binary;
3000
+ }
3001
+ /** The exact path for one declared binary's managed shim. */
3002
+ function managedShimPath(context, binary) {
3003
+ return join(managedShimDirectory(context), checkedBinary(binary));
3004
+ }
3005
+ function receiptPath(context) {
3006
+ return join(context.dataDir, RECEIPT_FILE);
3007
+ }
3008
+ function emptyReceipt() {
3009
+ return {
3010
+ version: RECEIPT_VERSION,
3011
+ shims: {}
3012
+ };
3013
+ }
3014
+ async function readReceipt(context) {
3015
+ let parsed;
3016
+ try {
3017
+ parsed = JSON.parse(await readFile(receiptPath(context), "utf8"));
3018
+ } catch (error) {
3019
+ return error.code === "ENOENT" ? emptyReceipt() : null;
3020
+ }
3021
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== RECEIPT_VERSION || typeof parsed.shims !== "object" || parsed.shims === null) return null;
3022
+ const shims = {};
3023
+ for (const [binary, value] of Object.entries(parsed.shims)) {
3024
+ if (!isSafeBinary(binary) || typeof value !== "object" || value === null || ![
3025
+ "path",
3026
+ "digest",
3027
+ "node",
3028
+ "entry"
3029
+ ].every((key) => typeof value[key] === "string")) return null;
3030
+ shims[binary] = value;
3031
+ }
3032
+ return {
3033
+ version: RECEIPT_VERSION,
3034
+ shims
3035
+ };
3036
+ }
3037
+ async function writeReceipt(context, receipt) {
3038
+ await writeFileAtomic(receiptPath(context), `${JSON.stringify(receipt, null, 2)}\n`);
3039
+ }
3040
+ function sha256(bytes) {
3041
+ return createHash("sha256").update(bytes).digest("hex");
3042
+ }
3043
+ function posixWord(word) {
3044
+ return `'${word.replaceAll("'", `'"'"'`)}'`;
3045
+ }
3046
+ function shimSource(spec) {
3047
+ return `#!/bin/sh\n# managed CLI shim v1\nexec ${spec.command.map(posixWord).join(" ")} "$@"\n`;
3048
+ }
3049
+ async function pinShimDirectory(context, create) {
3050
+ const publicDirectory = managedShimDirectory(context);
3051
+ if (create) await mkdir(publicDirectory, {
3052
+ recursive: true,
3053
+ mode: 493
3054
+ });
3055
+ let resolved;
3056
+ try {
3057
+ resolved = await realpath(publicDirectory);
3058
+ } catch (error) {
3059
+ if (!create && error.code === "ENOENT") return null;
3060
+ throw error;
3061
+ }
3062
+ return {
3063
+ public: publicDirectory,
3064
+ resolved
3065
+ };
3066
+ }
3067
+ async function assertPinnedDirectory(directory) {
3068
+ let now;
3069
+ try {
3070
+ now = await realpath(directory.public);
3071
+ } catch {
3072
+ 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." });
3073
+ }
3074
+ 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." });
3075
+ }
3076
+ async function regularFileBytes(path) {
3077
+ try {
3078
+ if (!(await lstat(path)).isFile()) return null;
3079
+ return await readFile(path, "utf8");
3080
+ } catch (error) {
3081
+ if (error.code === "ENOENT") return null;
3082
+ throw error;
3083
+ }
3084
+ }
3085
+ async function pathExists(path) {
3086
+ try {
3087
+ await lstat(path);
3088
+ return true;
3089
+ } catch (error) {
3090
+ if (error.code === "ENOENT") return false;
3091
+ throw error;
3092
+ }
3093
+ }
3094
+ /** Serialize this installation's receipt and shim mutations across its processes. */
3095
+ async function withMutationLock(context, change) {
3096
+ await mkdir(context.dataDir, {
3097
+ recursive: true,
3098
+ mode: 448
3099
+ });
3100
+ return withFileLock(join(context.dataDir, MUTATION_LOCK_FILE), change, {
3101
+ timeoutMs: 5e3,
3102
+ staleMs: 3e4,
3103
+ renewMs: 1e4,
3104
+ ...context.lockOptions
3105
+ });
3106
+ }
3107
+ /** Publish complete shim bytes only if the target is still absent. */
3108
+ async function createShimExclusive(directory, path, source) {
3109
+ const nonce = randomBytes(6).toString("hex");
3110
+ const temp = join(directory.resolved, `.${basename(path)}.${nonce}.tmp`);
3111
+ await assertPinnedDirectory(directory);
3112
+ const handle = await open(temp, "wx", POSIX_SHIM_MODE);
3113
+ try {
3114
+ await handle.writeFile(source);
3115
+ await handle.sync();
3116
+ } finally {
3117
+ await handle.close();
3118
+ }
3119
+ await assertPinnedDirectory(directory);
3120
+ await chmod(temp, POSIX_SHIM_MODE);
3121
+ try {
3122
+ await assertPinnedDirectory(directory);
3123
+ await link(temp, path);
3124
+ } finally {
3125
+ await assertPinnedDirectory(directory);
3126
+ await rm(temp, { force: true });
3127
+ }
3128
+ }
3129
+ /** Restore an object moved aside without ever replacing a raced object at its public path. */
3130
+ async function restoreMoved(directory, path, moved) {
3131
+ try {
3132
+ await assertPinnedDirectory(directory);
3133
+ await link(moved, path);
3134
+ } catch (error) {
3135
+ if (error.code === "EEXIST") return false;
3136
+ throw error;
3137
+ }
3138
+ await assertPinnedDirectory(directory);
3139
+ await rm(moved);
3140
+ return true;
3141
+ }
3142
+ /**
3143
+ * Move the exact current object aside atomically, then prove that object still has the owned digest. A raced foreign
3144
+ * file is put back; if another object has already occupied its path, both are preserved and the change is refused.
3145
+ */
3146
+ async function moveOwned(directory, path, digest, suffix, before) {
3147
+ await before?.(path);
3148
+ await assertPinnedDirectory(directory);
3149
+ const moved = join(directory.resolved, `.${basename(path)}.${randomBytes(6).toString("hex")}.${suffix}`);
3150
+ try {
3151
+ await rename(path, moved);
3152
+ } catch (error) {
3153
+ if (error.code === "ENOENT") return null;
3154
+ throw error;
3155
+ }
3156
+ const bytes = await regularFileBytes(moved);
3157
+ if (bytes !== null && sha256(bytes) === digest) return moved;
3158
+ 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." });
3159
+ return null;
3160
+ }
3161
+ /** Remove only the exact object whose bytes prove it is still owned. */
3162
+ async function removeOwned(directory, path, digest, before) {
3163
+ const moved = await moveOwned(directory, path, digest, "remove", before);
3164
+ if (moved === null) return false;
3165
+ await assertPinnedDirectory(directory);
3166
+ await rm(moved);
3167
+ return true;
3168
+ }
3169
+ function normalPath(path, platform) {
3170
+ const trimmed = path.replace(/[\\/]+$/, "");
3171
+ return platform === "win32" ? trimmed.toLowerCase() : trimmed;
3172
+ }
3173
+ function directoryOnPath(context, directory) {
3174
+ const platform = platformOf(context);
3175
+ const value = context.env.PATH ?? context.env.Path ?? "";
3176
+ const separator = platform === "win32" ? ";" : ":";
3177
+ return value.split(separator).filter(Boolean).some((part) => normalPath(part, platform) === normalPath(directory, platform));
3178
+ }
3179
+ function pathNotice(directory) {
3180
+ return `The shim directory ${directory} is not on PATH. For this shell, run: export PATH=${posixWord(directory)}:"$PATH". No shell profile was edited.`;
3181
+ }
3182
+ /** Create or repoint one shim, but only while its target is free or still matches this installation's receipt. */
3183
+ async function writeManagedShim(context, spec) {
3184
+ checkedBinary(spec.binary);
3185
+ const directory = await pinShimDirectory(context, true);
3186
+ if (directory === null) throw new CommsError("UNEXPECTED", "the managed shim directory was not created");
3187
+ const publicPath = join(directory.public, spec.binary);
3188
+ const path = join(directory.resolved, spec.binary);
3189
+ const result = (status) => {
3190
+ const onPath = directoryOnPath(context, directory.public);
3191
+ return {
3192
+ path: publicPath,
3193
+ directory: directory.public,
3194
+ status,
3195
+ onPath,
3196
+ ...onPath ? {} : { pathNotice: pathNotice(directory.public) }
3197
+ };
3198
+ };
3199
+ return withMutationLock(context, async () => {
3200
+ const receipt = await readReceipt(context);
3201
+ const recorded = receipt?.shims[spec.binary];
3202
+ const current = await regularFileBytes(path);
3203
+ const owned = receipt !== null && recorded !== void 0 && recorded.path === path && current !== null && sha256(current) === recorded.digest;
3204
+ if (await pathExists(path) && !owned) return result("foreign");
3205
+ if (receipt === null || recorded !== void 0 && recorded.path !== path) return result("foreign");
3206
+ const node = spec.command[0];
3207
+ if (node === void 0) throw new CommsError("CONFIG", "the managed shim command has no program");
3208
+ const source = shimSource(spec);
3209
+ const digest = sha256(source);
3210
+ let previous = null;
3211
+ if (owned) {
3212
+ previous = await moveOwned(directory, path, recorded.digest, "old", context.hooks?.beforeReplace);
3213
+ if (previous === null) return result("foreign");
3214
+ }
3215
+ try {
3216
+ await createShimExclusive(directory, path, source);
3217
+ } catch (error) {
3218
+ if (previous !== null) {
3219
+ 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}`, {
3220
+ hint: "Resolve the two files by hand, then run the managed install again.",
3221
+ cause: error
3222
+ });
3223
+ }
3224
+ if (error.code === "EEXIST") return result("foreign");
3225
+ throw error;
3226
+ }
3227
+ receipt.shims[spec.binary] = {
3228
+ path,
3229
+ digest,
3230
+ node,
3231
+ entry: spec.entry
3232
+ };
3233
+ try {
3234
+ await assertPinnedDirectory(directory);
3235
+ await writeReceipt(context, receipt);
3236
+ } catch (error) {
3237
+ const removed = await removeOwned(directory, path, digest);
3238
+ 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}`, {
3239
+ hint: "Restore that file to the reported shim path, then run the managed install again.",
3240
+ cause: error
3241
+ });
3242
+ throw error;
3243
+ }
3244
+ if (previous !== null) {
3245
+ await assertPinnedDirectory(directory);
3246
+ await rm(previous);
3247
+ }
3248
+ return result(owned ? "updated" : "created");
3249
+ });
3250
+ }
3251
+ function isWithin(root, path) {
3252
+ const child = relative(root, path);
3253
+ return child === "" || !child.startsWith("..") && !isAbsolute(child);
3254
+ }
3255
+ /** Remove unchanged owned shims whose recorded entry is inside one of the runtimes prune removed. */
3256
+ async function removeManagedShimsForRuntimes(context, runtimes, options = {}) {
3257
+ if (!managedShimsSupported(context)) return {
3258
+ removed: [],
3259
+ kept: []
3260
+ };
3261
+ const directory = await pinShimDirectory(context, false);
3262
+ if (directory === null) return {
3263
+ removed: [],
3264
+ kept: []
3265
+ };
3266
+ return withMutationLock(context, async () => {
3267
+ const receipt = await readReceipt(context);
3268
+ if (receipt === null) return {
3269
+ removed: [],
3270
+ kept: []
3271
+ };
3272
+ const removed = [];
3273
+ const kept = [];
3274
+ let changed = false;
3275
+ for (const [binary, recorded] of Object.entries(receipt.shims)) {
3276
+ if (!runtimes.some((runtime) => isWithin(runtime, recorded.entry))) continue;
3277
+ const publicPath = join(directory.public, binary);
3278
+ const path = join(directory.resolved, binary);
3279
+ if (recorded.path !== path) {
3280
+ kept.push(recorded.path);
3281
+ continue;
3282
+ }
3283
+ if (options.dryRun) {
3284
+ const bytes = await regularFileBytes(path);
3285
+ if (bytes !== null && sha256(bytes) === recorded.digest) removed.push(publicPath);
3286
+ else if (await pathExists(path)) kept.push(publicPath);
3287
+ continue;
3288
+ }
3289
+ if (await removeOwned(directory, path, recorded.digest, context.hooks?.beforeRemove)) {
3290
+ delete receipt.shims[binary];
3291
+ changed = true;
3292
+ removed.push(publicPath);
3293
+ } else if (!await pathExists(path)) {
3294
+ delete receipt.shims[binary];
3295
+ changed = true;
3296
+ } else kept.push(publicPath);
3297
+ }
3298
+ if (changed) {
3299
+ await assertPinnedDirectory(directory);
3300
+ await writeReceipt(context, receipt);
3301
+ }
3302
+ return {
3303
+ removed,
3304
+ kept
3305
+ };
3306
+ });
3307
+ }
2919
3308
  /**
2920
3309
  * The programs this package starts, named so that no folder it happens to be in can stand in for them.
2921
3310
  *
@@ -4032,8 +4421,8 @@ async function mergeIntoJsonConfig(path, client, name, entry) {
4032
4421
  }
4033
4422
  /** Starts the server exactly as a client would, and completes the handshake. */
4034
4423
  async function verifyEntry(entry, product) {
4035
- const { Client } = await import("./dist-COPefvtq-COGS2a-K.mjs");
4036
- const { StdioClientTransport } = await import("./stdio-CHZ2WRcx-B2_wGdA5.mjs");
4424
+ const { Client } = await import("./dist-C7DG2LdL-l0mj9_dH.mjs");
4425
+ const { StdioClientTransport } = await import("./stdio-CyjIgrch-59zcjSEt.mjs");
4037
4426
  const client = new Client({
4038
4427
  name: `${product.binary}-install-check`,
4039
4428
  version: product.version
@@ -4088,6 +4477,14 @@ function commandLinesFrom(ps) {
4088
4477
  });
4089
4478
  }
4090
4479
  const RUNTIME_NAME = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]*)?$/;
4480
+ /** Whether a runtime's own manifest, the one the installer writes, names this package as what it installs. */
4481
+ async function pinsPackage(runtime, packageName) {
4482
+ try {
4483
+ return typeof JSON.parse(await readFile(join(runtime, "package.json"), "utf8")).dependencies?.[packageName] === "string";
4484
+ } catch {
4485
+ return false;
4486
+ }
4487
+ }
4091
4488
  /**
4092
4489
  * This product's managed runtimes on disk, oldest name first: every directory directly inside `<data>/runtime`,
4093
4490
  * named like a version, that holds this product's package — never a symbolic link, never another product's runtime,
@@ -4116,6 +4513,7 @@ async function listManagedRuntimes(dataDir, packageName) {
4116
4513
  } catch {
4117
4514
  continue;
4118
4515
  }
4516
+ if (!name.endsWith(suffix) && !await pinsPackage(path, packageName)) continue;
4119
4517
  found.push({
4120
4518
  path,
4121
4519
  version: name.endsWith(suffix) ? name.slice(0, -suffix.length) : name
@@ -4158,7 +4556,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
4158
4556
  runtimeDir: join(context.core.paths.dataDir, "runtime"),
4159
4557
  dryRun: options.dryRun === true,
4160
4558
  removed: [],
4161
- kept: []
4559
+ kept: [],
4560
+ shims: {
4561
+ removed: [],
4562
+ kept: []
4563
+ }
4162
4564
  };
4163
4565
  const candidates = [];
4164
4566
  for (const { path, version } of await listManagedRuntimes(context.core.paths.dataDir, product.packageName)) {
@@ -4225,6 +4627,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
4225
4627
  const left = handedOut.filter((record) => !gone.some((path) => mentions([path], record.runtime)));
4226
4628
  if (left.length !== handedOut.length) await writeFileAtomic(ledger, left.map((record) => `${JSON.stringify(record)}\n`).join(""));
4227
4629
  }
4630
+ if (result.removed.length > 0) result.shims = await removeManagedShimsForRuntimes({
4631
+ env: context.env,
4632
+ dataDir: context.core.paths.dataDir,
4633
+ platform: context.platform
4634
+ }, result.removed.map((item) => item.path), { dryRun: result.dryRun });
4228
4635
  return result;
4229
4636
  }
4230
4637
  /** Every runtime the installer handed out, or null when the record is there and cannot be read. */
@@ -4357,6 +4764,8 @@ function rivalWordWarnings(servers, word, can, product) {
4357
4764
  }
4358
4765
  /** Every channel in core's snapshot, the core first. */
4359
4766
  const CHANNELS = Object.freeze(CHANNEL_SNAPSHOT.map((entry) => entry.manifest.channel));
4767
+ /** Every declared MCP server, channels first and then services, with no separate hand-kept list. */
4768
+ const SERVERS = Object.freeze([...CHANNELS, ...SERVICE_SNAPSHOT.map((entry) => entry.declaration.server.defaultName)]);
4360
4769
  /**
4361
4770
  * The value a pin's flag carries in an entry's arguments: `--account acme/resend`, or `--account=acme/resend` — the
4362
4771
  * first given, in either form.
@@ -4417,8 +4826,28 @@ function serverFactsOf({ packageName, manifest }) {
4417
4826
  };
4418
4827
  }
4419
4828
  const CHANNEL_SERVERS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, serverFactsOf(entry)])));
4829
+ /** Service server facts, derived from each service package's declaration. */
4830
+ const SERVICE_SERVERS = Object.freeze(Object.fromEntries(SERVICE_SNAPSHOT.map(({ packageName, declaration }) => [declaration.server.defaultName, {
4831
+ packageName,
4832
+ binary: declaration.binary,
4833
+ defaultServerName: declaration.server.defaultName,
4834
+ npxPackage: packageName,
4835
+ npxArgs: ["mcp"],
4836
+ serverArgs: () => [],
4837
+ narrowingOf: () => ({})
4838
+ }])));
4839
+ /** All managed servers, derived from channel manifests and service declarations. */
4840
+ const SERVERS_BY_ID = Object.freeze({
4841
+ ...CHANNEL_SERVERS,
4842
+ ...SERVICE_SERVERS
4843
+ });
4420
4844
  /** How each server is named to a person: in a preview, and in what a tool returns. */
4421
4845
  const CHANNEL_LABELS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, entry.manifest.label])));
4846
+ /** Labels for every managed server; a service declares its public server name. */
4847
+ const SERVER_LABELS = Object.freeze({
4848
+ ...CHANNEL_LABELS,
4849
+ ...Object.fromEntries(SERVICE_SNAPSHOT.map(({ declaration }) => [declaration.server.defaultName, declaration.server.defaultName]))
4850
+ });
4422
4851
  /** A channel's manifest, from core's snapshot. */
4423
4852
  function channelManifest(channel) {
4424
4853
  return CHANNEL_SNAPSHOT.find((entry) => entry.manifest.channel === channel)?.manifest;
@@ -4426,19 +4855,32 @@ function channelManifest(channel) {
4426
4855
  function isChannel(value) {
4427
4856
  return typeof value === "string" && CHANNELS.includes(value);
4428
4857
  }
4858
+ /** Whether a word identifies any channel or service server in this release. */
4859
+ function isServer(value) {
4860
+ return typeof value === "string" && SERVERS.includes(value);
4861
+ }
4429
4862
  /** Refuses a word that is not a channel of this release, naming the ones that are. */
4430
4863
  function notAChannel(value) {
4431
4864
  return new CommsError("USAGE", `"${String(value)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
4432
4865
  }
4866
+ /** Refuses a word that is not a declared server of this release. */
4867
+ function notAServer(value) {
4868
+ return new CommsError("USAGE", `"${String(value)}" is not a server`, { hint: `One of: ${SERVERS.join(", ")}.` });
4869
+ }
4433
4870
  /** A channel's server facts, by its word; refused for a word that is not a channel. */
4434
4871
  function channelServer(channel) {
4435
4872
  if (!isChannel(channel)) throw notAChannel(channel);
4436
4873
  return CHANNEL_SERVERS[channel];
4437
4874
  }
4438
- /** How a channel's server is named to a person, by its word; refused for a word that is not a channel. */
4439
- function channelLabel(channel) {
4440
- if (!isChannel(channel)) throw notAChannel(channel);
4441
- return CHANNEL_LABELS[channel];
4875
+ /** A channel or service's server facts, by its declared server name. */
4876
+ function serverFacts(server) {
4877
+ if (!isServer(server)) throw notAServer(server);
4878
+ return SERVERS_BY_ID[server];
4879
+ }
4880
+ /** How a channel or service server is named to a person. */
4881
+ function serverLabel(server) {
4882
+ if (!isServer(server)) throw notAServer(server);
4883
+ return SERVER_LABELS[server];
4442
4884
  }
4443
4885
  /** A channel's manifest, by its word; refused for a word that is not a channel. */
4444
4886
  function requireChannelManifest(channel) {
@@ -8862,7 +9304,7 @@ function folding(platform) {
8862
9304
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
8863
9305
  }
8864
9306
  /** The package version, read from package.json at build time. */
8865
- const VERSION = "0.17.0";
9307
+ const VERSION = "0.18.1";
8866
9308
  /**
8867
9309
  * Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
8868
9310
  * to decide whether the release it last heard of is newer than the one running.
@@ -8998,15 +9440,18 @@ var PrintedCommand = class {
8998
9440
  #entry;
8999
9441
  /** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
9000
9442
  words;
9001
- /** The line to paste; null when one of the words has no printing that every Windows shell passes on alike. */
9443
+ /** The line to paste; null when no supported common or labelled shell rendering is safe. */
9002
9444
  line;
9003
9445
  platform;
9446
+ shell;
9004
9447
  constructor(gate, words, platform, entry) {
9005
9448
  if (gate !== GATE) throw new TypeError("a printed command is made by locateCliCommand, and only there");
9006
9449
  this.#entry = entry;
9007
9450
  this.words = Object.freeze([...words]);
9008
- this.line = quoteCommand(this.words, platform).line;
9451
+ const quoted = quoteCommand(this.words, platform);
9452
+ this.line = quoted.line;
9009
9453
  this.platform = platform;
9454
+ if (quoted.shell !== void 0) this.shell = quoted.shell;
9010
9455
  Object.freeze(this);
9011
9456
  }
9012
9457
  /**
@@ -9033,7 +9478,7 @@ const STRIP_TYPES = "--experimental-strip-types";
9033
9478
  const TRANSFORM_TYPES = "--experimental-transform-types";
9034
9479
  /** The suite's packages, by name, and its channel packages, which reach core through their own dependency on it. */
9035
9480
  const SUITE_PACKAGES = new Set(CHANNEL_SNAPSHOT.map(({ packageName }) => packageName));
9036
- const CHANNEL_PACKAGES$1 = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
9481
+ const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
9037
9482
  /**
9038
9483
  * Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
9039
9484
  *
@@ -9116,7 +9561,7 @@ function locateCliCommand(request, runtime = {
9116
9561
  sameCheckout: true
9117
9562
  });
9118
9563
  }
9119
- if (target.manifest.channel === "core" && CHANNEL_PACKAGES$1.has(caller.packageName)) {
9564
+ if (target.manifest.channel === "core" && CHANNEL_PACKAGES.has(caller.packageName)) {
9120
9565
  const installed = installedPackage(dirname(callerFile), target.packageName);
9121
9566
  if (installed === null) return fail("core", `${caller.packageName} has no ${target.packageName} installed where it can find one`);
9122
9567
  const manifest = readPackageManifest(installed);
@@ -9144,7 +9589,9 @@ function locateCliCommand(request, runtime = {
9144
9589
  }
9145
9590
  if (!SUITE_PACKAGES.has(caller.packageName)) return fail("direction", `${caller.packageName} is not a package of this suite`);
9146
9591
  if (request.registrations === void 0) return fail("no-registrations", "nothing registered was given to look through for another product's command");
9147
- const facts = registrationFactsOf(target);
9592
+ const channelTarget = CHANNEL_SNAPSHOT.find((entry) => entry.packageName === target.packageName);
9593
+ if (channelTarget === void 0) return fail("direction", `${target.packageName} is not a channel package this locator can find through a registration`);
9594
+ const facts = registrationFactsOf(channelTarget);
9148
9595
  const notUsed = [];
9149
9596
  const usable = [];
9150
9597
  let engineRange;
@@ -9242,8 +9689,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
9242
9689
  let entry;
9243
9690
  if (found.source) entry = join(found.root, "src", "cli.ts");
9244
9691
  else {
9245
- const declared = found.manifest.agentcomms;
9246
- const binary = typeof declared === "object" && declared !== null ? declared.binary : void 0;
9692
+ const binary = declaredBinary(found.manifest);
9247
9693
  const bin = found.manifest.bin;
9248
9694
  const path = binary === target.manifest.binary && typeof bin === "object" && bin !== null && Object.hasOwn(bin, target.manifest.binary) ? bin[target.manifest.binary] : void 0;
9249
9695
  if (typeof path !== "string" || path.length === 0) return {
@@ -9272,6 +9718,16 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
9272
9718
  };
9273
9719
  }
9274
9720
  /**
9721
+ * The command a package declares for itself: a channel's in its `agentcomms` field, a service's in `agentcommsPackage`
9722
+ * (D14). A library declares none, and no package declares both: the registry refuses one that does.
9723
+ */
9724
+ function declaredBinary(manifest) {
9725
+ const channel = manifest.agentcomms;
9726
+ if (typeof channel === "object" && channel !== null) return channel.binary;
9727
+ const service = manifest.agentcommsPackage;
9728
+ if (typeof service === "object" && service !== null && service.kind === "service") return service.binary;
9729
+ }
9730
+ /**
9275
9731
  * The target's package a registration's files lead to, or null: a script it starts, real or as written when it is
9276
9732
  * gone, inside the package (or inside the product's own server wrapper, which depends on it); or, for the product's
9277
9733
  * own command, the package that command links into or that npm installed beside it.
@@ -9403,7 +9859,7 @@ function notLocated(target, version, reason, detail, nodeVersion, extra) {
9403
9859
  * anywhere else for the product, PATH included, and a later locate checks the path again.
9404
9860
  */
9405
9861
  function checkedEntry(root, entry) {
9406
- if (!isWithin(root, entry)) return {
9862
+ if (!isWithin$1(root, entry)) return {
9407
9863
  ok: false,
9408
9864
  why: `${entry} is outside its package, ${root}`
9409
9865
  };
@@ -9416,7 +9872,7 @@ function checkedEntry(root, entry) {
9416
9872
  why: `${entry} is not there`
9417
9873
  };
9418
9874
  }
9419
- if (!isWithin(root, real)) return {
9875
+ if (!isWithin$1(root, real)) return {
9420
9876
  ok: false,
9421
9877
  why: `${entry} leads to ${real}, outside its package, ${root}`
9422
9878
  };
@@ -9751,6 +10207,10 @@ function renderInstall(result, color) {
9751
10207
  lines.push(paint(color, "bold", `Add this to ${result.configPath ?? `the MCP configuration of ${result.client}`}:`), result.snippet.trimEnd());
9752
10208
  }
9753
10209
  if (result.backupPath) lines.push(`The entry it replaced was saved to ${result.backupPath}.`);
10210
+ if (result.shim && result.shim.status !== "foreign") lines.push(`CLI shim ${result.shim.status}: ${result.shim.path}.`);
10211
+ if (result.shimFailure) lines.push(paint(color, "yellow", `CLI shim failed: ${result.shimFailure}.`));
10212
+ if (result.shimNotice) lines.push(paint(color, "yellow", result.shimNotice));
10213
+ if (result.shim?.pathNotice) lines.push(result.shim.pathNotice);
9754
10214
  if (result.verification === "passed") lines.push(paint(color, "green", `Checked: ${result.verifyDetail}`));
9755
10215
  else if (result.verification === "failed") {
9756
10216
  lines.push(paint(color, "red", `Failed to start: ${result.verifyDetail ?? "no reason given"}`));
@@ -9765,6 +10225,8 @@ function renderPrune(result, color) {
9765
10225
  if (result.refused) lines.push(paint(color, "yellow", `Nothing removed: ${result.refused}.`));
9766
10226
  const verb = result.dryRun ? "Would remove" : "Removed";
9767
10227
  for (const item of result.removed) lines.push(paint(color, "green", `${verb} ${item.version}: ${item.path}`));
10228
+ for (const path of result.shims.removed) lines.push(paint(color, "green", `${verb} CLI shim: ${path}`));
10229
+ for (const path of result.shims.kept) lines.push(`Kept foreign CLI shim: ${path}`);
9768
10230
  for (const item of result.kept) lines.push(`Kept ${item.version} (${item.reason}): ${item.path}`);
9769
10231
  if (lines.length === 0) lines.push(`No managed runtimes to remove in ${result.runtimeDir}.`);
9770
10232
  return lines.join("\n");
@@ -10857,10 +11319,10 @@ strictObject({
10857
11319
  sha256: string().regex(/^[0-9a-f]{64}$/, "a SHA-256 is 64 lowercase hex digits")
10858
11320
  });
10859
11321
  /**
10860
- * Registering, pruning and listing the MCP servers of every channel, from the core.
11322
+ * Registering, pruning and listing every declared channel or service MCP server, from the core.
10861
11323
  *
10862
- * `agentcomms mcp install` registers the core server; `comms_server_install` registers any channel's; and
10863
- * `agent-gmail mcp install` and `agent-slack mcp install` register their own. All four are the one change here, over
11324
+ * `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
11325
+ * server; and channel CLIs register their own. They are the one change here, over
10864
11326
  * the shared installer (`mcp-install.ts`), and all go through the change flow: registering a server is a loosening in
10865
11327
  * 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
10866
11328
  * second call with the approval. Printing an entry registers nothing and asks nobody.
@@ -10871,7 +11333,7 @@ strictObject({
10871
11333
  * the other — or worse, a channel's own command would register with no approval at all, which is what it did.
10872
11334
  *
10873
11335
  * The version installed is this core's own, or the calling channel's when it passes its product. The packages are
10874
- * released together, at one version, so the core that is asked to install Gmail installs the Gmail of its own release.
11336
+ * released together, at one version, so the core asked to install Gmail or events installs its own release.
10875
11337
  */
10876
11338
  const CLIENTS = Object.freeze([
10877
11339
  "claude-code",
@@ -10912,10 +11374,10 @@ const exists = (path) => access(path, constants.R_OK).then(() => true, () => fal
10912
11374
  * core depends on none of them. So the channel is looked for beside this core — `packages/gmail` next to
10913
11375
  * `packages/core` in a checkout — and from source when this core runs from source, as the channel CLIs do.
10914
11376
  */
10915
- async function localModuleUrl(channel) {
11377
+ async function localModuleUrl(server) {
10916
11378
  const coreRoot = await corePackageRoot();
10917
- const unscopedName = channelServer(channel).packageName.split("/").at(-1) ?? channel;
10918
- const root = channel === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
11379
+ const unscopedName = serverFacts(server).packageName.split("/").at(-1) ?? server;
11380
+ const root = server === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
10919
11381
  const source = join(root, "src", "cli.ts");
10920
11382
  const built = join(root, "dist", "cli.mjs");
10921
11383
  const fromSource = fileURLToPath(import.meta.url).endsWith(".ts");
@@ -10923,7 +11385,7 @@ async function localModuleUrl(channel) {
10923
11385
  if (!await exists(cli)) continue;
10924
11386
  return pathToFileURL(cli === source ? join(root, "src", "mcp", "module.ts") : join(root, "dist", "module.mjs")).href;
10925
11387
  }
10926
- 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.` });
11388
+ 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.` });
10927
11389
  }
10928
11390
  /**
10929
11391
  * The product the shared installer registers, for one channel: at this core's version unless another is named.
@@ -10933,11 +11395,11 @@ async function localModuleUrl(channel) {
10933
11395
  * newer release here is all it takes to register one this core is older than — which is what `agentcomms update` does.
10934
11396
  * `comms_server_install` and every `mcp install` name none, and register their own release as they always have.
10935
11397
  */
10936
- async function channelProduct(channel, launcher, version = VERSION) {
11398
+ async function serverProduct(server, launcher, version = VERSION) {
10937
11399
  return {
10938
- ...channelServer(channel),
11400
+ ...serverFacts(server),
10939
11401
  version,
10940
- moduleUrl: launcher === "local" ? await localModuleUrl(channel) : ""
11402
+ moduleUrl: launcher === "local" ? await localModuleUrl(server) : ""
10941
11403
  };
10942
11404
  }
10943
11405
  /**
@@ -10950,8 +11412,8 @@ async function channelProduct(channel, launcher, version = VERSION) {
10950
11412
  * registered, from the channel; a product for another package would register something else under those words, and
10951
11413
  * the person would have approved a sentence that was not true.
10952
11414
  */
10953
- function ownProduct(channel, own) {
10954
- 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`);
11415
+ function ownProduct(server, own) {
11416
+ 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`);
10955
11417
  return own;
10956
11418
  }
10957
11419
  /**
@@ -10961,11 +11423,11 @@ function ownProduct(channel, own) {
10961
11423
  * register a server reaching every workspace for a caller who believed they had pinned it.
10962
11424
  */
10963
11425
  function checkRequest(request) {
10964
- if (!isChannel(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
11426
+ if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
10965
11427
  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(", ")}.` });
10966
11428
  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(", ")}.` });
10967
11429
  if (request.name !== void 0) checkServerName(request.name);
10968
- const label = channelLabel(request.channel);
11430
+ const label = serverLabel(request.channel);
10969
11431
  const refuse = (option) => {
10970
11432
  const owner = narrowingOwner(option);
10971
11433
  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`);
@@ -11001,7 +11463,7 @@ function checkPin(config, request) {
11001
11463
  return;
11002
11464
  }
11003
11465
  const { account } = resolveName(config, "account", pin);
11004
- if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${channelLabel(request.channel)} ${accountNoun(request.channel)}`);
11466
+ if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${serverLabel(request.channel)} ${accountNoun(request.channel)}`);
11005
11467
  }
11006
11468
  function installOptions(request) {
11007
11469
  const own = pinOption(request.channel);
@@ -11020,7 +11482,7 @@ function installOptions(request) {
11020
11482
  };
11021
11483
  }
11022
11484
  /**
11023
- * Registering one channel's server with one client, as a change.
11485
+ * Registering one declared server with one client, as a change.
11024
11486
  *
11025
11487
  * The effects are what a person is agreeing to, one sentence each, and everything that decides what the server may
11026
11488
  * reach is in them — which server, which client, under which name, pinned to what, replacing what, started how. The
@@ -11035,21 +11497,22 @@ function installOptions(request) {
11035
11497
  * resolves another — `CLAUDE_CONFIG_DIR` set to another account's, a PATH that finds another node, a core running
11036
11498
  * from another build of the checkout — is another registration, refused as one.
11037
11499
  */
11038
- function serverInstallChange(core, env, request, own) {
11500
+ function serverInstallChange(core, env, request, own, deps = {}) {
11039
11501
  checkRequest(request);
11040
11502
  ownProduct(request.channel, own);
11041
11503
  const context = {
11042
11504
  env,
11043
- core
11505
+ core,
11506
+ ...deps.platform === void 0 ? {} : { platform: deps.platform }
11044
11507
  };
11045
- const facts = channelServer(request.channel);
11046
- const label = channelLabel(request.channel);
11508
+ const facts = serverFacts(request.channel);
11509
+ const label = serverLabel(request.channel);
11047
11510
  const noun = accountNoun(request.channel);
11048
11511
  const pinName = pinOption(request.channel);
11049
11512
  const switchable = hasNarrowing(request.channel, "readOnly");
11050
11513
  const name = request.name ?? facts.defaultServerName;
11051
11514
  const launcher = request.launcher ?? "managed";
11052
- const productNow = async () => own ?? await channelProduct(request.channel, request.launcher);
11515
+ const productNow = async () => own ?? await serverProduct(request.channel, request.launcher);
11053
11516
  let planned;
11054
11517
  return {
11055
11518
  plan: async (config) => {
@@ -11080,6 +11543,14 @@ function serverInstallChange(core, env, request, own) {
11080
11543
  else if (launcher === "local") effects.push(`${request.client} will start it from ${await localCliEntry(product.moduleUrl)}`);
11081
11544
  }
11082
11545
  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)}`);
11546
+ if (request.print !== true && launcher === "managed" && managedShimsSupported(context)) {
11547
+ const path = managedShimPath({
11548
+ env,
11549
+ dataDir: core.paths.dataDir,
11550
+ platform: context.platform
11551
+ }, facts.binary);
11552
+ effects.push(`writes or repoints the owned CLI shim at ${path}; a foreign file there is left unchanged`);
11553
+ }
11083
11554
  return {
11084
11555
  before: config,
11085
11556
  after: config,
@@ -11089,9 +11560,10 @@ function serverInstallChange(core, env, request, own) {
11089
11560
  },
11090
11561
  apply: async () => {
11091
11562
  if (planned === void 0) throw new CommsError("UNEXPECTED", "the registration was applied before it was planned");
11092
- const result = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
11563
+ const installed = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
11093
11564
  throw await remedyLocated(error, core);
11094
11565
  });
11566
+ const result = request.print === true ? installed : await withManagedShim(context, planned.product, installed);
11095
11567
  return {
11096
11568
  ...result,
11097
11569
  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
@@ -11100,6 +11572,68 @@ function serverInstallChange(core, env, request, own) {
11100
11572
  };
11101
11573
  }
11102
11574
  /**
11575
+ * A managed install's CLI shim (CUE-406), once the registration's outcome is known: the command the locator finds in
11576
+ * the runtime the entry starts, written where nothing is or over this installation's own unchanged shim, and never
11577
+ * over anything else. A shim that cannot be written leaves the registration as it went, and says how to repair both.
11578
+ * The installer may not import the locator, so the operation writes it — for `mcp install` and `update` alike.
11579
+ */
11580
+ async function withManagedShim(context, product, installed) {
11581
+ if (installed.launcher !== "managed") return installed;
11582
+ const cliEntry = installed.entry.args[0];
11583
+ if (cliEntry === void 0) throw new CommsError("UNEXPECTED", "the managed entry has no CLI path");
11584
+ const shimContext = {
11585
+ env: context.env,
11586
+ dataDir: context.core.paths.dataDir,
11587
+ platform: context.platform
11588
+ };
11589
+ if (!managedShimsSupported(shimContext)) return {
11590
+ ...installed,
11591
+ shimNotice: managedShimUnsupportedNotice(shimContext)
11592
+ };
11593
+ const warnings = [...installed.warnings];
11594
+ let shim;
11595
+ let shimFailure;
11596
+ try {
11597
+ const located = locateCliCommand({
11598
+ caller: {
11599
+ url: pathToFileURL(cliEntry).href,
11600
+ packageName: product.packageName
11601
+ },
11602
+ target: {
11603
+ packageName: product.packageName,
11604
+ manifest: {
11605
+ label: product.defaultServerName,
11606
+ binary: product.binary
11607
+ }
11608
+ },
11609
+ words: [],
11610
+ uses: [],
11611
+ paths: {
11612
+ ...context.core.paths,
11613
+ downloadsDir: context.core.paths.dataDir
11614
+ },
11615
+ platform: context.platform
11616
+ });
11617
+ if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
11618
+ shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
11619
+ binary: product.binary,
11620
+ command: located.command.words,
11621
+ entry: located.command.entry
11622
+ });
11623
+ } catch (error) {
11624
+ shimFailure = error instanceof Error ? error.message : String(error);
11625
+ const path = join(homeDirectory(context.env), ".local", "bin", product.binary);
11626
+ 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.`);
11627
+ }
11628
+ 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.`);
11629
+ return {
11630
+ ...installed,
11631
+ warnings,
11632
+ ...shim === void 0 ? {} : { shim },
11633
+ ...shimFailure === void 0 ? {} : { shimFailure }
11634
+ };
11635
+ }
11636
+ /**
11103
11637
  * A refusal of `mcp install` with its remedy — another install of the same product — said with the command located:
11104
11638
  * the printing package's own, core's, or another product's found among this machine's registrations (CUE-403). The
11105
11639
  * installer says only the words; it may not import the locator.
@@ -11107,17 +11641,24 @@ function serverInstallChange(core, env, request, own) {
11107
11641
  async function remedyLocated(error, core) {
11108
11642
  const remedy = installRemedyOf(error);
11109
11643
  if (remedy === void 0 || !(error instanceof CommsError)) return error;
11110
- const channel = CHANNELS.find((each) => channelServer(each).binary === remedy.binary);
11111
- if (channel === void 0) return error;
11644
+ const server = SERVERS.find((each) => serverFacts(each).binary === remedy.binary);
11645
+ if (server === void 0) return error;
11112
11646
  const maker = await requireHandoffs(core).registered();
11647
+ const handoff = isChannel(server) ? maker.of(server, remedy.words) : maker.core([
11648
+ "mcp",
11649
+ "install",
11650
+ "--server",
11651
+ server,
11652
+ ...remedy.words.slice(2)
11653
+ ]);
11113
11654
  return new CommsError(error.code, error.message, {
11114
- hint: handoffSentence(maker.of(channel, remedy.words), remedy.say),
11655
+ hint: handoffSentence(handoff, remedy.say),
11115
11656
  ...error.details === void 0 ? {} : { details: error.details },
11116
11657
  ...error.cause === void 0 ? {} : { cause: error.cause }
11117
11658
  });
11118
11659
  }
11119
11660
  /**
11120
- * Removing a channel's unused managed runtimes, as a change.
11661
+ * Removing a declared server's unused managed runtimes, as a change.
11121
11662
  *
11122
11663
  * A dry run is free: it removes nothing. Otherwise the plan is that same dry run, and each runtime it would remove is
11123
11664
  * one effect — the removal cannot be taken back — so the approval is bound to exactly that list; and the removal is
@@ -11127,20 +11668,20 @@ async function remedyLocated(error, core) {
11127
11668
  * release of the command that was run; checked to be that channel's, as for an install.
11128
11669
  */
11129
11670
  function serverPruneChange(core, env, request, own) {
11130
- if (!isChannel(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
11671
+ if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
11131
11672
  const context = {
11132
11673
  env,
11133
11674
  core
11134
11675
  };
11135
11676
  const { packageName, version } = ownProduct(request.channel, own) ?? {
11136
- packageName: channelServer(request.channel).packageName,
11137
- version: "0.17.0"
11677
+ packageName: serverFacts(request.channel).packageName,
11678
+ version: "0.18.1"
11138
11679
  };
11139
11680
  const product = {
11140
11681
  packageName,
11141
11682
  version
11142
11683
  };
11143
- const label = channelLabel(request.channel);
11684
+ const label = serverLabel(request.channel);
11144
11685
  const base = {
11145
11686
  includePrinted: request.includePrinted === true,
11146
11687
  ...request.processes ? { processes: request.processes } : {}
@@ -11161,7 +11702,7 @@ function serverPruneChange(core, env, request, own) {
11161
11702
  return {
11162
11703
  before: config,
11163
11704
  after: config,
11164
- effects: preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`),
11705
+ 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}`)],
11165
11706
  summary: `Remove ${approved.length} unused ${label} runtime${approved.length === 1 ? "" : "s"}`
11166
11707
  };
11167
11708
  },
@@ -11434,13 +11975,14 @@ function updateStopMessage(pending, where, commands) {
11434
11975
  * the yes is a different change, and the claim is refused. Nothing behind, nothing is prepared.
11435
11976
  */
11436
11977
  /**
11437
- * The packages this suite publishes, whose latest releases an update reads: every channel's, from the manifests, and
11438
- * every package a channel's server is run through by `npx` when that is another one — Gmail's `gmail-mcp`.
11978
+ * The packages this suite publishes, whose latest releases an update reads: every declared server owner's, from the
11979
+ * channel and service declarations, and every package a server is run through by `npx` when that is another one —
11980
+ * Gmail's `gmail-mcp`.
11439
11981
  */
11440
- const CHANNEL_PACKAGES = Object.freeze(CHANNELS.map((channel) => channelServer(channel).packageName));
11441
- const SERVER_PACKAGES = Object.freeze(CHANNELS.map((channel) => channelServer(channel).npxPackage).filter((name) => !CHANNEL_PACKAGES.includes(name)));
11442
- Object.freeze([.../* @__PURE__ */ new Set([...CHANNEL_PACKAGES, ...SERVER_PACKAGES])].sort());
11443
- channelServer("core").packageName;
11982
+ const OWNER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).packageName));
11983
+ const SERVER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).npxPackage).filter((name) => !OWNER_PACKAGES.includes(name)));
11984
+ Object.freeze([.../* @__PURE__ */ new Set([...OWNER_PACKAGES, ...SERVER_PACKAGES])].sort());
11985
+ serverFacts("core").packageName;
11444
11986
  /**
11445
11987
  * "Not now": no server or command on this machine stops for the update until the next local midnight.
11446
11988
  *
@@ -11788,7 +12330,7 @@ async function askLine(streams, question) {
11788
12330
  }
11789
12331
  channelServer("core").packageName;
11790
12332
  //#endregion
11791
- //#region ../core/dist/tool-arguments-rfCRYwaZ.mjs
12333
+ //#region ../core/dist/tool-arguments-DLa_xsSp.mjs
11792
12334
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
11793
12335
  const TARGET = "draft-2020-12";
11794
12336
  /**
@@ -12096,10 +12638,60 @@ new TextEncoder();
12096
12638
  const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12097
12639
  const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
12098
12640
  for (let i = 0; i < 64; i++) base64Lookup[base64Chars.charCodeAt(i)] = i;
12641
+ new Set([
12642
+ "unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8",
12643
+ "866 cp866 csibm866 ibm866",
12644
+ "csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2",
12645
+ "csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3",
12646
+ "csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4",
12647
+ "csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988",
12648
+ "arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e",
12649
+ "iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987",
12650
+ "csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7",
12651
+ "iso_8859-7:1987 sun_eu_greek",
12652
+ "csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8",
12653
+ "iso_8859-8:1988 visual",
12654
+ "csiso88598i iso-8859-8-i logical",
12655
+ "csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6",
12656
+ "iso-8859-13 iso8859-13 iso885913",
12657
+ "iso-8859-14 iso8859-14 iso885914",
12658
+ "csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9",
12659
+ "iso-8859-16",
12660
+ "cskoi8r koi koi8 koi8-r koi8_r",
12661
+ "koi8-ru koi8-u",
12662
+ "csmacintosh mac macintosh x-mac-roman",
12663
+ "dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874",
12664
+ "cp1250 windows-1250 x-cp1250",
12665
+ "cp1251 windows-1251 x-cp1251",
12666
+ "ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591",
12667
+ "iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252",
12668
+ "cp1253 windows-1253 x-cp1253",
12669
+ "cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5",
12670
+ "windows-1254 x-cp1254",
12671
+ "cp1255 windows-1255 x-cp1255",
12672
+ "cp1256 windows-1256 x-cp1256",
12673
+ "cp1257 windows-1257 x-cp1257",
12674
+ "cp1258 windows-1258 x-cp1258",
12675
+ "x-mac-cyrillic x-mac-ukrainian",
12676
+ "chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk",
12677
+ "gb18030",
12678
+ "big5 big5-hkscs cn-big5 csbig5 x-x-big5",
12679
+ "cseucpkdfmtjapanese euc-jp x-euc-jp",
12680
+ "csiso2022jp iso-2022-jp",
12681
+ "csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis",
12682
+ "cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
12683
+ "windows-949",
12684
+ "csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
12685
+ "unicodefffe utf-16be",
12686
+ "csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
12687
+ "x-user-defined"
12688
+ ].join(" ").split(" "));
12689
+ new TextDecoder();
12099
12690
  const ENCODED_WORD_PATTERN = "=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=";
12100
12691
  new RegExp(ENCODED_WORD_PATTERN, "g");
12101
12692
  new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`);
12102
12693
  new TextDecoder("utf-8", { ignoreBOM: true });
12694
+ new Uint8Array([10]);
12103
12695
  //#endregion
12104
12696
  //#region ../../node_modules/.pnpm/deepmerge-ts@8.0.2/node_modules/deepmerge-ts/dist/index.mjs
12105
12697
  /**