@agentcomms/whatsapp 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as exemptFromUpdateGate, A as requireAccount, B as PATH_OPTIONS, Ct as writeResult, Et as VERSION, F as expandSource, G as approveAndWaitSentence, H as absoluteSearchPath, J as childEnvironment, K as approveChangeAtTerminal, L as CHANNEL_SERVERS, M as storeIdentity, N as checkStorePath, O as PLATFORM, P as defaultStorePath, Q as escapeForDisplay, R as CommsError, S as chatRefOf, St as withFileLock, U as agentMarker, W as approvalsOf, X as colorEnabled, Y as cliHandoffs, Z as commandPathOf, _ as WhatsAppContext, a as CHAT_KINDS, at as lookupName, b as requireSupportedNode, c as searchMessages, ct as openCore, dt as renderInstall, et as gatedChangeAtTerminal, f as draftMessage, ft as renderPrune, gt as serverPruneChange, ht as serverInstallChange, i as probeStore, it as isCommsError, j as requireNamedConfig, k as newWhatsAppAccount, l as Presenter, lt as paint, m as listKey, mt as runCommand, nt as handoffText, o as listChats, ot as nameAvailable, p as Visibility, pt as resolvePaths, q as canPrompt, r as whatsappStatus, rt as installExitStatus, s as readChat, st as nameShapeProblem, t as syncAccount, tt as handoffSentence, u as innerText, ut as pathOverridesFromCliOptions, v as WhatsAppIndex, vt as truncateDisplay, wt as CALLER, xt as windowsSystemProgram, yt as updateGateAtTerminal, z as EXIT_CODES } from "./sync-q1U9jYX3.mjs";
2
+ import { $ as exemptFromUpdateGate, A as requireAccount, B as PATH_OPTIONS, Ct as writeResult, Et as VERSION, F as expandSource, G as approveAndWaitSentence, H as absoluteSearchPath, J as childEnvironment, K as approveChangeAtTerminal, L as CHANNEL_SERVERS, M as storeIdentity, N as checkStorePath, O as PLATFORM, P as defaultStorePath, Q as escapeForDisplay, R as CommsError, S as chatRefOf, St as withFileLock, U as agentMarker, W as approvalsOf, X as colorEnabled, Y as cliHandoffs, Z as commandPathOf, _ as WhatsAppContext, a as CHAT_KINDS, at as lookupName, b as requireSupportedNode, c as searchMessages, ct as openCore, dt as renderInstall, et as gatedChangeAtTerminal, f as draftMessage, ft as renderPrune, gt as serverPruneChange, ht as serverInstallChange, i as probeStore, it as isCommsError, j as requireNamedConfig, k as newWhatsAppAccount, l as Presenter, lt as paint, m as listKey, mt as runCommand, nt as handoffText, o as listChats, ot as nameAvailable, p as Visibility, pt as resolvePaths, q as canPrompt, r as whatsappStatus, rt as installExitStatus, s as readChat, st as nameShapeProblem, t as syncAccount, tt as handoffSentence, u as innerText, ut as pathOverridesFromCliOptions, v as WhatsAppIndex, vt as truncateDisplay, wt as CALLER, xt as windowsSystemProgram, yt as updateGateAtTerminal, z as EXIT_CODES } from "./sync-Q9xVuh21.mjs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import path, { delimiter, join } from "node:path";
5
5
  import { stripVTControlCharacters } from "node:util";
@@ -3582,7 +3582,7 @@ a macOS dialog may be waiting) · 77 permission needed (macOS privacy) · 78 con
3582
3582
  ran = true;
3583
3583
  exitCode = await runCommand(output(), async () => {
3584
3584
  requireSupportedNode();
3585
- await (deps.startMcp ?? (await import("./stdio-entry-DeaLN63P.mjs")).startWhatsAppStdioServer)({
3585
+ await (deps.startMcp ?? (await import("./stdio-entry-c95dpKwO.mjs")).startWhatsAppStdioServer)({
3586
3586
  env,
3587
3587
  platform,
3588
3588
  ...deps,
package/dist/index.d.mts CHANGED
@@ -4277,7 +4277,8 @@ declare namespace external_d_exports {
4277
4277
  export { $brand, $input, $output, BRAND, CompileOptions, GlobalMeta, INVALID, Infer$1 as Infer, IssueData, JSONType, NEVER, $RefinementCtx as RefinementCtx, SafeExtendShape, Schema, TimePrecision, TypeOf, ZodAny, ZodArray, ZodBase64, ZodBase64URL, ZodBigInt, ZodBigIntFormat, ZodBoolean, ZodCIDRv4, ZodCIDRv6, ZodCUID, ZodCUID2, ZodCatch, ZodCodec, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, ZodCoercedNumber, ZodCoercedString, ZodCompileAsyncError, ZodCompileUnsupportedError, ZodCreditCard, ZodCustom, ZodCustomStringFormat, ZodDate, ZodDefault, ZodDiscriminatedUnion, ZodE164, ZodEmail, ZodEmoji, ZodEnum, ZodError, ZodErrorMap, ZodExactOptional, ZodFile, ZodFirstPartySchemaTypes, ZodFirstPartyTypeKind, ZodFlattenedError, ZodFloat32, ZodFloat64, ZodFormattedError, ZodFunction, ZodGUID, ZodIBAN, ZodIPv4, ZodIPv6, ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime, ZodInstanceOf, ZodInt, ZodInt32, ZodInt64, ZodIntersection, ZodIssue, ZodIssueCode, ZodJSONSchema, ZodJSONSchemaInternals, ZodJWT, ZodKSUID, ZodLazy, ZodLiteral, ZodMAC, ZodMap, ZodNaN, ZodNanoID, ZodNever, ZodNonOptional, ZodNull, ZodNullable, ZodNumber, ZodNumberFormat, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPreprocess, ZodPromise, ZodRawShape$1 as ZodRawShape, ZodReadonly, ZodRealError, ZodRecord, ZodSafeParseError, ZodSafeParseResult, ZodSafeParseSuccess, ZodSchema, ZodSet, ZodStandardSchemaWithJSON, ZodString, ZodStringFormat, ZodSuccess, ZodSymbol, ZodTemplateLiteral, ZodTransform, ZodTuple, ZodType, ZodTypeAny, ZodUInt32, ZodUInt64, ZodULID, ZodURL, ZodUUID, ZodUndefined, ZodUnion, ZodUnknown, ZodVoid, ZodXID, ZodXor, _ZodBigInt, _ZodBoolean, _ZodDate, _ZodNumber, _ZodString, _ZodType, _default, _function, any, array, base64, base64url, bigint$1 as bigint, boolean$1 as boolean, _catch as catch, check, cidrv4, cidrv6, clone, codec, coerce_d_exports as coerce, compile, config, index_d_exports$1 as core, creditCard, cuid, cuid2, currencyCode, custom, date$2 as date, decode, decodeAsync, deepPartial, describe, discriminatedUnion, e164, email, emoji, encode, encodeAsync, _endsWith as endsWith, _enum as enum, exactOptional, file, flattenError, float32, float64, formatError, fromJSONSchema, _function as function, getDiscriminatedOption, getErrorMap, globalRegistry, _gt as gt, _gte as gte, guid, hash, hex, hostname, httpUrl, iban, _includes as includes, output$1 as infer, inferFlattenedErrors, inferFormattedError, input, _instanceof as instanceof, int, int32, int64, intersection, invertCodec, ipv4, ipv6, iso_d_exports as iso, json, jwt, keyof, ksuid, lazy, _length as length, literal, index_d_exports$2 as locales, looseObject, looseRecord, _lowercase as lowercase, _lt as lt, _lte as lte, mac, map, _maxLength as maxLength, _maxSize as maxSize, memoizer, meta, _mime as mime, _minLength as minLength, _minSize as minSize, _multipleOf as multipleOf, nan, nanoid, nativeEnum, _negative as negative, never, _nonnegative as nonnegative, nonoptional, _nonpositive as nonpositive, _normalize as normalize, _null as null, nullable, nullish, number$1 as number, object, optional, output, _overwrite as overwrite, parse, parseAsync, partialRecord, pipe, _positive as positive, prefault, preprocess, prettifyError, promise, _properties as properties, _property as property, readonly, record, refine, _regex as regex, regexes_d_exports as regexes, registry, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync, set, setErrorMap, _size as size, _slugify as slugify, _startsWith as startsWith, strictObject, string$1 as string, stringFormat, stringbool, success, superRefine, symbol, templateLiteral, toJSONSchema, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, toZod, transform, treeifyError, _trim as trim, tuple, uint32, uint64, ulid, _undefined as undefined, union, unknown, _uppercase as uppercase, url, util_d_exports as util, uuid, uuidv4, uuidv6, uuidv7, validate, validateAsync, _void as void, withParser, xid, xor };
4278
4278
  }
4279
4279
  //#endregion
4280
- //#region ../core/dist/change-flow-j65bYTxL.d.mts
4280
+ //#region ../core/dist/change-flow-DOyBTYin.d.mts
4281
+ //#endregion
4281
4282
  //#region src/config-hooks.d.ts
4282
4283
  /**
4283
4284
  * Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
@@ -4735,6 +4736,16 @@ interface ConfigBody {
4735
4736
  /** Superseded rotating credentials which still need conclusive per-token cleanup. */
4736
4737
  pendingRevocations?: PendingRevocation[] | undefined;
4737
4738
  defaults: Defaults;
4739
+ /**
4740
+ * How what an agent sends is labelled (design 2026-10-15-agent-label, L5): the word, the name, the rules that say
4741
+ * who a message appears to come from, and each Slack account's agent emoji.
4742
+ *
4743
+ * `unknown` here, on purpose: it is read only through `agentLabelOf`, which reads a value it does not understand
4744
+ * as the stricter one — an unknown word as `agent`, a rule list it cannot read as no rules — so a hand edit or a
4745
+ * later release's key can only tighten. Kept through every write as the schemas keep every key they do not name, and
4746
+ * judged by `classifyChange`.
4747
+ */
4748
+ agentLabel?: unknown;
4738
4749
  }
4739
4750
  interface ConfigV1 extends ConfigBody {
4740
4751
  version: 1;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { C as chatKindOf, D as text, E as numeric, Et as VERSION, R as CommsError, T as kindOf, Tt as PACKAGE_NAME, _ as WhatsAppContext, d as composeDraft, g as visibilityUnitKey, h as statusAuthor, m as listKey, n as withCheckedCopy, p as Visibility, w as coreDataToIso, x as inspectSchema, y as rebuildIndex } from "./sync-q1U9jYX3.mjs";
2
- import { t as createWhatsAppMcpServer } from "./server-DeV9ut_P.mjs";
1
+ import { C as chatKindOf, D as text, E as numeric, Et as VERSION, R as CommsError, T as kindOf, Tt as PACKAGE_NAME, _ as WhatsAppContext, d as composeDraft, g as visibilityUnitKey, h as statusAuthor, m as listKey, n as withCheckedCopy, p as Visibility, w as coreDataToIso, x as inspectSchema, y as rebuildIndex } from "./sync-Q9xVuh21.mjs";
2
+ import { t as createWhatsAppMcpServer } from "./server-YS_r6FYq.mjs";
3
3
  //#region src/source/event-reader.ts
4
4
  function rawFromMe(value) {
5
5
  const number = numeric(value);
@@ -1,4 +1,4 @@
1
- import { Et as VERSION, I as strictToolArguments, V as UNTRUSTED_NOTICE, _ as WhatsAppContext, _t as toCommsError, a as CHAT_KINDS, bt as updateToolGate, c as searchMessages, f as draftMessage, o as listChats, r as whatsappStatus, s as readChat, t as syncAccount } from "./sync-q1U9jYX3.mjs";
1
+ import { Et as VERSION, I as strictToolArguments, V as UNTRUSTED_NOTICE, _ as WhatsAppContext, _t as toCommsError, a as CHAT_KINDS, bt as updateToolGate, c as searchMessages, f as draftMessage, o as listChats, r as whatsappStatus, s as readChat, t as syncAccount } from "./sync-Q9xVuh21.mjs";
2
2
  import { c as boolean, h as number, x as string } from "./iso-USuVU_sk.mjs";
3
3
  import { t as McpServer } from "./mcp-DIH4cS6P-CIWWZQbz.mjs";
4
4
  //#region src/mcp/server.ts
@@ -1,4 +1,4 @@
1
- import { t as createWhatsAppMcpServer } from "./server-DeV9ut_P.mjs";
1
+ import { t as createWhatsAppMcpServer } from "./server-YS_r6FYq.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
@@ -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.18.1";
14
+ const VERSION$1 = "0.19.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-DvZxdFU_.mjs
31
+ //#region ../core/dist/update-check-CVvpp5G8.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
@@ -903,6 +903,175 @@ var TaintStore = class {
903
903
  return sortedOrigins([...originEntry(sidecar), ...sidecar !== void 0 && isNewer(entry.at, sidecar.at) ? ["read"] : []]);
904
904
  }
905
905
  };
906
+ /**
907
+ * One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
908
+ * and the preview renderer (which makes them visible), so the two can never disagree.
909
+ */
910
+ /** Zero-width and formatting characters, bidi controls, variation selectors and Unicode tag characters. */
911
+ const INVISIBLE_RANGES = [
912
+ [173, 173],
913
+ [847, 847],
914
+ [1564, 1564],
915
+ [4447, 4448],
916
+ [6068, 6069],
917
+ [6155, 6159],
918
+ [8203, 8207],
919
+ [8232, 8233],
920
+ [8234, 8238],
921
+ [8288, 8292],
922
+ [8294, 8303],
923
+ [12644, 12644],
924
+ [65024, 65039],
925
+ [65279, 65279],
926
+ [65440, 65440],
927
+ [917504, 917631]
928
+ ];
929
+ function isInvisible(codePoint) {
930
+ for (const [from, to] of INVISIBLE_RANGES) if (codePoint >= from && codePoint <= to) return true;
931
+ return false;
932
+ }
933
+ /**
934
+ * C0 controls except tab and newline (ESC, and so every ANSI/OSC sequence, starts here), DEL, and C1 controls (CSI
935
+ * among them). A lone carriage return counts: it moves a terminal cursor back over text already printed.
936
+ */
937
+ function isControl(codePoint) {
938
+ if (codePoint === 9 || codePoint === 10) return false;
939
+ return codePoint < 32 || codePoint === 127 || codePoint >= 128 && codePoint <= 159;
940
+ }
941
+ function isDangerous(codePoint) {
942
+ return isControl(codePoint) || isInvisible(codePoint);
943
+ }
944
+ /**
945
+ * Strips control characters (ESC, CSI, OSC and the rest), DEL, lone carriage returns, zero-width, bidi-control and tag
946
+ * characters from sender-controlled text; returns the text and how many were removed. CRLF becomes LF first.
947
+ *
948
+ * This lives beside the table rather than in the sanitiser because `neutralise` needs it too, and the two must not
949
+ * drift: a pattern that looks for `</untrusted-content` cannot see it through a zero-width space, so stripping
950
+ * has to happen before any such pattern runs, on every path, not only on the ones that render a body.
951
+ */
952
+ function stripInvisible(text) {
953
+ let removed = 0;
954
+ let out = "";
955
+ for (const char of text.replace(/\r\n/g, "\n")) if (isDangerous(char.codePointAt(0) ?? 0)) removed += 1;
956
+ else out += char;
957
+ return {
958
+ text: out,
959
+ removed
960
+ };
961
+ }
962
+ /**
963
+ * Labelling what an agent sends (design 2026-10-15-agent-label): the phrase `<name> (<word>)`, who a message appears to
964
+ * come from, and which setting changes loosen that. Pure: nothing here reads the configuration or talks to a channel,
965
+ * so every channel decides the same way from the same settings.
966
+ */
967
+ /** L2: the words a label may use, exactly as written. Adding one is a code change with its own review. */
968
+ const AGENT_LABEL_WORDS = Object.freeze([
969
+ "agent",
970
+ "assistant",
971
+ "twin",
972
+ "minion",
973
+ "bot",
974
+ "fake",
975
+ "AI"
976
+ ]);
977
+ /** L4: the kinds of act a rule may name. Slack posts, replies, reacts, edits and shares files; mail sends and replies. */
978
+ const ACT_KINDS = Object.freeze([
979
+ "post",
980
+ "reply",
981
+ "reaction",
982
+ "edit",
983
+ "file",
984
+ "send"
985
+ ]);
986
+ /** L2's characters a name may not hold: parentheses would fake a second label, the rest break an address header. */
987
+ const FORBIDDEN_NAME_CHARACTERS = /[()<>"@,;:\\]/u;
988
+ function isAgentLabelWord(value) {
989
+ return typeof value === "string" && AGENT_LABEL_WORDS.includes(value);
990
+ }
991
+ /** Why a name cannot be labelled, or undefined when it can. */
992
+ function nameProblem(name) {
993
+ if (name.length === 0) return "the name is empty";
994
+ if (name.trim() !== name) return "the name starts or ends with a space";
995
+ if (FORBIDDEN_NAME_CHARACTERS.test(name)) return "the name holds a parenthesis or one of <>\"@,;:\\";
996
+ for (const char of name) if (isDangerous(char.codePointAt(0) ?? 0)) return "the name holds a hidden or control character";
997
+ }
998
+ const ruleKey = (rule) => JSON.stringify([
999
+ rule.account,
1000
+ rule.accountId ?? null,
1001
+ rule.conversation ?? null,
1002
+ rule.kinds === void 0 ? null : [...rule.kinds].sort(),
1003
+ rule.as
1004
+ ]);
1005
+ /**
1006
+ * L5: the changes that need the person's confirmation — anything that could leave a message unlabelled or change how
1007
+ * the label looks. Adding an `agent` rule or removing a `me` rule tightens, and is not listed.
1008
+ */
1009
+ function classifyAgentLabelChange(before, after) {
1010
+ const changes = [];
1011
+ const wordBefore = before?.word ?? "agent";
1012
+ const wordAfter = after?.word ?? "agent";
1013
+ if (wordBefore !== wordAfter) changes.push({
1014
+ path: "agentLabel.word",
1015
+ was: wordBefore,
1016
+ now: wordAfter
1017
+ });
1018
+ if ((before?.name ?? null) !== (after?.name ?? null)) changes.push({
1019
+ path: "agentLabel.name",
1020
+ was: before?.name,
1021
+ now: after?.name
1022
+ });
1023
+ const rulesBefore = new Map((before?.rules ?? []).map((rule) => [ruleKey(rule), rule]));
1024
+ const rulesAfter = new Map((after?.rules ?? []).map((rule) => [ruleKey(rule), rule]));
1025
+ const meAdded = [...rulesAfter].some(([key, rule]) => rule.as === "me" && !rulesBefore.has(key));
1026
+ const agentRemoved = [...rulesBefore].some(([key, rule]) => rule.as === "agent" && !rulesAfter.has(key));
1027
+ if (meAdded || agentRemoved) changes.push({
1028
+ path: "agentLabel.rules",
1029
+ was: before?.rules ?? [],
1030
+ now: after?.rules ?? []
1031
+ });
1032
+ if (JSON.stringify(sorted(before?.slackIcons)) !== JSON.stringify(sorted(after?.slackIcons))) changes.push({
1033
+ path: "agentLabel.slackIcons",
1034
+ was: before?.slackIcons ?? {},
1035
+ now: after?.slackIcons ?? {}
1036
+ });
1037
+ return changes;
1038
+ }
1039
+ function sorted(record) {
1040
+ return Object.entries(record ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1041
+ }
1042
+ const EMOJI_NAME = /^:[a-z0-9_+'-]{1,100}:$/u;
1043
+ function isRule(value) {
1044
+ if (typeof value !== "object" || value === null) return false;
1045
+ const rule = value;
1046
+ if (typeof rule.account !== "string" || rule.account.length === 0) return false;
1047
+ if (rule.as !== "agent" && rule.as !== "me") return false;
1048
+ if (rule.accountId !== void 0 && (typeof rule.accountId !== "string" || rule.accountId.length === 0)) return false;
1049
+ if (rule.conversation !== void 0 && (typeof rule.conversation !== "string" || rule.conversation.length === 0)) return false;
1050
+ if (rule.kinds !== void 0 && !(Array.isArray(rule.kinds) && rule.kinds.length > 0 && rule.kinds.every((kind) => ACT_KINDS.includes(kind)))) return false;
1051
+ return true;
1052
+ }
1053
+ /**
1054
+ * The stored `agentLabel`, read so that a bad value can only tighten. An unknown word reads as `agent`, an invalid
1055
+ * name as no name, and a rule list with any invalid rule is ignored whole: dropping one `agent` rule could leave a
1056
+ * broader `me` rule in charge. A malformed icon entry is dropped, falling back to the plain photo.
1057
+ */
1058
+ function readAgentLabelSettings(raw) {
1059
+ if (typeof raw !== "object" || raw === null) return {};
1060
+ const stored = raw;
1061
+ const settings = {};
1062
+ if (isAgentLabelWord(stored.word)) settings.word = stored.word;
1063
+ if (typeof stored.name === "string" && nameProblem(stored.name) === void 0) settings.name = stored.name;
1064
+ if (Array.isArray(stored.rules) && stored.rules.every(isRule)) settings.rules = stored.rules.map((rule) => ({ ...rule }));
1065
+ if (typeof stored.slackIcons === "object" && stored.slackIcons !== null) {
1066
+ const icons = Object.entries(stored.slackIcons).filter((entry) => typeof entry[1] === "string" && EMOJI_NAME.test(entry[1]));
1067
+ if (icons.length > 0) settings.slackIcons = Object.fromEntries(icons);
1068
+ }
1069
+ return settings;
1070
+ }
1071
+ /** The person's label settings from a loaded configuration, read as `readAgentLabelSettings` reads them. */
1072
+ function agentLabelOf(config) {
1073
+ return readAgentLabelSettings(config.agentLabel);
1074
+ }
906
1075
  /** Every first-party channel, the core first: what core knows about each without installing it. */
907
1076
  const CHANNEL_SNAPSHOT = [
908
1077
  {
@@ -1072,7 +1241,11 @@ const CHANNEL_SNAPSHOT = [
1072
1241
  word: "slack",
1073
1242
  can: "post to Slack"
1074
1243
  },
1075
- hosts: ["slack.com", "files.slack.com"],
1244
+ hosts: [
1245
+ "slack.com",
1246
+ "files.slack.com",
1247
+ "avatars.slack-edge.com"
1248
+ ],
1076
1249
  approve: "agent-slack approve",
1077
1250
  skills: {
1078
1251
  prefix: "slack-",
@@ -2968,6 +3141,99 @@ function commandLinePackage(command, args) {
2968
3141
  const line = [command, ...withoutPathOptions(args).words].join(" ");
2969
3142
  return /(@[\w.-]+\/[\w.-]+|(?<=\s)[\w.-]+-mcp)(?=@|\s|$)/.exec(line)?.[1];
2970
3143
  }
3144
+ /**
3145
+ * The programs this package starts, named so that no folder it happens to be in can stand in for them.
3146
+ *
3147
+ * A bare name is looked up, and on Windows the lookup starts in the current folder: libuv tries it before `PATH`, and
3148
+ * so does every program that looks a name up with `SearchPath` or `CreateProcess`. The current folder is where a
3149
+ * command was run or a server was started — and, since a download asks where to save, one of the three places a
3150
+ * stranger's file may be saved into. A `reg.exe` or `rundll32.exe` saved there would then be what runs the next time
3151
+ * this package reads the registry or opens a browser. So a Windows program is named by its full path, under the Windows
3152
+ * folder, and every process started on Windows is told, by `NoDefaultCurrentDirectoryInExePath`, not to look in its
3153
+ * own current folder for the programs it starts in turn.
3154
+ */
3155
+ /** An environment variable by name, whatever its case: Windows's are case-blind, an object passed in is not. */
3156
+ function envValue(env, name) {
3157
+ const wanted = name.toLowerCase();
3158
+ for (const [key, value] of Object.entries(env)) if (key.toLowerCase() === wanted && value) return value;
3159
+ }
3160
+ /**
3161
+ * The Windows folder: `%SystemRoot%` (or `%windir%`) when it names a folder on a drive, and `C:\Windows` otherwise.
3162
+ * A value that is not a drive's absolute path — relative, a share, a device path — is not taken: it would be looked up
3163
+ * against the current folder, or on another machine, which is what naming the program in full is to avoid.
3164
+ */
3165
+ function windowsFolder(env = process.env) {
3166
+ for (const name of ["SystemRoot", "windir"]) {
3167
+ const value = envValue(env, name);
3168
+ if (value !== void 0 && /^[A-Za-z]:[\\/]/.test(value)) return path.win32.resolve(value);
3169
+ }
3170
+ return "C:\\Windows";
3171
+ }
3172
+ /** A program of Windows's own by its full path: `%SystemRoot%\System32\<name>`, never a bare name to look up. */
3173
+ function windowsSystemProgram(name, env = process.env) {
3174
+ return path.win32.join(windowsFolder(env), "System32", name);
3175
+ }
3176
+ /**
3177
+ * The environment a child process is started with: `env` as it is, and on Windows also
3178
+ * `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
3179
+ * program from its current folder either. Elsewhere nothing is added: a Unix shell looks in the current folder only
3180
+ * when `PATH` says to.
3181
+ */
3182
+ function childEnvironment(env = process.env, platform = process.platform) {
3183
+ if (platform !== "win32") return env;
3184
+ return {
3185
+ ...env,
3186
+ NoDefaultCurrentDirectoryInExePath: "1"
3187
+ };
3188
+ }
3189
+ /**
3190
+ * The directories of a search path that name a folder on their own: absolute, and on Windows on a drive. An empty
3191
+ * entry means the current folder to a Unix shell, and a relative one is read against it; either would make the program
3192
+ * found — and run — depend on where this was started, which is where a download may have saved a stranger's file.
3193
+ */
3194
+ function absoluteSearchPath(entries, platform = process.platform) {
3195
+ return entries.filter((entry) => platform === "win32" ? /^[A-Za-z]:[\\/]/.test(entry) : entry !== "" && path.posix.isAbsolute(entry));
3196
+ }
3197
+ /** This Node's real file, or its path as reported when that cannot be resolved. */
3198
+ function realFile(path) {
3199
+ try {
3200
+ return realpathSync(path);
3201
+ } catch {
3202
+ return path;
3203
+ }
3204
+ }
3205
+ /** Two real paths are one file: compared without case on Windows, whose paths are. */
3206
+ function sameRealPath(a, b, platform) {
3207
+ return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
3208
+ }
3209
+ /** The names `node` is found by in one folder of PATH, as a shell finds it: with each PATHEXT extension on Windows. */
3210
+ function nodeNames(env, platform) {
3211
+ if (platform !== "win32") return ["node"];
3212
+ return (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean).map((extension) => `node${extension.toLowerCase()}`);
3213
+ }
3214
+ /**
3215
+ * The path a command names this Node by: the first `node` on `env`'s PATH that is the same real file as
3216
+ * `process.execPath`, as PATH spells it; otherwise `process.execPath`. With no `env`, nothing is looked for.
3217
+ *
3218
+ * Read when a command is located, as everything else in it is (D6): a link removed or repointed later is what the
3219
+ * command then meets. Only absolute folders of PATH are read, as the registrations' own search reads them.
3220
+ */
3221
+ function nodeProgram(env, platform = process.platform) {
3222
+ const own = process.execPath;
3223
+ if (env === void 0) return own;
3224
+ const real = realFile(own);
3225
+ const search = env.PATH ?? env.Path ?? "";
3226
+ for (const directory of absoluteSearchPath(search.split(delimiter), platform)) for (const name of nodeNames(env, platform)) {
3227
+ const candidate = join(directory, name);
3228
+ try {
3229
+ if (!statSync(candidate).isFile()) continue;
3230
+ accessSync(candidate, constants$1.X_OK);
3231
+ if (!sameRealPath(realpathSync(candidate), real, platform)) continue;
3232
+ return sameRealPath(candidate, own, platform) ? own : candidate;
3233
+ } catch {}
3234
+ }
3235
+ return own;
3236
+ }
2971
3237
  const RECEIPT_VERSION = 1;
2972
3238
  const RECEIPT_FILE = "managed-shims.json";
2973
3239
  const MUTATION_LOCK_FILE = "managed-shims.lock";
@@ -3305,59 +3571,6 @@ async function removeManagedShimsForRuntimes(context, runtimes, options = {}) {
3305
3571
  };
3306
3572
  });
3307
3573
  }
3308
- /**
3309
- * The programs this package starts, named so that no folder it happens to be in can stand in for them.
3310
- *
3311
- * A bare name is looked up, and on Windows the lookup starts in the current folder: libuv tries it before `PATH`, and
3312
- * so does every program that looks a name up with `SearchPath` or `CreateProcess`. The current folder is where a
3313
- * command was run or a server was started — and, since a download asks where to save, one of the three places a
3314
- * stranger's file may be saved into. A `reg.exe` or `rundll32.exe` saved there would then be what runs the next time
3315
- * this package reads the registry or opens a browser. So a Windows program is named by its full path, under the Windows
3316
- * folder, and every process started on Windows is told, by `NoDefaultCurrentDirectoryInExePath`, not to look in its
3317
- * own current folder for the programs it starts in turn.
3318
- */
3319
- /** An environment variable by name, whatever its case: Windows's are case-blind, an object passed in is not. */
3320
- function envValue(env, name) {
3321
- const wanted = name.toLowerCase();
3322
- for (const [key, value] of Object.entries(env)) if (key.toLowerCase() === wanted && value) return value;
3323
- }
3324
- /**
3325
- * The Windows folder: `%SystemRoot%` (or `%windir%`) when it names a folder on a drive, and `C:\Windows` otherwise.
3326
- * A value that is not a drive's absolute path — relative, a share, a device path — is not taken: it would be looked up
3327
- * against the current folder, or on another machine, which is what naming the program in full is to avoid.
3328
- */
3329
- function windowsFolder(env = process.env) {
3330
- for (const name of ["SystemRoot", "windir"]) {
3331
- const value = envValue(env, name);
3332
- if (value !== void 0 && /^[A-Za-z]:[\\/]/.test(value)) return path.win32.resolve(value);
3333
- }
3334
- return "C:\\Windows";
3335
- }
3336
- /** A program of Windows's own by its full path: `%SystemRoot%\System32\<name>`, never a bare name to look up. */
3337
- function windowsSystemProgram(name, env = process.env) {
3338
- return path.win32.join(windowsFolder(env), "System32", name);
3339
- }
3340
- /**
3341
- * The environment a child process is started with: `env` as it is, and on Windows also
3342
- * `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
3343
- * program from its current folder either. Elsewhere nothing is added: a Unix shell looks in the current folder only
3344
- * when `PATH` says to.
3345
- */
3346
- function childEnvironment(env = process.env, platform = process.platform) {
3347
- if (platform !== "win32") return env;
3348
- return {
3349
- ...env,
3350
- NoDefaultCurrentDirectoryInExePath: "1"
3351
- };
3352
- }
3353
- /**
3354
- * The directories of a search path that name a folder on their own: absolute, and on Windows on a drive. An empty
3355
- * entry means the current folder to a Unix shell, and a relative one is read against it; either would make the program
3356
- * found — and run — depend on where this was started, which is where a download may have saved a stranger's file.
3357
- */
3358
- function absoluteSearchPath(entries, platform = process.platform) {
3359
- return entries.filter((entry) => platform === "win32" ? /^[A-Za-z]:[\\/]/.test(entry) : entry !== "" && path.posix.isAbsolute(entry));
3360
- }
3361
3574
  /** The install options that pin a server to one account: the generic one, and Gmail's and Slack's own names. */
3362
3575
  const PIN_OPTIONS = Object.freeze([
3363
3576
  "account",
@@ -4945,62 +5158,6 @@ function connectMailboxWords() {
4945
5158
  */
4946
5159
  const CONVERSION_HOOKS = Symbol("agentcomms.configConversionHooks");
4947
5160
  /**
4948
- * One table of dangerous characters, shared by the inbound sanitiser (which strips them from sender-controlled text)
4949
- * and the preview renderer (which makes them visible), so the two can never disagree.
4950
- */
4951
- /** Zero-width and formatting characters, bidi controls, variation selectors and Unicode tag characters. */
4952
- const INVISIBLE_RANGES = [
4953
- [173, 173],
4954
- [847, 847],
4955
- [1564, 1564],
4956
- [4447, 4448],
4957
- [6068, 6069],
4958
- [6155, 6159],
4959
- [8203, 8207],
4960
- [8232, 8233],
4961
- [8234, 8238],
4962
- [8288, 8292],
4963
- [8294, 8303],
4964
- [12644, 12644],
4965
- [65024, 65039],
4966
- [65279, 65279],
4967
- [65440, 65440],
4968
- [917504, 917631]
4969
- ];
4970
- function isInvisible(codePoint) {
4971
- for (const [from, to] of INVISIBLE_RANGES) if (codePoint >= from && codePoint <= to) return true;
4972
- return false;
4973
- }
4974
- /**
4975
- * C0 controls except tab and newline (ESC, and so every ANSI/OSC sequence, starts here), DEL, and C1 controls (CSI
4976
- * among them). A lone carriage return counts: it moves a terminal cursor back over text already printed.
4977
- */
4978
- function isControl(codePoint) {
4979
- if (codePoint === 9 || codePoint === 10) return false;
4980
- return codePoint < 32 || codePoint === 127 || codePoint >= 128 && codePoint <= 159;
4981
- }
4982
- function isDangerous(codePoint) {
4983
- return isControl(codePoint) || isInvisible(codePoint);
4984
- }
4985
- /**
4986
- * Strips control characters (ESC, CSI, OSC and the rest), DEL, lone carriage returns, zero-width, bidi-control and tag
4987
- * characters from sender-controlled text; returns the text and how many were removed. CRLF becomes LF first.
4988
- *
4989
- * This lives beside the table rather than in the sanitiser because `neutralise` needs it too, and the two must not
4990
- * drift: a pattern that looks for `</untrusted-content` cannot see it through a zero-width space, so stripping
4991
- * has to happen before any such pattern runs, on every path, not only on the ones that render a body.
4992
- */
4993
- function stripInvisible(text) {
4994
- let removed = 0;
4995
- let out = "";
4996
- for (const char of text.replace(/\r\n/g, "\n")) if (isDangerous(char.codePointAt(0) ?? 0)) removed += 1;
4997
- else out += char;
4998
- return {
4999
- text: out,
5000
- removed
5001
- };
5002
- }
5003
- /**
5004
5161
  * Whether a folder, as written, says where it is on its own: from the home (`~`), or absolute — and on Windows with its
5005
5162
  * drive (`C:\…`) or share (`\\server\share\…`). Windows calls `\outgoing` absolute, but it is on whichever drive is
5006
5163
  * current when it is read, and `C:outgoing` is relative to that drive's current folder; either would move with the
@@ -6114,11 +6271,39 @@ function classifyChange(before, after) {
6114
6271
  if (before.secrets === void 0 && after.secrets !== void 0 && after.secrets.store !== "keychain") {
6115
6272
  if (holdsSecrets(before)) loosen("secrets.store", secretsStoreOf(before), after.secrets.store);
6116
6273
  }
6274
+ for (const loosening of classifyAgentLabelChange(agentLabelOf(before), agentLabelOf(after))) loosen(loosening.path, labelSettingValue(loosening, loosening.was), labelSettingValue(loosening, loosening.now));
6117
6275
  return {
6118
6276
  loosened: changes.map((change) => change.path),
6119
6277
  changes
6120
6278
  };
6121
6279
  }
6280
+ /**
6281
+ * One label rule in words, as a preview lists it: `acme/slack in C0ACME for post/reply: me`. No comma inside, since a
6282
+ * preview joins a list of them with commas.
6283
+ */
6284
+ function agentLabelRuleText(rule) {
6285
+ const where = rule.conversation === void 0 ? "" : ` in ${rule.conversation}`;
6286
+ const kinds = rule.kinds === void 0 ? "" : ` for ${rule.kinds.join("/")}`;
6287
+ return `${rule.account}${where}${kinds}: ${rule.as}`;
6288
+ }
6289
+ /** A label setting's value as a loosening carries it: the rules in words, the icons as they are, the rest as text. */
6290
+ function labelSettingValue(loosening, value) {
6291
+ if (value === void 0 || value === null) return void 0;
6292
+ if (loosening.path === "agentLabel.rules") return value.map(agentLabelRuleText);
6293
+ if (loosening.path === "agentLabel.slackIcons") return { ...value };
6294
+ return String(value);
6295
+ }
6296
+ /** The label settings a change can write, as the file holds them — raw, as `changedSettings` records every setting. */
6297
+ const LABEL_SETTINGS = [
6298
+ "word",
6299
+ "name",
6300
+ "rules",
6301
+ "slackIcons"
6302
+ ];
6303
+ function storedAgentLabel(config) {
6304
+ const stored = config.agentLabel;
6305
+ return typeof stored === "object" && stored !== null && !Array.isArray(stored) ? stored : {};
6306
+ }
6122
6307
  /** The settings of a mailbox, and of an account, that a change can write — the ones the classifier judges. */
6123
6308
  const INBOX_SETTINGS = [
6124
6309
  "sendPolicy",
@@ -6132,7 +6317,7 @@ const ACCOUNT_SETTINGS = [
6132
6317
  ];
6133
6318
  /**
6134
6319
  * Every setting that differs between `before` and `after`, loosened or tightened: the defaults, the secret store,
6135
- * and each mailbox's and account's own settings — including those of one added or removed.
6320
+ * the agent label's settings, and each mailbox's and account's own settings — including those of one added or removed.
6136
6321
  *
6137
6322
  * What a change approval binds, beside its loosenings. A preview shows the whole change — "sends approved by never;
6138
6323
  * changes approved by chat" — and binding only the loosenings let a claim drop the tightening a person read, since
@@ -6170,6 +6355,9 @@ function changedSettings(before, after) {
6170
6355
  const defaultsAfter = after.defaults;
6171
6356
  for (const key of [.../* @__PURE__ */ new Set([...Object.keys(defaultsBefore), ...Object.keys(defaultsAfter)])].sort()) record(`defaults.${key}`, defaultsBefore[key], defaultsAfter[key]);
6172
6357
  record("secrets.store", before.secrets?.store, after.secrets?.store);
6358
+ const labelBefore = storedAgentLabel(before);
6359
+ const labelAfter = storedAgentLabel(after);
6360
+ for (const key of LABEL_SETTINGS) record(`agentLabel.${key}`, labelBefore[key], labelAfter[key]);
6173
6361
  return changes;
6174
6362
  }
6175
6363
  /**
@@ -9304,7 +9492,7 @@ function folding(platform) {
9304
9492
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
9305
9493
  }
9306
9494
  /** The package version, read from package.json at build time. */
9307
- const VERSION = "0.18.1";
9495
+ const VERSION = "0.19.0";
9308
9496
  /**
9309
9497
  * Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
9310
9498
  * to decide whether the release it last heard of is newer than the one running.
@@ -9482,8 +9670,10 @@ const CHANNEL_PACKAGES = new Set(CHANNEL_SNAPSHOT.filter(({ manifest }) => manif
9482
9670
  /**
9483
9671
  * Locates the command that runs `request.target`'s CLI with `request.words`, or says why there is none.
9484
9672
  *
9485
- * The program is always this process's Node (`process.execPath`), which must satisfy the target's `engines.node`; no
9486
- * interpreter named anywhere else is run, or asked its version. A TypeScript entry gets `--experimental-strip-types`,
9673
+ * The program is always this process's Node, which must satisfy the target's `engines.node`; no interpreter named
9674
+ * anywhere else is run, or asked its version. It is named as PATH finds it — the first `node` on `request.env`'s PATH
9675
+ * that is the same real file as `process.execPath` — and otherwise by `process.execPath` (CUE-492): the real file only
9676
+ * identifies it, and `process.version` is that file's. A TypeScript entry gets `--experimental-strip-types`,
9487
9677
  * which Node 22.12–22.17 needs, and `--experimental-transform-types` only when this source run was started with it;
9488
9678
  * no other flag this process was started with is carried — debug, preload, condition and memory flags are not what the
9489
9679
  * CLI needs. The suite directories the command uses go between the entry and its words, one canonical option each,
@@ -9524,6 +9714,7 @@ function locateCliCommand(request, runtime = {
9524
9714
  return fail("caller", `${nearest.root} cannot be resolved`);
9525
9715
  }
9526
9716
  if (!isAbsolute(process.execPath)) return fail("interpreter", `this Node, ${process.execPath}, is not a full path`);
9717
+ const program = nodeProgram(request.env);
9527
9718
  const pins = {};
9528
9719
  for (const key of request.uses) pins[key] = request.paths[key];
9529
9720
  let words;
@@ -9532,7 +9723,7 @@ function locateCliCommand(request, runtime = {
9532
9723
  } catch (error) {
9533
9724
  return fail("arguments", error instanceof Error ? error.message : String(error));
9534
9725
  }
9535
- const build = (found) => buildCommand(target, found, words, runtime, callerSource, platform);
9726
+ const build = (found) => buildCommand(target, found, words, runtime, callerSource, platform, program);
9536
9727
  const located = (built, found, direction) => Object.freeze({
9537
9728
  ok: true,
9538
9729
  command: built.command,
@@ -9672,7 +9863,7 @@ function locateCliCommand(request, runtime = {
9672
9863
  * Checks what was found and makes its command: the target's Node range against this Node, then the entry — the source
9673
9864
  * CLI or the manifest's bin for the command it declares — as a readable file inside its package.
9674
9865
  */
9675
- function buildCommand(target, found, words, runtime, callerSource, platform) {
9866
+ function buildCommand(target, found, words, runtime, callerSource, platform, program) {
9676
9867
  const engines = found.manifest.engines;
9677
9868
  const range = typeof engines === "object" && engines !== null ? engines.node : void 0;
9678
9869
  if (typeof range !== "string") return {
@@ -9709,7 +9900,7 @@ function buildCommand(target, found, words, runtime, callerSource, platform) {
9709
9900
  return {
9710
9901
  ok: true,
9711
9902
  command: new PrintedCommand(GATE, [
9712
- process.execPath,
9903
+ program,
9713
9904
  ...flags,
9714
9905
  checked.path,
9715
9906
  ...words
@@ -10013,7 +10204,8 @@ function cliHandoffs(options) {
10013
10204
  uses: usesOf(use),
10014
10205
  paths,
10015
10206
  platform,
10016
- ...registrations === void 0 ? {} : { registrations }
10207
+ ...registrations === void 0 ? {} : { registrations },
10208
+ ...options.env === void 0 ? {} : { env: options.env }
10017
10209
  }, ...options.runtime === void 0 ? [] : [options.runtime]);
10018
10210
  return result.ok ? result.command : result;
10019
10211
  };
@@ -10546,7 +10738,11 @@ const SETTING_LABELS = {
10546
10738
  "defaults.downloadsDir": "downloads folder",
10547
10739
  "defaults.confirm.elicitationClients": "clients trusted to ask for a send approval",
10548
10740
  "defaults.updateCheck": "daily update check",
10549
- "secrets.store": "where credentials are kept"
10741
+ "secrets.store": "where credentials are kept",
10742
+ "agentLabel.word": "the word after your name in the agent label",
10743
+ "agentLabel.name": "the name in the agent label",
10744
+ "agentLabel.rules": "who each kind of message appears to come from",
10745
+ "agentLabel.slackIcons": "the icon of the agent’s Slack posts"
10550
10746
  };
10551
10747
  /**
10552
10748
  * What the loosening means for the person, in their terms.
@@ -10571,6 +10767,10 @@ function meaning(field, after, scope) {
10571
10767
  case "defaults.confirm.elicitationClients": return "these clients may ask you to approve a send in their own window";
10572
10768
  case "defaults.updateCheck": return "nothing on this machine will ask npm for a newer release, or stop until one is installed";
10573
10769
  case "secrets.store": return "credentials will move out of the system keychain into files on this disk";
10770
+ case "agentLabel.word": return "what an agent sends will carry this word after your name";
10771
+ case "agentLabel.name": return after === null ? "where a channel has no name of its own for you, an agent’s message will be refused until a name is set" : "where a channel has no name of its own for you, what an agent sends will carry this name";
10772
+ case "agentLabel.rules": return "what a rule sends as `me` goes out as you, with no label: it will look as if you wrote it yourself";
10773
+ case "agentLabel.slackIcons": return "the app’s Slack posts will show this icon beside your name";
10574
10774
  default: return "";
10575
10775
  }
10576
10776
  }
@@ -11612,7 +11812,8 @@ async function withManagedShim(context, product, installed) {
11612
11812
  ...context.core.paths,
11613
11813
  downloadsDir: context.core.paths.dataDir
11614
11814
  },
11615
- platform: context.platform
11815
+ platform: context.platform,
11816
+ env: context.env
11616
11817
  });
11617
11818
  if (!located.ok) throw new CommsError("CONFIG", `the managed CLI command could not be located: ${located.message}`);
11618
11819
  shim = await (context.writeShim ?? writeManagedShim)(shimContext, {
@@ -11675,7 +11876,7 @@ function serverPruneChange(core, env, request, own) {
11675
11876
  };
11676
11877
  const { packageName, version } = ownProduct(request.channel, own) ?? {
11677
11878
  packageName: serverFacts(request.channel).packageName,
11678
- version: "0.18.1"
11879
+ version: "0.19.0"
11679
11880
  };
11680
11881
  const product = {
11681
11882
  packageName,
@@ -12330,7 +12531,7 @@ async function askLine(streams, question) {
12330
12531
  }
12331
12532
  channelServer("core").packageName;
12332
12533
  //#endregion
12333
- //#region ../core/dist/tool-arguments-DLa_xsSp.mjs
12534
+ //#region ../core/dist/tool-arguments-BkdTZEb8.mjs
12334
12535
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
12335
12536
  const TARGET = "draft-2020-12";
12336
12537
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcomms/whatsapp",
3
- "version": "0.18.1",
3
+ "version": "0.19.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",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@agentcomms/core": "0.18.1"
37
+ "@agentcomms/core": "0.19.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@modelcontextprotocol/client": "2.3.1",
@@ -98,5 +98,5 @@
98
98
  "typecheck": "tsc -p tsconfig.json",
99
99
  "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test --test-timeout=600000 \"test/**/*.test.ts\""
100
100
  },
101
- "gitHead": "fc398edc67287e411f8c5cb821f434220cb83aa5"
101
+ "gitHead": "29a7121620a9ef305a4260f193c40db1980312bd"
102
102
  }