@agentcomms/whatsapp 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.
@@ -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.16.0";
14
+ const VERSION$1 = "0.18.0";
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-23Ii90fQ.mjs
31
+ //#region ../core/dist/update-check-C_PW2LlM.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
@@ -4158,7 +4547,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
4158
4547
  runtimeDir: join(context.core.paths.dataDir, "runtime"),
4159
4548
  dryRun: options.dryRun === true,
4160
4549
  removed: [],
4161
- kept: []
4550
+ kept: [],
4551
+ shims: {
4552
+ removed: [],
4553
+ kept: []
4554
+ }
4162
4555
  };
4163
4556
  const candidates = [];
4164
4557
  for (const { path, version } of await listManagedRuntimes(context.core.paths.dataDir, product.packageName)) {
@@ -4225,6 +4618,11 @@ async function pruneManagedRuntimes(context, product, options = {}) {
4225
4618
  const left = handedOut.filter((record) => !gone.some((path) => mentions([path], record.runtime)));
4226
4619
  if (left.length !== handedOut.length) await writeFileAtomic(ledger, left.map((record) => `${JSON.stringify(record)}\n`).join(""));
4227
4620
  }
4621
+ if (result.removed.length > 0) result.shims = await removeManagedShimsForRuntimes({
4622
+ env: context.env,
4623
+ dataDir: context.core.paths.dataDir,
4624
+ platform: context.platform
4625
+ }, result.removed.map((item) => item.path), { dryRun: result.dryRun });
4228
4626
  return result;
4229
4627
  }
4230
4628
  /** Every runtime the installer handed out, or null when the record is there and cannot be read. */
@@ -4357,6 +4755,8 @@ function rivalWordWarnings(servers, word, can, product) {
4357
4755
  }
4358
4756
  /** Every channel in core's snapshot, the core first. */
4359
4757
  const CHANNELS = Object.freeze(CHANNEL_SNAPSHOT.map((entry) => entry.manifest.channel));
4758
+ /** Every declared MCP server, channels first and then services, with no separate hand-kept list. */
4759
+ const SERVERS = Object.freeze([...CHANNELS, ...SERVICE_SNAPSHOT.map((entry) => entry.declaration.server.defaultName)]);
4360
4760
  /**
4361
4761
  * The value a pin's flag carries in an entry's arguments: `--account acme/resend`, or `--account=acme/resend` — the
4362
4762
  * first given, in either form.
@@ -4417,8 +4817,28 @@ function serverFactsOf({ packageName, manifest }) {
4417
4817
  };
4418
4818
  }
4419
4819
  const CHANNEL_SERVERS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, serverFactsOf(entry)])));
4820
+ /** Service server facts, derived from each service package's declaration. */
4821
+ const SERVICE_SERVERS = Object.freeze(Object.fromEntries(SERVICE_SNAPSHOT.map(({ packageName, declaration }) => [declaration.server.defaultName, {
4822
+ packageName,
4823
+ binary: declaration.binary,
4824
+ defaultServerName: declaration.server.defaultName,
4825
+ npxPackage: packageName,
4826
+ npxArgs: ["mcp"],
4827
+ serverArgs: () => [],
4828
+ narrowingOf: () => ({})
4829
+ }])));
4830
+ /** All managed servers, derived from channel manifests and service declarations. */
4831
+ const SERVERS_BY_ID = Object.freeze({
4832
+ ...CHANNEL_SERVERS,
4833
+ ...SERVICE_SERVERS
4834
+ });
4420
4835
  /** How each server is named to a person: in a preview, and in what a tool returns. */
4421
4836
  const CHANNEL_LABELS = Object.freeze(Object.fromEntries(CHANNEL_SNAPSHOT.map((entry) => [entry.manifest.channel, entry.manifest.label])));
4837
+ /** Labels for every managed server; a service declares its public server name. */
4838
+ const SERVER_LABELS = Object.freeze({
4839
+ ...CHANNEL_LABELS,
4840
+ ...Object.fromEntries(SERVICE_SNAPSHOT.map(({ declaration }) => [declaration.server.defaultName, declaration.server.defaultName]))
4841
+ });
4422
4842
  /** A channel's manifest, from core's snapshot. */
4423
4843
  function channelManifest(channel) {
4424
4844
  return CHANNEL_SNAPSHOT.find((entry) => entry.manifest.channel === channel)?.manifest;
@@ -4426,19 +4846,32 @@ function channelManifest(channel) {
4426
4846
  function isChannel(value) {
4427
4847
  return typeof value === "string" && CHANNELS.includes(value);
4428
4848
  }
4849
+ /** Whether a word identifies any channel or service server in this release. */
4850
+ function isServer(value) {
4851
+ return typeof value === "string" && SERVERS.includes(value);
4852
+ }
4429
4853
  /** Refuses a word that is not a channel of this release, naming the ones that are. */
4430
4854
  function notAChannel(value) {
4431
4855
  return new CommsError("USAGE", `"${String(value)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
4432
4856
  }
4857
+ /** Refuses a word that is not a declared server of this release. */
4858
+ function notAServer(value) {
4859
+ return new CommsError("USAGE", `"${String(value)}" is not a server`, { hint: `One of: ${SERVERS.join(", ")}.` });
4860
+ }
4433
4861
  /** A channel's server facts, by its word; refused for a word that is not a channel. */
4434
4862
  function channelServer(channel) {
4435
4863
  if (!isChannel(channel)) throw notAChannel(channel);
4436
4864
  return CHANNEL_SERVERS[channel];
4437
4865
  }
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];
4866
+ /** A channel or service's server facts, by its declared server name. */
4867
+ function serverFacts(server) {
4868
+ if (!isServer(server)) throw notAServer(server);
4869
+ return SERVERS_BY_ID[server];
4870
+ }
4871
+ /** How a channel or service server is named to a person. */
4872
+ function serverLabel(server) {
4873
+ if (!isServer(server)) throw notAServer(server);
4874
+ return SERVER_LABELS[server];
4442
4875
  }
4443
4876
  /** A channel's manifest, by its word; refused for a word that is not a channel. */
4444
4877
  function requireChannelManifest(channel) {
@@ -8862,7 +9295,7 @@ function folding(platform) {
8862
9295
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
8863
9296
  }
8864
9297
  /** The package version, read from package.json at build time. */
8865
- const VERSION = "0.16.0";
9298
+ const VERSION = "0.18.0";
8866
9299
  /**
8867
9300
  * Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
8868
9301
  * to decide whether the release it last heard of is newer than the one running.
@@ -8998,15 +9431,18 @@ var PrintedCommand = class {
8998
9431
  #entry;
8999
9432
  /** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
9000
9433
  words;
9001
- /** The line to paste; null when one of the words has no printing that every Windows shell passes on alike. */
9434
+ /** The line to paste; null when no supported common or labelled shell rendering is safe. */
9002
9435
  line;
9003
9436
  platform;
9437
+ shell;
9004
9438
  constructor(gate, words, platform, entry) {
9005
9439
  if (gate !== GATE) throw new TypeError("a printed command is made by locateCliCommand, and only there");
9006
9440
  this.#entry = entry;
9007
9441
  this.words = Object.freeze([...words]);
9008
- this.line = quoteCommand(this.words, platform).line;
9442
+ const quoted = quoteCommand(this.words, platform);
9443
+ this.line = quoted.line;
9009
9444
  this.platform = platform;
9445
+ if (quoted.shell !== void 0) this.shell = quoted.shell;
9010
9446
  Object.freeze(this);
9011
9447
  }
9012
9448
  /**
@@ -9033,7 +9469,7 @@ const STRIP_TYPES = "--experimental-strip-types";
9033
9469
  const TRANSFORM_TYPES = "--experimental-transform-types";
9034
9470
  /** The suite's packages, by name, and its channel packages, which reach core through their own dependency on it. */
9035
9471
  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));
9472
+ const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manifest.channel !== "core").map(({ packageName }) => packageName));
9037
9473
  /**
9038
9474
  * Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
9039
9475
  *
@@ -9116,7 +9552,7 @@ function locateCliCommand(request, runtime = {
9116
9552
  sameCheckout: true
9117
9553
  });
9118
9554
  }
9119
- if (target.manifest.channel === "core" && CHANNEL_PACKAGES$1.has(caller.packageName)) {
9555
+ if (target.manifest.channel === "core" && CHANNEL_PACKAGES.has(caller.packageName)) {
9120
9556
  const installed = installedPackage(dirname(callerFile), target.packageName);
9121
9557
  if (installed === null) return fail("core", `${caller.packageName} has no ${target.packageName} installed where it can find one`);
9122
9558
  const manifest = readPackageManifest(installed);
@@ -9144,7 +9580,9 @@ function locateCliCommand(request, runtime = {
9144
9580
  }
9145
9581
  if (!SUITE_PACKAGES.has(caller.packageName)) return fail("direction", `${caller.packageName} is not a package of this suite`);
9146
9582
  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);
9583
+ const channelTarget = CHANNEL_SNAPSHOT.find((entry) => entry.packageName === target.packageName);
9584
+ if (channelTarget === void 0) return fail("direction", `${target.packageName} is not a channel package this locator can find through a registration`);
9585
+ const facts = registrationFactsOf(channelTarget);
9148
9586
  const notUsed = [];
9149
9587
  const usable = [];
9150
9588
  let engineRange;
@@ -9242,8 +9680,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
9242
9680
  let entry;
9243
9681
  if (found.source) entry = join(found.root, "src", "cli.ts");
9244
9682
  else {
9245
- const declared = found.manifest.agentcomms;
9246
- const binary = typeof declared === "object" && declared !== null ? declared.binary : void 0;
9683
+ const binary = declaredBinary(found.manifest);
9247
9684
  const bin = found.manifest.bin;
9248
9685
  const path = binary === target.manifest.binary && typeof bin === "object" && bin !== null && Object.hasOwn(bin, target.manifest.binary) ? bin[target.manifest.binary] : void 0;
9249
9686
  if (typeof path !== "string" || path.length === 0) return {
@@ -9272,6 +9709,16 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
9272
9709
  };
9273
9710
  }
9274
9711
  /**
9712
+ * The command a package declares for itself: a channel's in its `agentcomms` field, a service's in `agentcommsPackage`
9713
+ * (D14). A library declares none, and no package declares both: the registry refuses one that does.
9714
+ */
9715
+ function declaredBinary(manifest) {
9716
+ const channel = manifest.agentcomms;
9717
+ if (typeof channel === "object" && channel !== null) return channel.binary;
9718
+ const service = manifest.agentcommsPackage;
9719
+ if (typeof service === "object" && service !== null && service.kind === "service") return service.binary;
9720
+ }
9721
+ /**
9275
9722
  * The target's package a registration's files lead to, or null: a script it starts, real or as written when it is
9276
9723
  * gone, inside the package (or inside the product's own server wrapper, which depends on it); or, for the product's
9277
9724
  * own command, the package that command links into or that npm installed beside it.
@@ -9403,7 +9850,7 @@ function notLocated(target, version, reason, detail, nodeVersion, extra) {
9403
9850
  * anywhere else for the product, PATH included, and a later locate checks the path again.
9404
9851
  */
9405
9852
  function checkedEntry(root, entry) {
9406
- if (!isWithin(root, entry)) return {
9853
+ if (!isWithin$1(root, entry)) return {
9407
9854
  ok: false,
9408
9855
  why: `${entry} is outside its package, ${root}`
9409
9856
  };
@@ -9416,7 +9863,7 @@ function checkedEntry(root, entry) {
9416
9863
  why: `${entry} is not there`
9417
9864
  };
9418
9865
  }
9419
- if (!isWithin(root, real)) return {
9866
+ if (!isWithin$1(root, real)) return {
9420
9867
  ok: false,
9421
9868
  why: `${entry} leads to ${real}, outside its package, ${root}`
9422
9869
  };
@@ -9751,6 +10198,10 @@ function renderInstall(result, color) {
9751
10198
  lines.push(paint(color, "bold", `Add this to ${result.configPath ?? `the MCP configuration of ${result.client}`}:`), result.snippet.trimEnd());
9752
10199
  }
9753
10200
  if (result.backupPath) lines.push(`The entry it replaced was saved to ${result.backupPath}.`);
10201
+ if (result.shim && result.shim.status !== "foreign") lines.push(`CLI shim ${result.shim.status}: ${result.shim.path}.`);
10202
+ if (result.shimFailure) lines.push(paint(color, "yellow", `CLI shim failed: ${result.shimFailure}.`));
10203
+ if (result.shimNotice) lines.push(paint(color, "yellow", result.shimNotice));
10204
+ if (result.shim?.pathNotice) lines.push(result.shim.pathNotice);
9754
10205
  if (result.verification === "passed") lines.push(paint(color, "green", `Checked: ${result.verifyDetail}`));
9755
10206
  else if (result.verification === "failed") {
9756
10207
  lines.push(paint(color, "red", `Failed to start: ${result.verifyDetail ?? "no reason given"}`));
@@ -9765,6 +10216,8 @@ function renderPrune(result, color) {
9765
10216
  if (result.refused) lines.push(paint(color, "yellow", `Nothing removed: ${result.refused}.`));
9766
10217
  const verb = result.dryRun ? "Would remove" : "Removed";
9767
10218
  for (const item of result.removed) lines.push(paint(color, "green", `${verb} ${item.version}: ${item.path}`));
10219
+ for (const path of result.shims.removed) lines.push(paint(color, "green", `${verb} CLI shim: ${path}`));
10220
+ for (const path of result.shims.kept) lines.push(`Kept foreign CLI shim: ${path}`);
9768
10221
  for (const item of result.kept) lines.push(`Kept ${item.version} (${item.reason}): ${item.path}`);
9769
10222
  if (lines.length === 0) lines.push(`No managed runtimes to remove in ${result.runtimeDir}.`);
9770
10223
  return lines.join("\n");
@@ -10857,10 +11310,10 @@ strictObject({
10857
11310
  sha256: string().regex(/^[0-9a-f]{64}$/, "a SHA-256 is 64 lowercase hex digits")
10858
11311
  });
10859
11312
  /**
10860
- * Registering, pruning and listing the MCP servers of every channel, from the core.
11313
+ * Registering, pruning and listing every declared channel or service MCP server, from the core.
10861
11314
  *
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
11315
+ * `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
11316
+ * server; and channel CLIs register their own. They are the one change here, over
10864
11317
  * the shared installer (`mcp-install.ts`), and all go through the change flow: registering a server is a loosening in
10865
11318
  * 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
11319
  * second call with the approval. Printing an entry registers nothing and asks nobody.
@@ -10871,7 +11324,7 @@ strictObject({
10871
11324
  * the other — or worse, a channel's own command would register with no approval at all, which is what it did.
10872
11325
  *
10873
11326
  * 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.
11327
+ * released together, at one version, so the core asked to install Gmail or events installs its own release.
10875
11328
  */
10876
11329
  const CLIENTS = Object.freeze([
10877
11330
  "claude-code",
@@ -10912,10 +11365,10 @@ const exists = (path) => access(path, constants.R_OK).then(() => true, () => fal
10912
11365
  * core depends on none of them. So the channel is looked for beside this core — `packages/gmail` next to
10913
11366
  * `packages/core` in a checkout — and from source when this core runs from source, as the channel CLIs do.
10914
11367
  */
10915
- async function localModuleUrl(channel) {
11368
+ async function localModuleUrl(server) {
10916
11369
  const coreRoot = await corePackageRoot();
10917
- const unscopedName = channelServer(channel).packageName.split("/").at(-1) ?? channel;
10918
- const root = channel === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
11370
+ const unscopedName = serverFacts(server).packageName.split("/").at(-1) ?? server;
11371
+ const root = server === "core" ? coreRoot : join(dirname(coreRoot), unscopedName);
10919
11372
  const source = join(root, "src", "cli.ts");
10920
11373
  const built = join(root, "dist", "cli.mjs");
10921
11374
  const fromSource = fileURLToPath(import.meta.url).endsWith(".ts");
@@ -10923,7 +11376,7 @@ async function localModuleUrl(channel) {
10923
11376
  if (!await exists(cli)) continue;
10924
11377
  return pathToFileURL(cli === source ? join(root, "src", "mcp", "module.ts") : join(root, "dist", "module.mjs")).href;
10925
11378
  }
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.` });
11379
+ 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
11380
  }
10928
11381
  /**
10929
11382
  * The product the shared installer registers, for one channel: at this core's version unless another is named.
@@ -10933,11 +11386,11 @@ async function localModuleUrl(channel) {
10933
11386
  * newer release here is all it takes to register one this core is older than — which is what `agentcomms update` does.
10934
11387
  * `comms_server_install` and every `mcp install` name none, and register their own release as they always have.
10935
11388
  */
10936
- async function channelProduct(channel, launcher, version = VERSION) {
11389
+ async function serverProduct(server, launcher, version = VERSION) {
10937
11390
  return {
10938
- ...channelServer(channel),
11391
+ ...serverFacts(server),
10939
11392
  version,
10940
- moduleUrl: launcher === "local" ? await localModuleUrl(channel) : ""
11393
+ moduleUrl: launcher === "local" ? await localModuleUrl(server) : ""
10941
11394
  };
10942
11395
  }
10943
11396
  /**
@@ -10950,8 +11403,8 @@ async function channelProduct(channel, launcher, version = VERSION) {
10950
11403
  * registered, from the channel; a product for another package would register something else under those words, and
10951
11404
  * the person would have approved a sentence that was not true.
10952
11405
  */
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`);
11406
+ function ownProduct(server, own) {
11407
+ 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
11408
  return own;
10956
11409
  }
10957
11410
  /**
@@ -10961,11 +11414,11 @@ function ownProduct(channel, own) {
10961
11414
  * register a server reaching every workspace for a caller who believed they had pinned it.
10962
11415
  */
10963
11416
  function checkRequest(request) {
10964
- if (!isChannel(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
11417
+ if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
10965
11418
  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
11419
  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
11420
  if (request.name !== void 0) checkServerName(request.name);
10968
- const label = channelLabel(request.channel);
11421
+ const label = serverLabel(request.channel);
10969
11422
  const refuse = (option) => {
10970
11423
  const owner = narrowingOwner(option);
10971
11424
  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 +11454,7 @@ function checkPin(config, request) {
11001
11454
  return;
11002
11455
  }
11003
11456
  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)}`);
11457
+ if (account.platform !== request.channel) throw new CommsError("USAGE", `"${pin}" is not a ${serverLabel(request.channel)} ${accountNoun(request.channel)}`);
11005
11458
  }
11006
11459
  function installOptions(request) {
11007
11460
  const own = pinOption(request.channel);
@@ -11020,7 +11473,7 @@ function installOptions(request) {
11020
11473
  };
11021
11474
  }
11022
11475
  /**
11023
- * Registering one channel's server with one client, as a change.
11476
+ * Registering one declared server with one client, as a change.
11024
11477
  *
11025
11478
  * The effects are what a person is agreeing to, one sentence each, and everything that decides what the server may
11026
11479
  * reach is in them — which server, which client, under which name, pinned to what, replacing what, started how. The
@@ -11035,21 +11488,22 @@ function installOptions(request) {
11035
11488
  * resolves another — `CLAUDE_CONFIG_DIR` set to another account's, a PATH that finds another node, a core running
11036
11489
  * from another build of the checkout — is another registration, refused as one.
11037
11490
  */
11038
- function serverInstallChange(core, env, request, own) {
11491
+ function serverInstallChange(core, env, request, own, deps = {}) {
11039
11492
  checkRequest(request);
11040
11493
  ownProduct(request.channel, own);
11041
11494
  const context = {
11042
11495
  env,
11043
- core
11496
+ core,
11497
+ ...deps.platform === void 0 ? {} : { platform: deps.platform }
11044
11498
  };
11045
- const facts = channelServer(request.channel);
11046
- const label = channelLabel(request.channel);
11499
+ const facts = serverFacts(request.channel);
11500
+ const label = serverLabel(request.channel);
11047
11501
  const noun = accountNoun(request.channel);
11048
11502
  const pinName = pinOption(request.channel);
11049
11503
  const switchable = hasNarrowing(request.channel, "readOnly");
11050
11504
  const name = request.name ?? facts.defaultServerName;
11051
11505
  const launcher = request.launcher ?? "managed";
11052
- const productNow = async () => own ?? await channelProduct(request.channel, request.launcher);
11506
+ const productNow = async () => own ?? await serverProduct(request.channel, request.launcher);
11053
11507
  let planned;
11054
11508
  return {
11055
11509
  plan: async (config) => {
@@ -11080,6 +11534,14 @@ function serverInstallChange(core, env, request, own) {
11080
11534
  else if (launcher === "local") effects.push(`${request.client} will start it from ${await localCliEntry(product.moduleUrl)}`);
11081
11535
  }
11082
11536
  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)}`);
11537
+ if (request.print !== true && launcher === "managed" && managedShimsSupported(context)) {
11538
+ const path = managedShimPath({
11539
+ env,
11540
+ dataDir: core.paths.dataDir,
11541
+ platform: context.platform
11542
+ }, facts.binary);
11543
+ effects.push(`writes or repoints the owned CLI shim at ${path}; a foreign file there is left unchanged`);
11544
+ }
11083
11545
  return {
11084
11546
  before: config,
11085
11547
  after: config,
@@ -11089,9 +11551,10 @@ function serverInstallChange(core, env, request, own) {
11089
11551
  },
11090
11552
  apply: async () => {
11091
11553
  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) => {
11554
+ const installed = await mcpInstall(context, planned.product, installOptions(request), planned.install).catch(async (error) => {
11093
11555
  throw await remedyLocated(error, core);
11094
11556
  });
11557
+ const result = request.print === true ? installed : await withManagedShim(context, planned.product, installed);
11095
11558
  return {
11096
11559
  ...result,
11097
11560
  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 +11563,68 @@ function serverInstallChange(core, env, request, own) {
11100
11563
  };
11101
11564
  }
11102
11565
  /**
11566
+ * A managed install's CLI shim (CUE-406), once the registration's outcome is known: the command the locator finds in
11567
+ * the runtime the entry starts, written where nothing is or over this installation's own unchanged shim, and never
11568
+ * over anything else. A shim that cannot be written leaves the registration as it went, and says how to repair both.
11569
+ * The installer may not import the locator, so the operation writes it — for `mcp install` and `update` alike.
11570
+ */
11571
+ async function withManagedShim(context, product, installed) {
11572
+ if (installed.launcher !== "managed") return installed;
11573
+ const cliEntry = installed.entry.args[0];
11574
+ if (cliEntry === void 0) throw new CommsError("UNEXPECTED", "the managed entry has no CLI path");
11575
+ const shimContext = {
11576
+ env: context.env,
11577
+ dataDir: context.core.paths.dataDir,
11578
+ platform: context.platform
11579
+ };
11580
+ if (!managedShimsSupported(shimContext)) return {
11581
+ ...installed,
11582
+ shimNotice: managedShimUnsupportedNotice(shimContext)
11583
+ };
11584
+ const warnings = [...installed.warnings];
11585
+ let shim;
11586
+ let shimFailure;
11587
+ try {
11588
+ const located = locateCliCommand({
11589
+ caller: {
11590
+ url: pathToFileURL(cliEntry).href,
11591
+ packageName: product.packageName
11592
+ },
11593
+ target: {
11594
+ packageName: product.packageName,
11595
+ manifest: {
11596
+ label: product.defaultServerName,
11597
+ binary: product.binary
11598
+ }
11599
+ },
11600
+ words: [],
11601
+ uses: [],
11602
+ paths: {
11603
+ ...context.core.paths,
11604
+ downloadsDir: context.core.paths.dataDir
11605
+ },
11606
+ platform: context.platform
11607
+ });
11608
+ if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
11609
+ shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
11610
+ binary: product.binary,
11611
+ command: located.command.words,
11612
+ entry: located.command.entry
11613
+ });
11614
+ } catch (error) {
11615
+ shimFailure = error instanceof Error ? error.message : String(error);
11616
+ const path = join(homeDirectory(context.env), ".local", "bin", product.binary);
11617
+ 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.`);
11618
+ }
11619
+ 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.`);
11620
+ return {
11621
+ ...installed,
11622
+ warnings,
11623
+ ...shim === void 0 ? {} : { shim },
11624
+ ...shimFailure === void 0 ? {} : { shimFailure }
11625
+ };
11626
+ }
11627
+ /**
11103
11628
  * A refusal of `mcp install` with its remedy — another install of the same product — said with the command located:
11104
11629
  * the printing package's own, core's, or another product's found among this machine's registrations (CUE-403). The
11105
11630
  * installer says only the words; it may not import the locator.
@@ -11107,17 +11632,24 @@ function serverInstallChange(core, env, request, own) {
11107
11632
  async function remedyLocated(error, core) {
11108
11633
  const remedy = installRemedyOf(error);
11109
11634
  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;
11635
+ const server = SERVERS.find((each) => serverFacts(each).binary === remedy.binary);
11636
+ if (server === void 0) return error;
11112
11637
  const maker = await requireHandoffs(core).registered();
11638
+ const handoff = isChannel(server) ? maker.of(server, remedy.words) : maker.core([
11639
+ "mcp",
11640
+ "install",
11641
+ "--server",
11642
+ server,
11643
+ ...remedy.words.slice(2)
11644
+ ]);
11113
11645
  return new CommsError(error.code, error.message, {
11114
- hint: handoffSentence(maker.of(channel, remedy.words), remedy.say),
11646
+ hint: handoffSentence(handoff, remedy.say),
11115
11647
  ...error.details === void 0 ? {} : { details: error.details },
11116
11648
  ...error.cause === void 0 ? {} : { cause: error.cause }
11117
11649
  });
11118
11650
  }
11119
11651
  /**
11120
- * Removing a channel's unused managed runtimes, as a change.
11652
+ * Removing a declared server's unused managed runtimes, as a change.
11121
11653
  *
11122
11654
  * A dry run is free: it removes nothing. Otherwise the plan is that same dry run, and each runtime it would remove is
11123
11655
  * one effect — the removal cannot be taken back — so the approval is bound to exactly that list; and the removal is
@@ -11127,20 +11659,20 @@ async function remedyLocated(error, core) {
11127
11659
  * release of the command that was run; checked to be that channel's, as for an install.
11128
11660
  */
11129
11661
  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(", ")}.` });
11662
+ if (!isServer(request.channel)) throw new CommsError("USAGE", `"${String(request.channel)}" is not a channel`, { hint: `One of: ${CHANNELS.join(", ")}.` });
11131
11663
  const context = {
11132
11664
  env,
11133
11665
  core
11134
11666
  };
11135
11667
  const { packageName, version } = ownProduct(request.channel, own) ?? {
11136
- packageName: channelServer(request.channel).packageName,
11137
- version: "0.16.0"
11668
+ packageName: serverFacts(request.channel).packageName,
11669
+ version: "0.18.0"
11138
11670
  };
11139
11671
  const product = {
11140
11672
  packageName,
11141
11673
  version
11142
11674
  };
11143
- const label = channelLabel(request.channel);
11675
+ const label = serverLabel(request.channel);
11144
11676
  const base = {
11145
11677
  includePrinted: request.includePrinted === true,
11146
11678
  ...request.processes ? { processes: request.processes } : {}
@@ -11161,7 +11693,7 @@ function serverPruneChange(core, env, request, own) {
11161
11693
  return {
11162
11694
  before: config,
11163
11695
  after: config,
11164
- effects: preview.removed.map((item) => `deletes the unused ${label} runtime ${item.version} at ${item.path}`),
11696
+ 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
11697
  summary: `Remove ${approved.length} unused ${label} runtime${approved.length === 1 ? "" : "s"}`
11166
11698
  };
11167
11699
  },
@@ -11434,13 +11966,14 @@ function updateStopMessage(pending, where, commands) {
11434
11966
  * the yes is a different change, and the claim is refused. Nothing behind, nothing is prepared.
11435
11967
  */
11436
11968
  /**
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`.
11969
+ * The packages this suite publishes, whose latest releases an update reads: every declared server owner's, from the
11970
+ * channel and service declarations, and every package a server is run through by `npx` when that is another one —
11971
+ * Gmail's `gmail-mcp`.
11439
11972
  */
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;
11973
+ const OWNER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).packageName));
11974
+ const SERVER_PACKAGES = Object.freeze(SERVERS.map((server) => serverFacts(server).npxPackage).filter((name) => !OWNER_PACKAGES.includes(name)));
11975
+ Object.freeze([.../* @__PURE__ */ new Set([...OWNER_PACKAGES, ...SERVER_PACKAGES])].sort());
11976
+ serverFacts("core").packageName;
11444
11977
  /**
11445
11978
  * "Not now": no server or command on this machine stops for the update until the next local midnight.
11446
11979
  *
@@ -11788,7 +12321,7 @@ async function askLine(streams, question) {
11788
12321
  }
11789
12322
  channelServer("core").packageName;
11790
12323
  //#endregion
11791
- //#region ../core/dist/tool-arguments-DA5ZqgoB.mjs
12324
+ //#region ../core/dist/tool-arguments-C_RA7n3A.mjs
11792
12325
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
11793
12326
  const TARGET = "draft-2020-12";
11794
12327
  /**
@@ -12096,10 +12629,60 @@ new TextEncoder();
12096
12629
  const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12097
12630
  const base64Lookup = /* @__PURE__ */ new Uint8Array(256);
12098
12631
  for (let i = 0; i < 64; i++) base64Lookup[base64Chars.charCodeAt(i)] = i;
12632
+ new Set([
12633
+ "unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8",
12634
+ "866 cp866 csibm866 ibm866",
12635
+ "csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2",
12636
+ "csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3",
12637
+ "csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4",
12638
+ "csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988",
12639
+ "arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e",
12640
+ "iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987",
12641
+ "csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7",
12642
+ "iso_8859-7:1987 sun_eu_greek",
12643
+ "csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8",
12644
+ "iso_8859-8:1988 visual",
12645
+ "csiso88598i iso-8859-8-i logical",
12646
+ "csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6",
12647
+ "iso-8859-13 iso8859-13 iso885913",
12648
+ "iso-8859-14 iso8859-14 iso885914",
12649
+ "csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9",
12650
+ "iso-8859-16",
12651
+ "cskoi8r koi koi8 koi8-r koi8_r",
12652
+ "koi8-ru koi8-u",
12653
+ "csmacintosh mac macintosh x-mac-roman",
12654
+ "dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874",
12655
+ "cp1250 windows-1250 x-cp1250",
12656
+ "cp1251 windows-1251 x-cp1251",
12657
+ "ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591",
12658
+ "iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252",
12659
+ "cp1253 windows-1253 x-cp1253",
12660
+ "cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5",
12661
+ "windows-1254 x-cp1254",
12662
+ "cp1255 windows-1255 x-cp1255",
12663
+ "cp1256 windows-1256 x-cp1256",
12664
+ "cp1257 windows-1257 x-cp1257",
12665
+ "cp1258 windows-1258 x-cp1258",
12666
+ "x-mac-cyrillic x-mac-ukrainian",
12667
+ "chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk",
12668
+ "gb18030",
12669
+ "big5 big5-hkscs cn-big5 csbig5 x-x-big5",
12670
+ "cseucpkdfmtjapanese euc-jp x-euc-jp",
12671
+ "csiso2022jp iso-2022-jp",
12672
+ "csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis",
12673
+ "cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601",
12674
+ "windows-949",
12675
+ "csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement",
12676
+ "unicodefffe utf-16be",
12677
+ "csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le",
12678
+ "x-user-defined"
12679
+ ].join(" ").split(" "));
12680
+ new TextDecoder();
12099
12681
  const ENCODED_WORD_PATTERN = "=\\?([^?\\s]+)\\?([QqBb])\\?([^?]*)\\?=";
12100
12682
  new RegExp(ENCODED_WORD_PATTERN, "g");
12101
12683
  new RegExp(`^(?:${ENCODED_WORD_PATTERN}\\s*)+$`);
12102
12684
  new TextDecoder("utf-8", { ignoreBOM: true });
12685
+ new Uint8Array([10]);
12103
12686
  //#endregion
12104
12687
  //#region ../../node_modules/.pnpm/deepmerge-ts@8.0.2/node_modules/deepmerge-ts/dist/index.mjs
12105
12688
  /**