@agentcomms/whatsapp 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as renderPrune, A as CommsError, B as colorEnabled, C as checkStorePath, F as approvalsOf, G as installExitStatus, H as escapeForDisplay, I as approveChangeAtTerminal, J as nameAvailable, K as isCommsError, L as canPrompt, M as absoluteSearchPath, N as agentMarker, O as CHANNELS, P as approvalKind, Q as renderInstall, R as channelManifest, S as storeIdentity, T as expandSource, U as exemptFromUpdateGate, V as commandPathOf, W as gatedChangeAtTerminal, X as openCore, Y as nameShapeProblem, Z as paint, _ as chatRefOf, a as listChats, at as updateGateAtTerminal, b as requireAccount, c as Presenter, ct as withFileLock, d as draftMessage, et as runCommand, f as Visibility, g as requireSupportedNode, h as WhatsAppIndex, i as CHAT_KINDS, it as truncateDisplay, j as EXIT_CODES, k as CHANNEL_SERVERS, l as innerText, lt as writeResult, m as WhatsAppContext, n as whatsappStatus, nt as serverPruneChange, o as readChat, p as listKey, q as lookupName, r as probeStore, s as searchMessages, st as windowsSystemProgram, t as syncAccount, tt as serverInstallChange, ut as VERSION, v as PLATFORM, w as defaultStorePath, x as requireNamedConfig, y as newWhatsAppAccount, z as childEnvironment } from "./sync-
|
|
2
|
+
import { $ as renderPrune, A as CommsError, B as colorEnabled, C as checkStorePath, F as approvalsOf, G as installExitStatus, H as escapeForDisplay, I as approveChangeAtTerminal, J as nameAvailable, K as isCommsError, L as canPrompt, M as absoluteSearchPath, N as agentMarker, O as CHANNELS, P as approvalKind, Q as renderInstall, R as channelManifest, S as storeIdentity, T as expandSource, U as exemptFromUpdateGate, V as commandPathOf, W as gatedChangeAtTerminal, X as openCore, Y as nameShapeProblem, Z as paint, _ as chatRefOf, a as listChats, at as updateGateAtTerminal, b as requireAccount, c as Presenter, ct as withFileLock, d as draftMessage, et as runCommand, f as Visibility, g as requireSupportedNode, h as WhatsAppIndex, i as CHAT_KINDS, it as truncateDisplay, j as EXIT_CODES, k as CHANNEL_SERVERS, l as innerText, lt as writeResult, m as WhatsAppContext, n as whatsappStatus, nt as serverPruneChange, o as readChat, p as listKey, q as lookupName, r as probeStore, s as searchMessages, st as windowsSystemProgram, t as syncAccount, tt as serverInstallChange, ut as VERSION, v as PLATFORM, w as defaultStorePath, x as requireNamedConfig, y as newWhatsAppAccount, z as childEnvironment } from "./sync-CBrmicvU.mjs";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import path, { delimiter, join } from "node:path";
|
|
5
|
+
import fs, { accessSync, constants as constants$1 } from "node:fs";
|
|
5
6
|
import childProcess, { spawn } from "node:child_process";
|
|
6
7
|
import { stripVTControlCharacters } from "node:util";
|
|
7
|
-
import fs, { accessSync, constants as constants$1 } from "node:fs";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
|
|
@@ -3534,7 +3534,7 @@ a macOS dialog may be waiting) · 77 permission needed (macOS privacy) · 78 con
|
|
|
3534
3534
|
ran = true;
|
|
3535
3535
|
exitCode = await runCommand(output(), async () => {
|
|
3536
3536
|
requireSupportedNode();
|
|
3537
|
-
const { startWhatsAppStdioServer } = await import("./stdio-entry-
|
|
3537
|
+
const { startWhatsAppStdioServer } = await import("./stdio-entry-D23n9Ixl.mjs");
|
|
3538
3538
|
await startWhatsAppStdioServer({
|
|
3539
3539
|
env,
|
|
3540
3540
|
...flags.account ? { account: String(flags.account) } : {}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { u as composeDraft, ut as VERSION } from "./sync-
|
|
2
|
-
import { t as createWhatsAppMcpServer } from "./server-
|
|
1
|
+
import { u as composeDraft, ut as VERSION } from "./sync-CBrmicvU.mjs";
|
|
2
|
+
import { t as createWhatsAppMcpServer } from "./server-B4Qj5VDK.mjs";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
/**
|
|
5
5
|
* The library entry of `@agentcomms/whatsapp`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as strictToolArguments, E as UNTRUSTED_NOTICE, a as listChats, d as draftMessage, i as CHAT_KINDS, m as WhatsAppContext, n as whatsappStatus, o as readChat, ot as updateToolGate, rt as toCommsError, s as searchMessages, t as syncAccount, ut as VERSION } from "./sync-
|
|
1
|
+
import { D as strictToolArguments, E as UNTRUSTED_NOTICE, a as listChats, d as draftMessage, i as CHAT_KINDS, m as WhatsAppContext, n as whatsappStatus, o as readChat, ot as updateToolGate, rt as toCommsError, s as searchMessages, t as syncAccount, ut as VERSION } from "./sync-CBrmicvU.mjs";
|
|
2
2
|
import { c as boolean, h as number, x as string } from "./schemas-DiGKNDNq.mjs";
|
|
3
3
|
import { t as McpServer } from "./mcp-DXXb3Vv3-Co5BzjYe.mjs";
|
|
4
4
|
//#region src/mcp/server.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createWhatsAppMcpServer } from "./server-
|
|
1
|
+
import { t as createWhatsAppMcpServer } from "./server-B4Qj5VDK.mjs";
|
|
2
2
|
//#region src/mcp/stdio-entry.ts
|
|
3
3
|
/**
|
|
4
4
|
* Starting the stdio server. On stdio, stdout is the protocol, so the console is redirected to stderr first — the
|
|
@@ -5,15 +5,15 @@ import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
|
|
|
5
5
|
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import { homedir, platform, userInfo } from "node:os";
|
|
8
|
+
import { constants as constants$1 } from "node:fs";
|
|
8
9
|
import { execFile, spawn } from "node:child_process";
|
|
9
10
|
import { styleText } from "node:util";
|
|
10
|
-
import { constants as constants$1 } from "node:fs";
|
|
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.
|
|
14
|
+
const VERSION$1 = "0.12.0";
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region ../core/dist/update-
|
|
16
|
+
//#region ../core/dist/update-check-pKViNOsH.mjs
|
|
17
17
|
/**
|
|
18
18
|
* Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
|
|
19
19
|
* optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
|
|
@@ -951,74 +951,60 @@ function connectMailboxCommand() {
|
|
|
951
951
|
return mail ? `${mail.binary} inbox add` : void 0;
|
|
952
952
|
}
|
|
953
953
|
/**
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
* `cue/gmail` is the CUE++ mailbox, `cue/slack` the CUE++ workspace, `wf/gmail-tech` a second Wherefrom mailbox. The
|
|
957
|
-
* first half says who an account belongs to and the second what it is — and the schema checks the second half against
|
|
958
|
-
* the account itself, so a name cannot claim to be a Slack workspace while naming a mailbox.
|
|
959
|
-
*
|
|
960
|
-
* Kept apart from `config.ts`, with no imports, so the schema and the helpers built on it can both depend on it
|
|
961
|
-
* without depending on each other.
|
|
962
|
-
*/
|
|
963
|
-
/**
|
|
964
|
-
* Organisation names Windows cannot use as a directory.
|
|
965
|
-
*
|
|
966
|
-
* The organisation becomes a folder — downloads land in `downloads/cue/gmail/…` — and `con`, `nul` and the rest are
|
|
967
|
-
* device names on Windows in every directory, with or without an extension. Refused here rather than discovered the
|
|
968
|
-
* first time somebody on Windows saves an attachment.
|
|
954
|
+
* One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
|
|
955
|
+
* and the preview renderer (which makes them visible), so the two can never disagree.
|
|
969
956
|
*/
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
957
|
+
/** Zero-width and formatting characters, bidi controls, variation selectors and Unicode tag characters. */
|
|
958
|
+
const INVISIBLE_RANGES = [
|
|
959
|
+
[173, 173],
|
|
960
|
+
[847, 847],
|
|
961
|
+
[1564, 1564],
|
|
962
|
+
[4447, 4448],
|
|
963
|
+
[6068, 6069],
|
|
964
|
+
[6155, 6159],
|
|
965
|
+
[8203, 8207],
|
|
966
|
+
[8232, 8233],
|
|
967
|
+
[8234, 8238],
|
|
968
|
+
[8288, 8292],
|
|
969
|
+
[8294, 8303],
|
|
970
|
+
[12644, 12644],
|
|
971
|
+
[65024, 65039],
|
|
972
|
+
[65279, 65279],
|
|
973
|
+
[65440, 65440],
|
|
974
|
+
[917504, 917631]
|
|
977
975
|
];
|
|
978
|
-
function
|
|
979
|
-
|
|
976
|
+
function isInvisible(codePoint) {
|
|
977
|
+
for (const [from, to] of INVISIBLE_RANGES) if (codePoint >= from && codePoint <= to) return true;
|
|
978
|
+
return false;
|
|
980
979
|
}
|
|
981
|
-
const ORG = "[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?";
|
|
982
|
-
const PLATFORM$1 = "[a-z][a-z0-9]{0,15}";
|
|
983
980
|
/**
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
* Segments never start or end with a hyphen, and the character set is `[a-z0-9/-]` and nothing else: no `.`, so no
|
|
987
|
-
* `..`; no whitespace, no controls, no upper case, nothing that looks like something else.
|
|
981
|
+
* C0 controls except tab and newline (ESC, and so every ANSI/OSC sequence, starts here), DEL, and C1 controls (CSI
|
|
982
|
+
* among them). A lone carriage return counts: it moves a terminal cursor back over text already printed.
|
|
988
983
|
*/
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const match = NAME_PATTERN.exec(name);
|
|
996
|
-
if (!match) return null;
|
|
997
|
-
const [, org = "", platform = "", qualifier] = match;
|
|
998
|
-
return qualifier === void 0 ? {
|
|
999
|
-
org,
|
|
1000
|
-
platform
|
|
1001
|
-
} : {
|
|
1002
|
-
org,
|
|
1003
|
-
platform,
|
|
1004
|
-
qualifier
|
|
1005
|
-
};
|
|
984
|
+
function isControl(codePoint) {
|
|
985
|
+
if (codePoint === 9 || codePoint === 10) return false;
|
|
986
|
+
return codePoint < 32 || codePoint === 127 || codePoint >= 128 && codePoint <= 159;
|
|
987
|
+
}
|
|
988
|
+
function isDangerous(codePoint) {
|
|
989
|
+
return isControl(codePoint) || isInvisible(codePoint);
|
|
1006
990
|
}
|
|
1007
991
|
/**
|
|
1008
|
-
*
|
|
992
|
+
* Strips control characters (ESC, CSI, OSC and the rest), DEL, lone carriage returns, zero-width, bidi-control and tag
|
|
993
|
+
* characters from sender-controlled text; returns the text and how many were removed. CRLF becomes LF first.
|
|
1009
994
|
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
995
|
+
* This lives beside the table rather than in the sanitiser because `neutralise` needs it too, and the two must not
|
|
996
|
+
* drift: a pattern that looks for `</untrusted-content` cannot see it through a zero-width space, so stripping
|
|
997
|
+
* has to happen before any such pattern runs, on every path, not only on the ones that render a body.
|
|
1012
998
|
*/
|
|
1013
|
-
function
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
999
|
+
function stripInvisible(text) {
|
|
1000
|
+
let removed = 0;
|
|
1001
|
+
let out = "";
|
|
1002
|
+
for (const char of text.replace(/\r\n/g, "\n")) if (isDangerous(char.codePointAt(0) ?? 0)) removed += 1;
|
|
1003
|
+
else out += char;
|
|
1004
|
+
return {
|
|
1005
|
+
text: out,
|
|
1006
|
+
removed
|
|
1007
|
+
};
|
|
1022
1008
|
}
|
|
1023
1009
|
const APP_DIR_NAME = "agent-communications";
|
|
1024
1010
|
/**
|
|
@@ -1082,6 +1068,23 @@ function expandHome(path, home = homedir(), joinPaths = join) {
|
|
|
1082
1068
|
return path;
|
|
1083
1069
|
}
|
|
1084
1070
|
/**
|
|
1071
|
+
* A path as a preview shows it: the home at its start written `~`, the way a person reads and types it. The inverse of
|
|
1072
|
+
* `expandHome`, and like it, nothing else is touched. Case is ignored on Windows, whose paths are not case-sensitive.
|
|
1073
|
+
*/
|
|
1074
|
+
function shortenHome(path, home, platform = process.platform) {
|
|
1075
|
+
if (!home) return path;
|
|
1076
|
+
const same = (a, b) => platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1077
|
+
let end = home.length;
|
|
1078
|
+
while (end > 0 && (home[end - 1] === "/" || home[end - 1] === "\\")) end -= 1;
|
|
1079
|
+
const root = home.slice(0, end);
|
|
1080
|
+
if (same(path, root)) return "~";
|
|
1081
|
+
for (const separator of platform === "win32" ? ["\\", "/"] : ["/"]) {
|
|
1082
|
+
const prefix = `${root}${separator}`;
|
|
1083
|
+
if (path.length > prefix.length && same(path.slice(0, prefix.length), prefix)) return `~${separator}${path.slice(prefix.length)}`;
|
|
1084
|
+
}
|
|
1085
|
+
return path;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1085
1088
|
* The user's home directory, read from an environment.
|
|
1086
1089
|
*
|
|
1087
1090
|
* Windows does not set `HOME`; it sets `USERPROFILE`. Call sites that wrote `env.HOME ?? ''` and handed the result to
|
|
@@ -1097,6 +1100,88 @@ function homeDirectory(env = process.env) {
|
|
|
1097
1100
|
return env.HOME || env.USERPROFILE || homedir();
|
|
1098
1101
|
}
|
|
1099
1102
|
/**
|
|
1103
|
+
* Whether a folder, as written, says where it is on its own: from the home (`~`), or absolute — and on Windows with its
|
|
1104
|
+
* drive (`C:\…`) or share (`\\server\share\…`). Windows calls `\outgoing` absolute, but it is on whichever drive is
|
|
1105
|
+
* current when it is read, and `C:outgoing` is relative to that drive's current folder; either would move with the
|
|
1106
|
+
* process that reads it. A folder in the configuration that does not name its place — written by hand before 0.12.0,
|
|
1107
|
+
* say — allows nothing, and `agentcomms attach` takes none (#45).
|
|
1108
|
+
*/
|
|
1109
|
+
function namesItsPlace(path, platform = process.platform) {
|
|
1110
|
+
if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) return true;
|
|
1111
|
+
if (platform === "win32") return /^[A-Za-z]:[\\/]/.test(path) || /^[\\/]{2}[^\\/?.][^\\/]*[\\/][^\\/]+/.test(path);
|
|
1112
|
+
return isAbsolute(path);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* The grammar of an account name in version 2 of the config: `<organisation>/<platform>[-<qualifier>]`.
|
|
1116
|
+
*
|
|
1117
|
+
* `cue/gmail` is the CUE++ mailbox, `cue/slack` the CUE++ workspace, `wf/gmail-tech` a second Wherefrom mailbox. The
|
|
1118
|
+
* first half says who an account belongs to and the second what it is — and the schema checks the second half against
|
|
1119
|
+
* the account itself, so a name cannot claim to be a Slack workspace while naming a mailbox.
|
|
1120
|
+
*
|
|
1121
|
+
* Kept apart from `config.ts`, with no imports, so the schema and the helpers built on it can both depend on it
|
|
1122
|
+
* without depending on each other.
|
|
1123
|
+
*/
|
|
1124
|
+
/**
|
|
1125
|
+
* Organisation names Windows cannot use as a directory.
|
|
1126
|
+
*
|
|
1127
|
+
* The organisation becomes a folder — downloads land in `downloads/cue/gmail/…` — and `con`, `nul` and the rest are
|
|
1128
|
+
* device names on Windows in every directory, with or without an extension. Refused here rather than discovered the
|
|
1129
|
+
* first time somebody on Windows saves an attachment.
|
|
1130
|
+
*/
|
|
1131
|
+
const WINDOWS_RESERVED = [
|
|
1132
|
+
"con",
|
|
1133
|
+
"prn",
|
|
1134
|
+
"aux",
|
|
1135
|
+
"nul",
|
|
1136
|
+
...range$1("com"),
|
|
1137
|
+
...range$1("lpt")
|
|
1138
|
+
];
|
|
1139
|
+
function range$1(prefix) {
|
|
1140
|
+
return Array.from({ length: 9 }, (_, index) => `${prefix}${index + 1}`);
|
|
1141
|
+
}
|
|
1142
|
+
const ORG = "[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?";
|
|
1143
|
+
const PLATFORM$1 = "[a-z][a-z0-9]{0,15}";
|
|
1144
|
+
/**
|
|
1145
|
+
* The whole grammar as one expression, so a record key can be checked by the schema without a second pass.
|
|
1146
|
+
*
|
|
1147
|
+
* Segments never start or end with a hyphen, and the character set is `[a-z0-9/-]` and nothing else: no `.`, so no
|
|
1148
|
+
* `..`; no whitespace, no controls, no upper case, nothing that looks like something else.
|
|
1149
|
+
*/
|
|
1150
|
+
const NAME_PATTERN = new RegExp(`^(?!(?:${WINDOWS_RESERVED.join("|")})/)(${ORG})/(${PLATFORM$1})(?:-([a-z0-9](?:[a-z0-9-]{0,14}[a-z0-9])?))?$`);
|
|
1151
|
+
/** A platform word on its own — `gmail`, `slack` — which is also a channel's word in its manifest. */
|
|
1152
|
+
const PLATFORM_PATTERN = new RegExp(`^${PLATFORM$1}$`);
|
|
1153
|
+
const NAME_MESSAGE = "names look like organisation/platform, optionally with a qualifier: cue/gmail, wf/gmail-tech, cue/slack";
|
|
1154
|
+
/** The three parts of a valid name, or null for anything the grammar refuses. */
|
|
1155
|
+
function parseName(name) {
|
|
1156
|
+
const match = NAME_PATTERN.exec(name);
|
|
1157
|
+
if (!match) return null;
|
|
1158
|
+
const [, org = "", platform = "", qualifier] = match;
|
|
1159
|
+
return qualifier === void 0 ? {
|
|
1160
|
+
org,
|
|
1161
|
+
platform
|
|
1162
|
+
} : {
|
|
1163
|
+
org,
|
|
1164
|
+
platform,
|
|
1165
|
+
qualifier
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Why a name is refused, in words a person can act on — or null when it is fine.
|
|
1170
|
+
*
|
|
1171
|
+
* `platform` is what the account actually is. Checked here as well as in the schema, so the error arrives when the
|
|
1172
|
+
* name is proposed rather than as a refused config write after a sign-in has already been spent.
|
|
1173
|
+
*/
|
|
1174
|
+
function nameShapeProblem(name, platform) {
|
|
1175
|
+
const parsed = parseName(name);
|
|
1176
|
+
if (!parsed) {
|
|
1177
|
+
const org = name.split("/")[0] ?? "";
|
|
1178
|
+
if (WINDOWS_RESERVED.includes(org)) return `"${org}" cannot be an organisation name: Windows reserves it, and the organisation becomes a folder`;
|
|
1179
|
+
return `"${name}" is not a valid name: it should be the organisation, a slash, then ${platform} — for example acme/${platform}, or acme/${platform}-support for a second one`;
|
|
1180
|
+
}
|
|
1181
|
+
if (parsed.platform !== platform) return `"${name}" ends in /${parsed.platform}, but this is a ${platform} account`;
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1100
1185
|
* Whether this release may write version 2 of the config.
|
|
1101
1186
|
*
|
|
1102
1187
|
* Not while any reader that shares the file might still be one that cannot read it. So the release that teaches every
|
|
@@ -1671,8 +1756,17 @@ const POLICY_RANK$1 = {
|
|
|
1671
1756
|
* user a consent prompt for a change that is not one. Linux is case-sensitive, so case is kept.
|
|
1672
1757
|
*/
|
|
1673
1758
|
function normalisePath(path) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1759
|
+
return comparablePath(resolve(expandHome(path.trim(), homedir())));
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* An absolute path in the form two paths are compared in here: no trailing separator, and case folded on macOS and
|
|
1763
|
+
* Windows, whose filesystems are case-insensitive by default (see `normalisePath`). The top of a disk keeps its
|
|
1764
|
+
* separator — `/`, `C:\` — so it is not read as no path at all.
|
|
1765
|
+
*/
|
|
1766
|
+
function comparablePath(absolute) {
|
|
1767
|
+
const stripped = withoutTrailingSeparators(absolute);
|
|
1768
|
+
const kept = stripped === "" || /^[A-Za-z]:$/.test(stripped) ? absolute.slice(0, stripped.length + 1) : stripped;
|
|
1769
|
+
return platform() === "darwin" || platform() === "win32" ? kept.toLowerCase() : kept;
|
|
1676
1770
|
}
|
|
1677
1771
|
/**
|
|
1678
1772
|
* The path with any separators at its end removed. A loop, not `/[/\\]+$/`: that pattern backtracks over every run of
|
|
@@ -1687,9 +1781,14 @@ function withoutTrailingSeparators(path) {
|
|
|
1687
1781
|
function holdsSecrets(config) {
|
|
1688
1782
|
return Object.values(config.clients).some((client) => Boolean(client.secretRef)) || Object.values(config.inboxes).some((inbox) => Boolean(inbox.secretRef)) || Object.values(config.accounts).some((account) => Boolean(account.secretRef));
|
|
1689
1783
|
}
|
|
1690
|
-
/**
|
|
1784
|
+
/**
|
|
1785
|
+
* True when `candidate` is the same directory as `parent`, or inside it. Both may be unset. Both are compared as
|
|
1786
|
+
* written, so both have to be in one form first: `normalisePath`, or `comparablePath` of a real path. A parent that is
|
|
1787
|
+
* the top of a disk — `/`, `c:\` — holds everything on it.
|
|
1788
|
+
*/
|
|
1691
1789
|
function isInsideDirectory(candidate, parent) {
|
|
1692
1790
|
if (!candidate || !parent) return false;
|
|
1791
|
+
if (parent.endsWith("/") || parent.endsWith("\\")) return candidate.startsWith(parent);
|
|
1693
1792
|
return candidate === parent || candidate.startsWith(`${parent}/`) || candidate.startsWith(`${parent}\\`);
|
|
1694
1793
|
}
|
|
1695
1794
|
/**
|
|
@@ -1762,10 +1861,11 @@ function classifyChange(before, after) {
|
|
|
1762
1861
|
if (b.riskEscalation && !a.riskEscalation) loosen("defaults.riskEscalation", b.riskEscalation, a.riskEscalation);
|
|
1763
1862
|
if (updateCheckSetting(before) === "on" && updateCheckSetting(after) === "off") loosen("defaults.updateCheck", updateCheckSetting(before), updateCheckSetting(after));
|
|
1764
1863
|
if (a.sendCaps.perHour > b.sendCaps.perHour || a.sendCaps.perDay > b.sendCaps.perDay) loosen("defaults.sendCaps", b.sendCaps, a.sendCaps);
|
|
1765
|
-
const roots = (list) => new Set(list.map(
|
|
1766
|
-
const
|
|
1864
|
+
const roots = (list) => new Set(list.map((path) => comparablePath(resolve(expandHome(path, homedir())))));
|
|
1865
|
+
const readRoots = (list) => roots(list.filter((root) => namesItsPlace(root)));
|
|
1866
|
+
const before_roots = readRoots(b.attachRoots);
|
|
1767
1867
|
const after_deny = roots(a.attachDeny);
|
|
1768
|
-
if ([...
|
|
1868
|
+
if ([...readRoots(a.attachRoots)].some((root) => !before_roots.has(root))) loosen("defaults.attachRoots", b.attachRoots, a.attachRoots);
|
|
1769
1869
|
if ([...roots(b.attachDeny)].some((deny) => !after_deny.has(deny))) loosen("defaults.attachDeny", b.attachDeny, a.attachDeny);
|
|
1770
1870
|
const downloadsBefore = b.downloadsDir === void 0 ? void 0 : normalisePath(b.downloadsDir);
|
|
1771
1871
|
const downloadsAfter = a.downloadsDir === void 0 ? void 0 : normalisePath(a.downloadsDir);
|
|
@@ -3626,7 +3726,7 @@ async function installManagedRuntime(context, product, version) {
|
|
|
3626
3726
|
private: true
|
|
3627
3727
|
}, null, 2)}\n`);
|
|
3628
3728
|
const npmCli = await findNpmCli();
|
|
3629
|
-
await run(process.execPath, [
|
|
3729
|
+
await run(context.env, process.execPath, [
|
|
3630
3730
|
npmCli,
|
|
3631
3731
|
"install",
|
|
3632
3732
|
"--prefix",
|
|
@@ -3650,10 +3750,12 @@ async function findNpmCli() {
|
|
|
3650
3750
|
throw new CommsError("CONFIG", "npm could not be found next to this Node installation", { hint: "Install with `--launcher npx` instead, which runs the published package directly." });
|
|
3651
3751
|
}
|
|
3652
3752
|
/**
|
|
3653
|
-
* A command run to its end
|
|
3654
|
-
*
|
|
3753
|
+
* A command run to its end, in `env`: the environment the install resolved its target from — which client config,
|
|
3754
|
+
* which client CLI, which node — and not this process's, which may be another. On Windows it also tells the command
|
|
3755
|
+
* never to take a program from its current folder: `npm` and a client's command both start programs of their own by
|
|
3756
|
+
* name.
|
|
3655
3757
|
*/
|
|
3656
|
-
function run(command, args) {
|
|
3758
|
+
function run(env, command, args) {
|
|
3657
3759
|
return new Promise((resolvePromise, reject) => {
|
|
3658
3760
|
const child = spawn(command, args, {
|
|
3659
3761
|
stdio: [
|
|
@@ -3661,7 +3763,7 @@ function run(command, args) {
|
|
|
3661
3763
|
"ignore",
|
|
3662
3764
|
"pipe"
|
|
3663
3765
|
],
|
|
3664
|
-
env: childEnvironment(
|
|
3766
|
+
env: childEnvironment(env)
|
|
3665
3767
|
});
|
|
3666
3768
|
let stderr = "";
|
|
3667
3769
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -3674,14 +3776,19 @@ function run(command, args) {
|
|
|
3674
3776
|
});
|
|
3675
3777
|
});
|
|
3676
3778
|
}
|
|
3677
|
-
|
|
3779
|
+
/**
|
|
3780
|
+
* The entry for one server, starting the command its preflight resolved (`resolved`): what the plan named and the
|
|
3781
|
+
* person approved. Nothing here looks on PATH again — a node or an npx that turned up since would be another command
|
|
3782
|
+
* than the one the preview said.
|
|
3783
|
+
*/
|
|
3784
|
+
async function buildEntry(context, product, options, apply, resolved) {
|
|
3678
3785
|
const launcher = options.launcher ?? "managed";
|
|
3679
|
-
const node =
|
|
3786
|
+
const { node } = resolved;
|
|
3680
3787
|
const env = minimalEnv(context, node);
|
|
3681
3788
|
const serverArgs = product.serverArgs(options);
|
|
3682
3789
|
if (launcher === "npx") return {
|
|
3683
3790
|
entry: {
|
|
3684
|
-
command:
|
|
3791
|
+
command: resolved.command,
|
|
3685
3792
|
args: [
|
|
3686
3793
|
"-y",
|
|
3687
3794
|
`${product.npxPackage}@${product.version}`,
|
|
@@ -3825,6 +3932,13 @@ function claimName(product, options, name, existing) {
|
|
|
3825
3932
|
throw new CommsError("CONFIG", `${options.client} already has an MCP server called "${name}", and it is not this one (it runs ${what})`, { hint: `Register this one under another name: \`${installCommand$1(product, options, other)}\`. --force does not replace a server this did not install.` });
|
|
3826
3933
|
}
|
|
3827
3934
|
if (taken.length > 0 && !options.force) {
|
|
3935
|
+
for (const pin of PIN_OPTIONS) {
|
|
3936
|
+
const wanted = options[pin];
|
|
3937
|
+
const served = taken.map((server) => product.narrowingOf(server.args)[pin]).find(Boolean);
|
|
3938
|
+
if (!wanted || !served || served === wanted) continue;
|
|
3939
|
+
const second = `${product.defaultServerName}-${wanted.split("/")[0]}`;
|
|
3940
|
+
throw new CommsError("CONFIG", `${options.client} already has this server registered as "${name}"`, { hint: `That entry serves ${served}; to serve ${wanted} as well, register a second entry under its own name: \`${installCommand$1(product, options, second === name ? `${name}-2` : second)}\`.` });
|
|
3941
|
+
}
|
|
3828
3942
|
const npx = taken.some((server) => server.args.some((argument) => argument.startsWith(`${product.npxPackage}@`)));
|
|
3829
3943
|
const again = {
|
|
3830
3944
|
...keepNarrowing(product, options, taken).options,
|
|
@@ -3890,9 +4004,10 @@ function installCommand$1(product, options, name, extra = []) {
|
|
|
3890
4004
|
}
|
|
3891
4005
|
/**
|
|
3892
4006
|
* A command's exit status and what it printed, kept here and never shown: codex prints an entry's env. Started as
|
|
3893
|
-
* `run` starts one
|
|
4007
|
+
* `run` starts one — in the environment the target was resolved from — so that on Windows it takes no program from its
|
|
4008
|
+
* current folder either.
|
|
3894
4009
|
*/
|
|
3895
|
-
function capture$1(command, args) {
|
|
4010
|
+
function capture$1(env, command, args) {
|
|
3896
4011
|
return new Promise((resolvePromise, reject) => {
|
|
3897
4012
|
const child = spawn(command, args, {
|
|
3898
4013
|
stdio: [
|
|
@@ -3900,7 +4015,7 @@ function capture$1(command, args) {
|
|
|
3900
4015
|
"pipe",
|
|
3901
4016
|
"pipe"
|
|
3902
4017
|
],
|
|
3903
|
-
env: childEnvironment(
|
|
4018
|
+
env: childEnvironment(env)
|
|
3904
4019
|
});
|
|
3905
4020
|
let stdout = "";
|
|
3906
4021
|
let stderr = "";
|
|
@@ -3928,11 +4043,11 @@ function capture$1(command, args) {
|
|
|
3928
4043
|
* or a failure that is not "no such server", stops the install: not knowing is not the same as nothing there.
|
|
3929
4044
|
* Nothing codex prints is repeated, because its answer carries the entry's env.
|
|
3930
4045
|
*/
|
|
3931
|
-
async function codexRegistration(binary, name, path) {
|
|
4046
|
+
async function codexRegistration(env, binary, name, path) {
|
|
3932
4047
|
const unknown = () => new CommsError("CONFIG", `codex would not say what it has registered as "${name}", so nothing was written`, { hint: `Look with \`codex mcp get ${name}\`. If it is not an older copy of this server, choose another --name; \`--print\` shows the entry to add by hand.` });
|
|
3933
4048
|
let answer;
|
|
3934
4049
|
try {
|
|
3935
|
-
answer = await capture$1(binary, [
|
|
4050
|
+
answer = await capture$1(env, binary, [
|
|
3936
4051
|
"mcp",
|
|
3937
4052
|
"get",
|
|
3938
4053
|
name,
|
|
@@ -4034,21 +4149,48 @@ async function preflightInstall(context, product, options) {
|
|
|
4034
4149
|
}
|
|
4035
4150
|
let previous = writes ? claimName(product, options, name, existing) : [];
|
|
4036
4151
|
if (writes && binary && options.client === "codex") {
|
|
4037
|
-
const reported = await codexRegistration(binary, name, configPath ?? "codex");
|
|
4152
|
+
const reported = await codexRegistration(context.env, binary, name, configPath ?? "codex");
|
|
4038
4153
|
if (reported) previous = claimName(product, options, name, [reported]);
|
|
4039
4154
|
}
|
|
4040
4155
|
const { options: effective, kept } = keepNarrowing(product, options, previous);
|
|
4156
|
+
const node = await resolveNode(context.env);
|
|
4157
|
+
const command = (options.launcher ?? "managed") === "npx" ? await resolveNpx(context.env, node) : node;
|
|
4041
4158
|
return {
|
|
4042
4159
|
scan,
|
|
4043
4160
|
target,
|
|
4044
4161
|
previous,
|
|
4045
4162
|
effective,
|
|
4046
|
-
kept
|
|
4163
|
+
kept,
|
|
4164
|
+
node,
|
|
4165
|
+
command
|
|
4047
4166
|
};
|
|
4048
4167
|
}
|
|
4168
|
+
/**
|
|
4169
|
+
* The `npx` an npx entry starts, as a full path: from PATH, or beside the `node` found, where npm installs it.
|
|
4170
|
+
*
|
|
4171
|
+
* Never the bare name. An entry that says only `npx` is resolved by the client, later, in its own environment — its
|
|
4172
|
+
* PATH, and on Windows its current folder — so the approval would bind a word, and the program run would be whichever
|
|
4173
|
+
* the client found. With no `npx` either way, nothing is written.
|
|
4174
|
+
*/
|
|
4175
|
+
async function resolveNpx(env, node) {
|
|
4176
|
+
const found = await whichExecutable("npx", env) ?? await executableIn("npx", [dirname(node)], env);
|
|
4177
|
+
if (found !== null) return found;
|
|
4178
|
+
throw new CommsError("CONFIG", "npx could not be found, on PATH or beside node, so nothing was written", { hint: "Install Node.js with npm, which brings npx, or register with the managed launcher — the default — which needs no npx." });
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Where an install puts the entry and what the entry starts, as one sentence of the preview a person approves: the
|
|
4182
|
+
* client's own config file and the command, each with the home written `~`. In the approval's digest with the rest, so
|
|
4183
|
+
* a claim from an environment that resolves another file or another command — `CLAUDE_CONFIG_DIR` set to another
|
|
4184
|
+
* account's, a PATH that finds another node — is another change, and refused.
|
|
4185
|
+
*/
|
|
4186
|
+
function entryDestination(client, target, command, env) {
|
|
4187
|
+
const home = homeDirectory(env);
|
|
4188
|
+
const file = target.own ?? target.configPath;
|
|
4189
|
+
return `the entry goes in ${file === void 0 ? `${client}'s own configuration` : shortenHome(file, home)}, and ${client} will start it with ${shortenHome(command, home)}`;
|
|
4190
|
+
}
|
|
4049
4191
|
/** The plan a preflight arrived at, in the form `mcpInstall` compares: see `PlannedInstall`. */
|
|
4050
4192
|
function plannedInstall(preflight) {
|
|
4051
|
-
const { target, previous, effective } = preflight;
|
|
4193
|
+
const { target, previous, effective, node, command } = preflight;
|
|
4052
4194
|
return {
|
|
4053
4195
|
writes: target.writes,
|
|
4054
4196
|
cliName: target.cliName,
|
|
@@ -4070,7 +4212,9 @@ function plannedInstall(preflight) {
|
|
|
4070
4212
|
inbox: effective.inbox ?? null,
|
|
4071
4213
|
workspace: effective.workspace ?? null,
|
|
4072
4214
|
readOnly: effective.readOnly === true
|
|
4073
|
-
}
|
|
4215
|
+
},
|
|
4216
|
+
node,
|
|
4217
|
+
command
|
|
4074
4218
|
};
|
|
4075
4219
|
}
|
|
4076
4220
|
/**
|
|
@@ -4094,6 +4238,8 @@ function planDrift(product, client, name, planned, now) {
|
|
|
4094
4238
|
}).join(" ") || "no pin";
|
|
4095
4239
|
drift.push(`the server would start with ${flags(now.narrowing)}, not ${flags(planned.narrowing)}`);
|
|
4096
4240
|
}
|
|
4241
|
+
if (planned.command !== now.command) drift.push(`it would start the server with ${now.command}, not ${planned.command}`);
|
|
4242
|
+
else if (planned.node !== now.node) drift.push(`it would run under the node at ${now.node}, not ${planned.node}`);
|
|
4097
4243
|
return drift;
|
|
4098
4244
|
}
|
|
4099
4245
|
/** The planned pin and `--read-only`, as the options that start the server: each one given, none left to be found. */
|
|
@@ -4137,7 +4283,7 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4137
4283
|
if (!writes) {
|
|
4138
4284
|
if (existing.find((server) => server.client === options.client && server.name === name && server.scope !== "project" && !isProductServer(server, product))) warnings.push(`${options.client} already has a different server called "${name}"; pasting this would replace it. Use --name to choose another name.`);
|
|
4139
4285
|
}
|
|
4140
|
-
const { entry, launcher, runtimeMissing } = await buildEntry(context, product, effective, apply);
|
|
4286
|
+
const { entry, launcher, runtimeMissing } = await buildEntry(context, product, effective, apply, preflight);
|
|
4141
4287
|
const snippet = snippetFor(options.client, name, entry);
|
|
4142
4288
|
const backupPath = previous.length > 0 ? await backUp(context, options.client, name, previous, /* @__PURE__ */ new Date()) : void 0;
|
|
4143
4289
|
const ledger = handedOutRuntimesPath(context.core.paths.dataDir);
|
|
@@ -4191,13 +4337,13 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4191
4337
|
name
|
|
4192
4338
|
];
|
|
4193
4339
|
try {
|
|
4194
|
-
await run(binary, removal);
|
|
4340
|
+
await run(context.env, binary, removal);
|
|
4195
4341
|
} catch (error) {
|
|
4196
4342
|
const message = error instanceof CommsError ? error.message : String(error);
|
|
4197
4343
|
if (!/no (mcp )?server|not found|does not exist/i.test(message)) throw error;
|
|
4198
4344
|
}
|
|
4199
4345
|
try {
|
|
4200
|
-
await run(binary, args);
|
|
4346
|
+
await run(context.env, binary, args);
|
|
4201
4347
|
} catch (error) {
|
|
4202
4348
|
const restore = options.client === "claude-code" ? [
|
|
4203
4349
|
"mcp",
|
|
@@ -4221,7 +4367,7 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4221
4367
|
];
|
|
4222
4368
|
let restored = true;
|
|
4223
4369
|
try {
|
|
4224
|
-
await run(binary, restore);
|
|
4370
|
+
await run(context.env, binary, restore);
|
|
4225
4371
|
} catch {
|
|
4226
4372
|
restored = false;
|
|
4227
4373
|
}
|
|
@@ -4231,7 +4377,7 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4231
4377
|
});
|
|
4232
4378
|
}
|
|
4233
4379
|
} else try {
|
|
4234
|
-
await run(binary, args);
|
|
4380
|
+
await run(context.env, binary, args);
|
|
4235
4381
|
} catch (error) {
|
|
4236
4382
|
const message = error instanceof CommsError ? error.message : String(error);
|
|
4237
4383
|
if (/already exists/i.test(message)) throw new CommsError("CONFIG", `${cliName} already has an MCP server called "${name}", somewhere this could not read`, {
|
|
@@ -4720,62 +4866,6 @@ function requireChannelManifest(channel) {
|
|
|
4720
4866
|
if (manifest === void 0) throw notAChannel(channel);
|
|
4721
4867
|
return manifest;
|
|
4722
4868
|
}
|
|
4723
|
-
/**
|
|
4724
|
-
* One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
|
|
4725
|
-
* and the preview renderer (which makes them visible), so the two can never disagree.
|
|
4726
|
-
*/
|
|
4727
|
-
/** Zero-width and formatting characters, bidi controls, variation selectors and Unicode tag characters. */
|
|
4728
|
-
const INVISIBLE_RANGES = [
|
|
4729
|
-
[173, 173],
|
|
4730
|
-
[847, 847],
|
|
4731
|
-
[1564, 1564],
|
|
4732
|
-
[4447, 4448],
|
|
4733
|
-
[6068, 6069],
|
|
4734
|
-
[6155, 6159],
|
|
4735
|
-
[8203, 8207],
|
|
4736
|
-
[8232, 8233],
|
|
4737
|
-
[8234, 8238],
|
|
4738
|
-
[8288, 8292],
|
|
4739
|
-
[8294, 8303],
|
|
4740
|
-
[12644, 12644],
|
|
4741
|
-
[65024, 65039],
|
|
4742
|
-
[65279, 65279],
|
|
4743
|
-
[65440, 65440],
|
|
4744
|
-
[917504, 917631]
|
|
4745
|
-
];
|
|
4746
|
-
function isInvisible(codePoint) {
|
|
4747
|
-
for (const [from, to] of INVISIBLE_RANGES) if (codePoint >= from && codePoint <= to) return true;
|
|
4748
|
-
return false;
|
|
4749
|
-
}
|
|
4750
|
-
/**
|
|
4751
|
-
* C0 controls except tab and newline (ESC, and so every ANSI/OSC sequence, starts here), DEL, and C1 controls (CSI
|
|
4752
|
-
* among them). A lone carriage return counts: it moves a terminal cursor back over text already printed.
|
|
4753
|
-
*/
|
|
4754
|
-
function isControl(codePoint) {
|
|
4755
|
-
if (codePoint === 9 || codePoint === 10) return false;
|
|
4756
|
-
return codePoint < 32 || codePoint === 127 || codePoint >= 128 && codePoint <= 159;
|
|
4757
|
-
}
|
|
4758
|
-
function isDangerous(codePoint) {
|
|
4759
|
-
return isControl(codePoint) || isInvisible(codePoint);
|
|
4760
|
-
}
|
|
4761
|
-
/**
|
|
4762
|
-
* Strips control characters (ESC, CSI, OSC and the rest), DEL, lone carriage returns, zero-width, bidi-control and tag
|
|
4763
|
-
* characters from sender-controlled text; returns the text and how many were removed. CRLF becomes LF first.
|
|
4764
|
-
*
|
|
4765
|
-
* This lives beside the table rather than in the sanitiser because `neutralise` needs it too, and the two must not
|
|
4766
|
-
* drift: a pattern that looks for `</untrusted-content` cannot see it through a zero-width space, so stripping
|
|
4767
|
-
* has to happen before any such pattern runs, on every path, not only on the ones that render a body.
|
|
4768
|
-
*/
|
|
4769
|
-
function stripInvisible(text) {
|
|
4770
|
-
let removed = 0;
|
|
4771
|
-
let out = "";
|
|
4772
|
-
for (const char of text.replace(/\r\n/g, "\n")) if (isDangerous(char.codePointAt(0) ?? 0)) removed += 1;
|
|
4773
|
-
else out += char;
|
|
4774
|
-
return {
|
|
4775
|
-
text: out,
|
|
4776
|
-
removed
|
|
4777
|
-
};
|
|
4778
|
-
}
|
|
4779
4869
|
function okEnvelope(data) {
|
|
4780
4870
|
return {
|
|
4781
4871
|
ok: true,
|
|
@@ -5958,7 +6048,7 @@ function isPrerelease(version) {
|
|
|
5958
6048
|
return VERSION_PATTERN.exec(version)?.[4] !== void 0;
|
|
5959
6049
|
}
|
|
5960
6050
|
/** The package version, read from package.json at build time. */
|
|
5961
|
-
const VERSION = "0.
|
|
6051
|
+
const VERSION = "0.12.0";
|
|
5962
6052
|
/**
|
|
5963
6053
|
* Registering, pruning and listing the MCP servers of every channel, from the core.
|
|
5964
6054
|
*
|
|
@@ -6133,9 +6223,10 @@ function installOptions(request) {
|
|
|
6133
6223
|
*
|
|
6134
6224
|
* `own` is the calling channel's product, from its own `mcp install`: see `ownProduct`. The sentences depend on the
|
|
6135
6225
|
* request and the machine, not on which surface prepared them, so an approval prepared by one is claimed by the
|
|
6136
|
-
* other. The
|
|
6137
|
-
*
|
|
6138
|
-
*
|
|
6226
|
+
* other. The sentences that name code name it as this environment resolves it: the client's config file and the node
|
|
6227
|
+
* or npx the entry starts (#46), and for `--launcher local` the checkout's file. A claim from an environment that
|
|
6228
|
+
* resolves another — `CLAUDE_CONFIG_DIR` set to another account's, a PATH that finds another node, a core running
|
|
6229
|
+
* from another build of the checkout — is another registration, refused as one.
|
|
6139
6230
|
*/
|
|
6140
6231
|
function serverInstallChange(core, env, request, own) {
|
|
6141
6232
|
checkRequest(request);
|
|
@@ -6159,7 +6250,7 @@ function serverInstallChange(core, env, request, own) {
|
|
|
6159
6250
|
const product = await productNow();
|
|
6160
6251
|
const { version } = product;
|
|
6161
6252
|
const preflight = await preflightInstall(context, product, installOptions(request));
|
|
6162
|
-
const { target, previous, effective, kept } = preflight;
|
|
6253
|
+
const { target, previous, effective, kept, command } = preflight;
|
|
6163
6254
|
planned = {
|
|
6164
6255
|
product,
|
|
6165
6256
|
install: plannedInstall(preflight)
|
|
@@ -6168,12 +6259,14 @@ function serverInstallChange(core, env, request, own) {
|
|
|
6168
6259
|
if (target.writes) {
|
|
6169
6260
|
const pinned = pinName === void 0 || pinName === "readOnly" ? void 0 : effective[pinName];
|
|
6170
6261
|
const pins = [pinned !== void 0 ? `pinned to the ${noun} ${pinned}` : "", effective.readOnly ? "read-only" : ""].filter(Boolean);
|
|
6171
|
-
const
|
|
6262
|
+
const what = (pinName === void 0 || pinName === "readOnly" ? [] : [...new Set(previous.map((server) => facts.narrowingOf(server.args)[pinName] ?? null))]).map((pin) => pin === null ? `every ${noun}` : `the ${noun} ${pin}`).join(" and ");
|
|
6263
|
+
const replacing = previous.length > 0 ? `, replacing its own earlier entry of that name${what ? `, which served ${what}${kept.length > 0 ? "," : ""}` : ""}${kept.length > 0 ? ` and keeping ${kept.join(" ")} from it` : ""}` : "";
|
|
6172
6264
|
effects.push(`registers the ${label} MCP server with ${request.client} as "${name}"${pins.length > 0 ? `, ${pins.join(", ")}` : ""}${replacing}`);
|
|
6173
6265
|
const everyAccount = pinName !== void 0 && pinned === void 0;
|
|
6174
6266
|
const everyTool = switchable && effective.readOnly !== true;
|
|
6175
6267
|
if (everyAccount) effects.push(`not pinned: it reaches every ${noun} on this machine${everyTool ? ", with every tool" : ""}`);
|
|
6176
6268
|
else if (everyTool) effects.push(`not read-only: it has every tool for ${pinned}, including those that change it`);
|
|
6269
|
+
effects.push(entryDestination(request.client, target, command, env));
|
|
6177
6270
|
if (launcher === "npx") effects.push(`${request.client} will fetch ${facts.npxPackage}@${version} from npm each time it starts it`);
|
|
6178
6271
|
else if (launcher === "local") effects.push(`${request.client} will start it from ${await localCliEntry(product.moduleUrl)}`);
|
|
6179
6272
|
}
|
|
@@ -6213,7 +6306,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
6213
6306
|
};
|
|
6214
6307
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
6215
6308
|
packageName: channelServer(request.channel).packageName,
|
|
6216
|
-
version: "0.
|
|
6309
|
+
version: "0.12.0"
|
|
6217
6310
|
};
|
|
6218
6311
|
const product = {
|
|
6219
6312
|
packageName,
|
|
@@ -6552,7 +6645,6 @@ function updateLaterChange(core, options = {}) {
|
|
|
6552
6645
|
}
|
|
6553
6646
|
};
|
|
6554
6647
|
}
|
|
6555
|
-
channelServer("core").packageName;
|
|
6556
6648
|
Object.freeze({
|
|
6557
6649
|
kind: "send",
|
|
6558
6650
|
lookup: true
|
|
@@ -6836,8 +6928,9 @@ async function askLine(streams, question) {
|
|
|
6836
6928
|
rl.close();
|
|
6837
6929
|
}
|
|
6838
6930
|
}
|
|
6931
|
+
channelServer("core").packageName;
|
|
6839
6932
|
//#endregion
|
|
6840
|
-
//#region ../core/dist/tool-arguments-
|
|
6933
|
+
//#region ../core/dist/tool-arguments-Bc-xUPsF.mjs
|
|
6841
6934
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
6842
6935
|
const TARGET = "draft-2020-12";
|
|
6843
6936
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcomms/whatsapp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "WhatsApp for coding agents, read-only: list, read and search the chats WhatsApp for Mac keeps on this Mac, and draft replies as links the person sends. No network client; it never sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
39
39
|
"commander": "^15.0.0",
|
|
40
40
|
"zod": "^4.6.5",
|
|
41
|
-
"@agentcomms/core": "0.
|
|
41
|
+
"@agentcomms/core": "0.12.0"
|
|
42
42
|
},
|
|
43
43
|
"agentcomms": {
|
|
44
44
|
"contract": 1,
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"typecheck": "tsc -p tsconfig.json",
|
|
88
88
|
"test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test --test-timeout=600000 \"test/**/*.test.ts\""
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "2ef75fb4b6d69daee9c50f4ef8fbe87c9c75754d"
|
|
91
91
|
}
|