@agentcomms/whatsapp 0.9.0 → 0.11.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 serverInstallChange, A as CommsError, B as escapeForDisplay, C as checkStorePath, F as approveChangeAtTerminal, G as lookupName, H as gatedChangeAtTerminal, I as canPrompt, J as openCore, K as nameAvailable, L as channelManifest, M as agentMarker, N as approvalKind, O as CHANNELS, P as approvalsOf, Q as runCommand, R as colorEnabled, S as storeIdentity, T as expandSource, U as installExitStatus, V as exemptFromUpdateGate, W as isCommsError, X as renderInstall, Y as paint, Z as renderPrune, _ as chatRefOf, a as listChats, at as withFileLock, b as requireAccount, c as Presenter, d as draftMessage, et as serverPruneChange, f as Visibility, g as requireSupportedNode, h as WhatsAppIndex, i as CHAT_KINDS, j as EXIT_CODES, k as CHANNEL_SERVERS, l as innerText, m as WhatsAppContext, n as whatsappStatus, nt as truncateDisplay, o as readChat, ot as writeResult, p as listKey, q as nameShapeProblem, r as probeStore, rt as updateGateAtTerminal, s as searchMessages, st as VERSION, t as syncAccount, v as PLATFORM, w as defaultStorePath, x as requireNamedConfig, y as newWhatsAppAccount, z as commandPathOf } from "./sync-DU_KB2pO.mjs";
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-CinH_TIY.mjs";
3
3
  import { rm } from "node:fs/promises";
4
- import path from "node:path";
4
+ import path, { delimiter, join } from "node:path";
5
5
  import childProcess, { spawn } from "node:child_process";
6
6
  import { stripVTControlCharacters } from "node:util";
7
- import fs from "node:fs";
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
@@ -3161,18 +3161,17 @@ function clearChats(context, request) {
3161
3161
  }
3162
3162
  //#endregion
3163
3163
  //#region src/cli/opener.ts
3164
- function openLink(url, platform = process.platform) {
3164
+ function openLink(url, platform = process.platform, deps = {}) {
3165
3165
  if (!/^(?:whatsapp:\/\/send\?|https:\/\/wa\.me\/)/.test(url)) return false;
3166
- const [command, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", [
3167
- "/c",
3168
- "start",
3169
- "",
3170
- url
3171
- ]] : ["xdg-open", [url]];
3166
+ const env = deps.env ?? process.env;
3167
+ const command = linkOpener(platform, env);
3168
+ if (command === null) return false;
3169
+ const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
3172
3170
  try {
3173
- const child = spawn(command, args, {
3171
+ const child = (deps.spawn ?? spawn)(command, args, {
3174
3172
  stdio: "ignore",
3175
- detached: true
3173
+ detached: true,
3174
+ env: childEnvironment(env, platform)
3176
3175
  });
3177
3176
  child.on("error", () => void 0);
3178
3177
  child.unref();
@@ -3181,6 +3180,19 @@ function openLink(url, platform = process.platform) {
3181
3180
  return false;
3182
3181
  }
3183
3182
  }
3183
+ /** The opener's full path, or null when there is none that can be named without looking in the current folder. */
3184
+ function linkOpener(platform, env) {
3185
+ if (platform === "win32") return windowsSystemProgram("rundll32.exe", env);
3186
+ if (platform === "darwin") return "/usr/bin/open";
3187
+ for (const directory of absoluteSearchPath((env.PATH ?? "").split(delimiter), platform)) {
3188
+ const candidate = join(directory, "xdg-open");
3189
+ try {
3190
+ accessSync(candidate, constants$1.X_OK);
3191
+ return candidate;
3192
+ } catch {}
3193
+ }
3194
+ return null;
3195
+ }
3184
3196
  //#endregion
3185
3197
  //#region src/cli/render.ts
3186
3198
  /**
@@ -3522,7 +3534,7 @@ a macOS dialog may be waiting) · 77 permission needed (macOS privacy) · 78 con
3522
3534
  ran = true;
3523
3535
  exitCode = await runCommand(output(), async () => {
3524
3536
  requireSupportedNode();
3525
- const { startWhatsAppStdioServer } = await import("./stdio-entry-Blz5voaW.mjs");
3537
+ const { startWhatsAppStdioServer } = await import("./stdio-entry-r40LJtFu.mjs");
3526
3538
  await startWhatsAppStdioServer({
3527
3539
  env,
3528
3540
  ...flags.account ? { account: String(flags.account) } : {}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { st as VERSION, u as composeDraft } from "./sync-DU_KB2pO.mjs";
2
- import { t as createWhatsAppMcpServer } from "./server-3qn7im60.mjs";
1
+ import { u as composeDraft, ut as VERSION } from "./sync-CinH_TIY.mjs";
2
+ import { t as createWhatsAppMcpServer } from "./server-BLF6Xumr.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, it as updateToolGate, m as WhatsAppContext, n as whatsappStatus, o as readChat, s as searchMessages, st as VERSION, t as syncAccount, tt as toCommsError } from "./sync-DU_KB2pO.mjs";
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-CinH_TIY.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-3qn7im60.mjs";
1
+ import { t as createWhatsAppMcpServer } from "./server-BLF6Xumr.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
@@ -1,19 +1,19 @@
1
1
  import { C as unknown, b as strictObject, c as boolean, h as number, i as _enum, m as looseObject, p as literal, s as array, x as string, y as record } from "./schemas-DiGKNDNq.mjs";
2
2
  import { access, chmod, constants, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, utimes } from "node:fs/promises";
3
- import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
3
+ import path, { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
4
4
  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
- import { homedir, platform } from "node:os";
7
+ import { homedir, platform, userInfo } from "node:os";
8
8
  import { execFile, spawn } from "node:child_process";
9
9
  import { styleText } from "node:util";
10
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.9.0";
14
+ const VERSION$1 = "0.11.0";
15
15
  //#endregion
16
- //#region ../core/dist/update-gate-BwIKJmH7.mjs
16
+ //#region ../core/dist/update-gate-BMcLWU3V.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
@@ -1033,7 +1033,7 @@ const APP_DIR_NAME = "agent-communications";
1033
1033
  function resolvePaths(options = {}) {
1034
1034
  const env = options.env ?? process.env;
1035
1035
  const platform = options.platform ?? process.platform;
1036
- const home = options.home ?? ((platform === "win32" ? env.USERPROFILE : env.HOME) || homedir());
1036
+ const home = options.home ?? homeOf(env, platform);
1037
1037
  const configDir = resolve(env.AGENT_COMMS_CONFIG_DIR || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, "agent-communications") : platform === "win32" ? join(env.APPDATA || join(home, "AppData", "Roaming"), "agent-communications") : join(home, ".config", "agent-communications")));
1038
1038
  const explicitConfigDir = Boolean(env.AGENT_COMMS_CONFIG_DIR);
1039
1039
  const localRoot = platform === "win32" && !explicitConfigDir ? join(env.LOCALAPPDATA || join(home, "AppData", "Local"), APP_DIR_NAME) : configDir;
@@ -1045,10 +1045,40 @@ function resolvePaths(options = {}) {
1045
1045
  downloadsDir: resolve(join(home, "Downloads", APP_DIR_NAME))
1046
1046
  };
1047
1047
  }
1048
- /** Expands a leading `~` to the home directory. Nothing else is expanded. */
1049
- function expandHome(path, home = homedir()) {
1048
+ /**
1049
+ * The home the environment names, which is where Node's own `homedir()` looks for the running process — `HOME`, or
1050
+ * `USERPROFILE` on Windows. Asking `homedir()` directly ignored an environment passed in, so every test that gave the
1051
+ * harness a temporary HOME still had its data and downloads resolved to the real ones: one day of test runs left 748
1052
+ * backups of fixture entries in the maintainer's own data directory.
1053
+ *
1054
+ * Its own function since a download asks where to save: the person's Downloads folder is `<this>/Downloads`, and a
1055
+ * folder they type as `~/…` is expanded from it — the same home every other path here is resolved from. When the
1056
+ * environment names none, the account's own home is taken ({@link accountHome}); `account` is for a test to say what
1057
+ * that is.
1058
+ */
1059
+ function homeOf(env = process.env, platform = process.platform, account = accountHome) {
1060
+ return (platform === "win32" ? env.USERPROFILE : env.HOME) || account();
1061
+ }
1062
+ /**
1063
+ * The home the account itself has, for when the environment names none: the password database's (`os.userInfo()`),
1064
+ * which no variable moves, rather than `homedir()`, which reads `HOME` first — from this process's environment, not the
1065
+ * one the caller passed, and so from somewhere the caller did not say. Only when there is no such entry — some
1066
+ * containers run as an id with none — is `homedir()` asked instead.
1067
+ */
1068
+ function accountHome() {
1069
+ try {
1070
+ const home = userInfo().homedir;
1071
+ if (home) return home;
1072
+ } catch {}
1073
+ return homedir();
1074
+ }
1075
+ /**
1076
+ * Expands a leading `~` to the home directory. Nothing else is expanded. `joinPaths` is the platform's own, for a path
1077
+ * judged for another platform than this one: a Linux home joined on Windows would otherwise take its backslashes.
1078
+ */
1079
+ function expandHome(path, home = homedir(), joinPaths = join) {
1050
1080
  if (path === "~") return home;
1051
- if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
1081
+ if (path.startsWith("~/") || path.startsWith("~\\")) return joinPaths(home, path.slice(2));
1052
1082
  return path;
1053
1083
  }
1054
1084
  /**
@@ -1855,6 +1885,55 @@ function challengeMatches(answer, storedHash) {
1855
1885
  const b = Buffer.from(storedHash, "hex");
1856
1886
  return a.length === b.length && timingSafeEqual(a, b);
1857
1887
  }
1888
+ /**
1889
+ * The digest a download's question is bound to: the account, the download, the request, the files it listed and the
1890
+ * names it showed them under.
1891
+ *
1892
+ * The files and names keep their order, since the question showed them in it; the request is canonical JSON, so the
1893
+ * same arguments digest the same however an object happened to list its keys.
1894
+ */
1895
+ function downloadDigest(download) {
1896
+ return sha256Hex(canonicalJson$1({
1897
+ kind: "download",
1898
+ target: {
1899
+ kind: download.target.kind,
1900
+ name: download.target.name,
1901
+ id: download.target.id
1902
+ },
1903
+ operation: download.operation,
1904
+ request: download.request,
1905
+ files: [...download.files],
1906
+ names: [...download.names]
1907
+ }));
1908
+ }
1909
+ /**
1910
+ * Why a download claimed is not the one its question was asked for, in a sentence — the first difference found. Only
1911
+ * the words; what refuses is the digest.
1912
+ */
1913
+ function downloadDrift(asked, now) {
1914
+ if (asked.target.id !== now.target.id || asked.target.kind !== now.target.kind) return `the question was about ${asked.target.name}, not ${now.target.name}`;
1915
+ if (asked.operation !== now.operation) return "the question was asked for another kind of download";
1916
+ if (canonicalJson$1(asked.request) !== canonicalJson$1(now.request)) return "the question was asked about a different request: other messages, other files or another limit";
1917
+ if (canonicalJson$1(asked.files) !== canonicalJson$1(now.files)) return "the files are not the ones the question listed";
1918
+ const renamed = asked.files.find((_, index) => asked.names[index] !== now.names[index]);
1919
+ if (renamed !== void 0) return `${renamed} would now be saved under another name than the one the question showed: it was renamed since`;
1920
+ return "the download is not the one the question was asked for";
1921
+ }
1922
+ /**
1923
+ * Why a download's question cannot be claimed under this change policy — the stricter of `livePolicy` and the one it
1924
+ * was asked under — or null. Changes nothing, so a download can ask before it looks at a folder, and the store asks
1925
+ * again as it claims.
1926
+ *
1927
+ * Under `chat` nothing stops it: the person's answer, relayed from the conversation, is the answer. Anything stricter
1928
+ * needs the answer recorded on the question by a channel an agent cannot answer — the person's own terminal, or a
1929
+ * form a trusted client showed them. An answer carried in a tool's arguments or a command's flags is refused however
1930
+ * it was worded, with the command that answers it named, and the question left open for the person.
1931
+ */
1932
+ function downloadClaimRefusal(record, livePolicy, pendingHint) {
1933
+ if (stricterPolicy(livePolicy, record.requiredPolicy) === "chat") return null;
1934
+ if (record.state === "approved" && (record.approvedVia === "terminal" || record.approvedVia === "elicitation") && record.download?.answer !== void 0) return null;
1935
+ return refuseDownload("APPROVAL_PENDING", "the change policy here is confirm, so the person answers where to save themselves — at their own terminal, not through an agent", record, pendingHint ?? "Ask the person to answer it at their own terminal, with the approve command of the channel the files come from and this choice id; then make the download again with the choice id alone.");
1936
+ }
1858
1937
  /** The kind of a record. Absent is a send: every record written before changes had approvals. */
1859
1938
  function approvalKind(record) {
1860
1939
  return record.kind ?? "send";
@@ -1903,6 +1982,11 @@ function changeDrift(approved, now) {
1903
1982
  if (canonicalJson$1(approved.effects) !== canonicalJson$1(now.effects)) return "what it does outside the configuration is not what was approved";
1904
1983
  return "the change is not the one that was approved";
1905
1984
  }
1985
+ /**
1986
+ * What to do with a download's question, for a caller that took it for something else: it is answered, not approved
1987
+ * with a code — in the chat, or at the person's own terminal with the command of the channel that asked.
1988
+ */
1989
+ const DOWNLOAD_ANSWER_HINT = "It is answered, not approved with a code: the person says where in the chat, or — under a confirm change policy — at their own terminal, with the `approve` command of the channel the files come from and this id. The download that asked is then made again with this id.";
1906
1990
  const POLICY_RANK = {
1907
1991
  chat: 0,
1908
1992
  confirm: 1,
@@ -1931,18 +2015,36 @@ function refuseChange(code, reason, record, hint) {
1931
2015
  } : {}
1932
2016
  });
1933
2017
  }
2018
+ /**
2019
+ * The same refusal for a download's question, which saves nothing until it is claimed.
2020
+ *
2021
+ * Its hint says to ask again rather than to prepare anything: what the person is shown again is the question, and it
2022
+ * is the download itself that asks it.
2023
+ */
2024
+ function refuseDownload(code, reason, record, hint) {
2025
+ return new CommsError(code, `nothing was saved: ${reason}`, {
2026
+ hint: hint ?? "Make the download again without an answer, and show the person the new question.",
2027
+ details: record ? {
2028
+ choiceId: record.approvalId,
2029
+ state: record.state
2030
+ } : {}
2031
+ });
2032
+ }
1934
2033
  /** The refusal in the words of the record's own kind. */
1935
2034
  function refusalFor(record) {
1936
- return approvalKind(record) === "change" ? refuseChange : refuse;
2035
+ const kind = approvalKind(record);
2036
+ return kind === "change" ? refuseChange : kind === "download" ? refuseDownload : refuse;
1937
2037
  }
1938
2038
  var ApprovalStore = class {
1939
2039
  directory;
1940
2040
  #now;
1941
2041
  #ttlMs;
2042
+ #downloadTtlMs;
1942
2043
  constructor(stateDir, options = {}) {
1943
2044
  this.directory = join(stateDir, "approvals");
1944
2045
  this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
1945
2046
  this.#ttlMs = options.ttlMs ?? 6e5;
2047
+ this.#downloadTtlMs = options.downloadTtlMs ?? 18e5;
1946
2048
  }
1947
2049
  #path(approvalId, suffix = ".json") {
1948
2050
  if (!APPROVAL_ID_PATTERN.test(approvalId)) throw refuse("USAGE", `"${approvalId}" is not an approval id`);
@@ -2030,6 +2132,11 @@ var ApprovalStore = class {
2030
2132
  }
2031
2133
  #stateError(record) {
2032
2134
  const refusal = refusalFor(record);
2135
+ if (approvalKind(record) === "download") {
2136
+ if (record.state === "expired") return refusal("APPROVAL_EXPIRED", "the question expired before it was answered", record);
2137
+ if (record.state === "revoked") return refusal("APPROVAL_VOID", `the question was voided (${record.reason ?? "revoked"})`, record);
2138
+ return refusal("APPROVAL_VOID", record.state === "used" ? "the question was answered already, and an answer is used once" : `the question is ${record.state}`, record);
2139
+ }
2033
2140
  if (record.state === "expired") return refusal("APPROVAL_EXPIRED", "the approval expired before it was used", record);
2034
2141
  if (record.state === "revoked") return refusal("APPROVAL_VOID", `the approval was voided (${record.reason ?? "revoked"})`, record);
2035
2142
  return refusal("APPROVAL_REQUIRED", `the approval is ${record.state}`, record);
@@ -2041,8 +2148,11 @@ var ApprovalStore = class {
2041
2148
  * somebody is about to approve, because an agent passed its id to a change, would punish the wrong party.
2042
2149
  */
2043
2150
  #requireKind(record, kind) {
2044
- if (approvalKind(record) === kind) return;
2151
+ const actual = approvalKind(record);
2152
+ if (actual === kind) return;
2045
2153
  const id = record.approvalId;
2154
+ if (actual === "download") throw (kind === "change" ? refuseChange : refuse)("USAGE", `${id} is a question about where to save files, not ${kind === "send" ? "a send" : "a configuration change"}`, record, DOWNLOAD_ANSWER_HINT);
2155
+ if (kind === "download") throw refuseDownload("USAGE", `${id} is ${actual === "change" ? "a configuration change" : "a send"}, not a question about where to save files`, record, "Pass the choice id the download’s question came with, and the person’s answer beside it.");
2046
2156
  throw kind === "send" ? refuse("USAGE", `approval ${id} is for a configuration change, not a send`, record, `A person approves it with \`agentcomms approve ${id}\` — or ${channelApproveCommands()}, whichever is installed — and it permits only the change it was prepared for.`) : refuseChange("USAGE", `approval ${id} is for a send, not a configuration change`, record, `It is approved with the command that prepared it — ${channelApproveCommands({ sending: true })} — and permits only that send.`);
2047
2157
  }
2048
2158
  /** Issues a new challenge to show a human; only its hash is kept. */
@@ -2264,6 +2374,139 @@ var ApprovalStore = class {
2264
2374
  await this.#markClaimed(result);
2265
2375
  return result;
2266
2376
  }
2377
+ /**
2378
+ * A download's question, pending, bound to `downloadDigest(input.download)` — computed here, never taken from the
2379
+ * caller, as a change's digest is. It stands in for the draft revision too.
2380
+ *
2381
+ * `policy` is the change policy of the mailbox or workspace the files come from, as it stands when the question is
2382
+ * asked: where a stranger's files land on this machine is a change to it, and is answered the way that account's
2383
+ * other changes are approved. A claim holds the question to the stricter of this and the policy then.
2384
+ */
2385
+ async createDownload(input) {
2386
+ const now = this.#now();
2387
+ const download = {
2388
+ summary: input.download.summary,
2389
+ target: { ...input.download.target },
2390
+ operation: input.download.operation,
2391
+ request: JSON.parse(canonicalJson$1(input.download.request)),
2392
+ files: [...input.download.files],
2393
+ names: [...input.download.names],
2394
+ folders: {
2395
+ downloads: input.download.folders.downloads,
2396
+ current: input.download.folders.current
2397
+ },
2398
+ ...input.download.listing === void 0 ? {} : { listing: input.download.listing.map((file) => ({
2399
+ name: file.name,
2400
+ size: file.size,
2401
+ ...file.renamed === void 0 ? {} : { renamed: file.renamed },
2402
+ ...file.flags === void 0 || file.flags.length === 0 ? {} : { flags: [...file.flags] }
2403
+ })) }
2404
+ };
2405
+ const digest = downloadDigest(download);
2406
+ const policy = input.policy === "chat" ? "chat" : "confirm";
2407
+ const record = {
2408
+ approvalId: newApprovalId(),
2409
+ kind: "download",
2410
+ digestVersion: 1,
2411
+ inboxId: download.target.id,
2412
+ draftId: "download",
2413
+ draftMessageId: digest,
2414
+ digest,
2415
+ policy,
2416
+ requiredPolicy: policy,
2417
+ riskFlags: [],
2418
+ expect: {
2419
+ to: [],
2420
+ cc: [],
2421
+ bcc: [],
2422
+ subject: download.summary
2423
+ },
2424
+ challengeAttempts: 0,
2425
+ state: "pending",
2426
+ createdAt: now.toISOString(),
2427
+ expiresAt: new Date(now.getTime() + this.#downloadTtlMs).toISOString(),
2428
+ updatedAt: now.toISOString(),
2429
+ download
2430
+ };
2431
+ await this.#write(record);
2432
+ return record;
2433
+ }
2434
+ /**
2435
+ * Records the person's answer to a download's question, given where an agent cannot give it: at their own terminal,
2436
+ * or in a form a trusted client showed them. The question moves to `approved`, carrying the answer, and waits for
2437
+ * the download to claim it — which it may then do under `confirm`, and which saves where this answer says.
2438
+ *
2439
+ * Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
2440
+ */
2441
+ async answerDownload(approvalId, via, answer) {
2442
+ const recorded = answer.choice === "other" ? {
2443
+ choice: "other",
2444
+ folder: answer.folder
2445
+ } : { choice: answer.choice };
2446
+ return await this.#transition(approvalId, (current) => {
2447
+ this.#requireKind(current, "download");
2448
+ if (current.state === "approved") throw refuseDownload("APPROVAL_VOID", "the question was answered already, and it is answered once", current);
2449
+ if (current.state !== "pending") throw this.#stateError(current);
2450
+ if (!current.download || downloadDigest(current.download) !== current.digest) throw refuseDownload("APPROVAL_VOID", "the question does not describe the download it is bound to", current);
2451
+ return {
2452
+ ...current,
2453
+ state: "approved",
2454
+ approvedVia: via,
2455
+ approvedDigest: current.digest,
2456
+ download: {
2457
+ ...current.download,
2458
+ answer: recorded
2459
+ }
2460
+ };
2461
+ });
2462
+ }
2463
+ /**
2464
+ * Claims a download's question, once, for the download the caller is about to make — and returns it, with the
2465
+ * folders it offered, so that `downloads` and `current` in the answer mean the paths the person read, and with the
2466
+ * answer the person recorded, when they recorded one.
2467
+ *
2468
+ * `live` is the download as the caller computes it now: the same account, the same request, the same files under
2469
+ * the same names — the person answered for those files and no others. Any other is refused, and the question left
2470
+ * open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
2471
+ * question for it would make the person answer again for nothing. It still expires, and is claimed once.
2472
+ *
2473
+ * `options.policy` is the change policy of that account now, and the stricter of it and the one the question was
2474
+ * asked under decides. Under `chat`, a pending question is claimed with the answer the caller carries: the person
2475
+ * gave it in the conversation. Under `confirm`, only a question the person answered at a terminal or in a trusted
2476
+ * form can be claimed; a pending one is refused, and left as it is, so they can still answer it. One used, voided or
2477
+ * expired is refused as an approval in that state is.
2478
+ */
2479
+ async claimForDownload(approvalId, live, options = {}) {
2480
+ const digest = downloadDigest(live);
2481
+ let failure = null;
2482
+ const result = await this.#transition(approvalId, (current) => {
2483
+ this.#requireKind(current, "download");
2484
+ if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
2485
+ const voidWith = (reason) => {
2486
+ failure = {
2487
+ code: "APPROVAL_VOID",
2488
+ reason
2489
+ };
2490
+ return {
2491
+ ...current,
2492
+ state: "revoked",
2493
+ reason
2494
+ };
2495
+ };
2496
+ if (!current.download || downloadDigest(current.download) !== current.digest) return voidWith("the question does not describe the download it is bound to");
2497
+ if (digest !== current.digest) throw refuseDownload("USAGE", `${downloadDrift(current.download, live)}; the question is still open`, current, "Call again with the same arguments the question was asked with, and its choice id. If the files themselves changed, make the download again without an answer, and show the person the new question.");
2498
+ const refusal = downloadClaimRefusal(current, options.policy ?? "chat", options.pendingHint);
2499
+ if (refusal !== null) throw refusal;
2500
+ return {
2501
+ ...current,
2502
+ state: "used"
2503
+ };
2504
+ });
2505
+ const failed = failure;
2506
+ if (failed) throw refuseDownload(failed.code, failed.reason, result);
2507
+ await this.#markClaimed(result);
2508
+ return result;
2509
+ }
2267
2510
  /** Records the outcome of the one send attempt. */
2268
2511
  async complete(approvalId, outcome) {
2269
2512
  return this.#transition(approvalId, (current) => {
@@ -3144,6 +3387,59 @@ function packageFrom(server) {
3144
3387
  const line = [server.command, ...server.args].join(" ");
3145
3388
  return /(@[\w.-]+\/[\w.-]+|(?<=\s)[\w.-]+-mcp)(?=@|\s|$)/.exec(line)?.[1];
3146
3389
  }
3390
+ /**
3391
+ * The programs this package starts, named so that no folder it happens to be in can stand in for them.
3392
+ *
3393
+ * A bare name is looked up, and on Windows the lookup starts in the current folder: libuv tries it before `PATH`, and
3394
+ * so does every program that looks a name up with `SearchPath` or `CreateProcess`. The current folder is where a
3395
+ * command was run or a server was started — and, since a download asks where to save, one of the three places a
3396
+ * stranger's file may be saved into. A `reg.exe` or `rundll32.exe` saved there would then be what runs the next time
3397
+ * this package reads the registry or opens a browser. So a Windows program is named by its full path, under the Windows
3398
+ * folder, and every process started on Windows is told, by `NoDefaultCurrentDirectoryInExePath`, not to look in its
3399
+ * own current folder for the programs it starts in turn.
3400
+ */
3401
+ /** An environment variable by name, whatever its case: Windows's are case-blind, an object passed in is not. */
3402
+ function envValue(env, name) {
3403
+ const wanted = name.toLowerCase();
3404
+ for (const [key, value] of Object.entries(env)) if (key.toLowerCase() === wanted && value) return value;
3405
+ }
3406
+ /**
3407
+ * The Windows folder: `%SystemRoot%` (or `%windir%`) when it names a folder on a drive, and `C:\Windows` otherwise.
3408
+ * A value that is not a drive's absolute path — relative, a share, a device path — is not taken: it would be looked up
3409
+ * against the current folder, or on another machine, which is what naming the program in full is to avoid.
3410
+ */
3411
+ function windowsFolder(env = process.env) {
3412
+ for (const name of ["SystemRoot", "windir"]) {
3413
+ const value = envValue(env, name);
3414
+ if (value !== void 0 && /^[A-Za-z]:[\\/]/.test(value)) return path.win32.resolve(value);
3415
+ }
3416
+ return "C:\\Windows";
3417
+ }
3418
+ /** A program of Windows's own by its full path: `%SystemRoot%\System32\<name>`, never a bare name to look up. */
3419
+ function windowsSystemProgram(name, env = process.env) {
3420
+ return path.win32.join(windowsFolder(env), "System32", name);
3421
+ }
3422
+ /**
3423
+ * The environment a child process is started with: `env` as it is, and on Windows also
3424
+ * `NoDefaultCurrentDirectoryInExePath=1`, so that the child — `rundll32.exe`, a Node started again — never takes a
3425
+ * program from its current folder either. Elsewhere nothing is added: a Unix shell looks in the current folder only
3426
+ * when `PATH` says to.
3427
+ */
3428
+ function childEnvironment(env = process.env, platform = process.platform) {
3429
+ if (platform !== "win32") return env;
3430
+ return {
3431
+ ...env,
3432
+ NoDefaultCurrentDirectoryInExePath: "1"
3433
+ };
3434
+ }
3435
+ /**
3436
+ * The directories of a search path that name a folder on their own: absolute, and on Windows on a drive. An empty
3437
+ * entry means the current folder to a Unix shell, and a relative one is read against it; either would make the program
3438
+ * found — and run — depend on where this was started, which is where a download may have saved a stranger's file.
3439
+ */
3440
+ function absoluteSearchPath(entries, platform = process.platform) {
3441
+ return entries.filter((entry) => platform === "win32" ? /^[A-Za-z]:[\\/]/.test(entry) : entry !== "" && path.posix.isAbsolute(entry));
3442
+ }
3147
3443
  /** The install options that pin a server to one account: the generic one, and Gmail's and Slack's own names. */
3148
3444
  const PIN_OPTIONS = Object.freeze([
3149
3445
  "account",
@@ -3199,9 +3495,14 @@ function clientCliSearch(env) {
3199
3495
  /**
3200
3496
  * The first of `directories` that holds an executable `name`. PATH is read by the caller, once: a test that counts
3201
3497
  * how often the installer looks at PATH counts exactly the looks it means.
3498
+ *
3499
+ * Only absolute directories are looked in, whoever the caller. An empty entry of PATH is the current folder to a
3500
+ * shell, and a relative one is read against it: the `node`, `npx` or `claude` found there — and then run, or written
3501
+ * into a client's config to be run — would be whatever is in the folder this was started from, which is where a
3502
+ * download may have saved a stranger's file.
3202
3503
  */
3203
3504
  async function executableIn(name, directories, env) {
3204
- const paths = directories.filter(Boolean);
3505
+ const paths = absoluteSearchPath(directories);
3205
3506
  const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
3206
3507
  for (const directory of paths) for (const extension of extensions) {
3207
3508
  const candidate = join(directory, name + extension.toLowerCase());
@@ -3348,13 +3649,20 @@ async function findNpmCli() {
3348
3649
  } catch {}
3349
3650
  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." });
3350
3651
  }
3652
+ /**
3653
+ * A command run to its end. Its environment is this process's, and on Windows also tells it never to take a program
3654
+ * from its current folder: `npm` and a client's command both start programs of their own by name.
3655
+ */
3351
3656
  function run(command, args) {
3352
3657
  return new Promise((resolvePromise, reject) => {
3353
- const child = spawn(command, args, { stdio: [
3354
- "ignore",
3355
- "ignore",
3356
- "pipe"
3357
- ] });
3658
+ const child = spawn(command, args, {
3659
+ stdio: [
3660
+ "ignore",
3661
+ "ignore",
3662
+ "pipe"
3663
+ ],
3664
+ env: childEnvironment(process.env)
3665
+ });
3358
3666
  let stderr = "";
3359
3667
  child.stderr?.on("data", (chunk) => {
3360
3668
  stderr += String(chunk);
@@ -3580,14 +3888,20 @@ function installCommand$1(product, options, name, extra = []) {
3580
3888
  words.push(...extra);
3581
3889
  return words.map((word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`).join(" ");
3582
3890
  }
3583
- /** A command's exit status and what it printed, kept here and never shown: codex prints an entry's env. */
3891
+ /**
3892
+ * 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, so that on Windows it takes no program from its current folder either.
3894
+ */
3584
3895
  function capture$1(command, args) {
3585
3896
  return new Promise((resolvePromise, reject) => {
3586
- const child = spawn(command, args, { stdio: [
3587
- "ignore",
3588
- "pipe",
3589
- "pipe"
3590
- ] });
3897
+ const child = spawn(command, args, {
3898
+ stdio: [
3899
+ "ignore",
3900
+ "pipe",
3901
+ "pipe"
3902
+ ],
3903
+ env: childEnvironment(process.env)
3904
+ });
3591
3905
  let stdout = "";
3592
3906
  let stderr = "";
3593
3907
  child.stdout?.on("data", (chunk) => {
@@ -4040,11 +4354,27 @@ async function verifyEntry(entry, product) {
4040
4354
  await client.close().catch(() => void 0);
4041
4355
  }
4042
4356
  }
4043
- /** Every running process's command line, or null when they cannot be listed. */
4044
- async function runningCommandLines() {
4357
+ /** Where `ps` is looked for, in order: the system's own directories, never PATH. */
4358
+ const PS_DIRECTORIES = ["/bin", "/usr/bin"];
4359
+ /**
4360
+ * Every running process's command line, or null when they cannot be listed.
4361
+ *
4362
+ * `ps` is started by its full path, `/bin/ps` or else `/usr/bin/ps`, never by its bare name: a name is looked up, and
4363
+ * a lookup can reach the folder this was started from — where a download may have saved a stranger's `ps`, which would
4364
+ * then run, and whose answer would decide what is deleted. With neither there, the processes cannot be listed.
4365
+ *
4366
+ * `directories` and `list` are for tests: where to look for `ps`, and what runs the one found.
4367
+ */
4368
+ async function runningCommandLines(options = {}) {
4045
4369
  if (process.platform === "win32") return null;
4370
+ const ps = await executableIn("ps", options.directories ?? PS_DIRECTORIES, process.env);
4371
+ if (ps === null) return null;
4372
+ return (options.list ?? commandLinesFrom)(ps);
4373
+ }
4374
+ /** What `ps` at this path says is running: one command line per process, or null when it fails. */
4375
+ function commandLinesFrom(ps) {
4046
4376
  return new Promise((resolvePromise) => {
4047
- execFile("ps", [
4377
+ execFile(ps, [
4048
4378
  "-A",
4049
4379
  "-o",
4050
4380
  "args="
@@ -4780,6 +5110,7 @@ async function claimChange(core, approvalId, expect, options) {
4780
5110
  async function changeRecord(core, approvalId) {
4781
5111
  const record = await core.approvals.get(approvalId);
4782
5112
  if (!record) throw new CommsError("NOT_FOUND", `no approval ${approvalId}`, { hint: "Prepare the change again; an approval expires ten minutes after it is made." });
5113
+ if (approvalKind(record) === "download") throw new CommsError("USAGE", `${approvalId} is a question about where to save files, not a configuration change`, { hint: DOWNLOAD_ANSWER_HINT });
4783
5114
  if (approvalKind(record) !== "change") throw new CommsError("USAGE", `approval ${approvalId} is for a send, not a configuration change`, { hint: `Approve it with the command that prepared it: ${channelApproveCommands({ sending: true })}.` });
4784
5115
  if (!record.change || changeDigest(record.change) !== record.digest) throw new CommsError("BAD_DATA", "this approval does not describe the change it is bound to", {
4785
5116
  hint: "Nothing was approved. Prepare the change again.",
@@ -5627,7 +5958,7 @@ function isPrerelease(version) {
5627
5958
  return VERSION_PATTERN.exec(version)?.[4] !== void 0;
5628
5959
  }
5629
5960
  /** The package version, read from package.json at build time. */
5630
- const VERSION = "0.9.0";
5961
+ const VERSION = "0.11.0";
5631
5962
  /**
5632
5963
  * Registering, pruning and listing the MCP servers of every channel, from the core.
5633
5964
  *
@@ -5882,7 +6213,7 @@ function serverPruneChange(core, env, request, own) {
5882
6213
  };
5883
6214
  const { packageName, version } = ownProduct(request.channel, own) ?? {
5884
6215
  packageName: channelServer(request.channel).packageName,
5885
- version: "0.9.0"
6216
+ version: "0.11.0"
5886
6217
  };
5887
6218
  const product = {
5888
6219
  packageName,
@@ -6227,6 +6558,10 @@ Object.freeze({
6227
6558
  lookup: true
6228
6559
  });
6229
6560
  Object.freeze({ kind: "change" });
6561
+ Object.freeze({
6562
+ kind: "download",
6563
+ argument: "choiceId"
6564
+ });
6230
6565
  /**
6231
6566
  * Whether a call claims an approval the person already gave (§2): an approval this machine's approval store holds —
6232
6567
  * every channel keeps its approvals there — that is still waiting to be used, pending or approved, and of the kind the
@@ -6278,7 +6613,8 @@ function updateToolGate(options) {
6278
6613
  surface: "server"
6279
6614
  });
6280
6615
  if (pending === null) return null;
6281
- if (await claimsApproval(options.core, args.approvalId, options.approvals?.[tool])) return null;
6616
+ const claim = options.approvals?.[tool];
6617
+ if (await claimsApproval(options.core, args[claim?.argument ?? "approvalId"], claim)) return null;
6282
6618
  return stoppedCall(pending, {
6283
6619
  server: options.server,
6284
6620
  tool
@@ -6343,9 +6679,10 @@ function commandPathOf(command) {
6343
6679
  }
6344
6680
  /**
6345
6681
  * The approvals a Commander command claims: its `--approval`, the `--mcp-approval` Gmail's `setup` carries beside it,
6346
- * and an argument named `approvalId` — `agent-gmail send cancel <approvalId>`, `agent-resend send execute
6347
- * <approvalId>`, the commands whose tools take it as `approvalId`. What the gate hands `claimsApproval`, as a tool
6348
- * call's `approvalId` is handed it. Commander has read the arguments by the time a `preAction` hook runs.
6682
+ * the `--choice` a download carries its question's id in, and an argument named `approvalId` — `agent-gmail send
6683
+ * cancel <approvalId>`, `agent-resend send execute <approvalId>`, the commands whose tools take it as `approvalId`.
6684
+ * What the gate hands `claimsApproval`, as a tool call's `approvalId` is handed it. Commander has read the arguments
6685
+ * by the time a `preAction` hook runs.
6349
6686
  */
6350
6687
  function approvalsOf(command) {
6351
6688
  const options = command.opts();
@@ -6353,6 +6690,7 @@ function approvalsOf(command) {
6353
6690
  return [
6354
6691
  options.approval,
6355
6692
  options.mcpApproval,
6693
+ options.choice,
6356
6694
  ...named
6357
6695
  ];
6358
6696
  }
@@ -6499,7 +6837,7 @@ async function askLine(streams, question) {
6499
6837
  }
6500
6838
  }
6501
6839
  //#endregion
6502
- //#region ../core/dist/tool-arguments-Cx3Of7xZ.mjs
6840
+ //#region ../core/dist/tool-arguments-5aAzuZnA.mjs
6503
6841
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
6504
6842
  const TARGET = "draft-2020-12";
6505
6843
  /**
@@ -6517,7 +6855,7 @@ const TARGET = "draft-2020-12";
6517
6855
  * in, because what it asks the registry with is each server's own — and WhatsApp's is nothing at all. A call it
6518
6856
  * answers reaches nothing, as a refused one does.
6519
6857
  */
6520
- function strictToolArguments(server, refuse, gate) {
6858
+ function strictToolArguments(server, refuse, gate, retired = {}) {
6521
6859
  const target = server;
6522
6860
  if (typeof target.registerTool !== "function") throw new Error("strictToolArguments needs an MCP server: this has no registerTool");
6523
6861
  if (target[APPLIED]) return;
@@ -6533,7 +6871,7 @@ function strictToolArguments(server, refuse, gate) {
6533
6871
  inputSchema: passThrough(schema)
6534
6872
  }, async (input, context) => {
6535
6873
  const parsed = await schema.safeParseAsync(input ?? {});
6536
- if (!parsed.success) return refuse(refusal(name, published, input, parsed.error.issues));
6874
+ if (!parsed.success) return refuse(refusal(name, published, input, parsed.error.issues, retired[name] ?? {}));
6537
6875
  const stopped = gate ? await gate(name, parsed.data) : null;
6538
6876
  if (stopped !== null) return stopped;
6539
6877
  return declared ? handler(parsed.data, context) : handler(context);
@@ -6577,12 +6915,22 @@ function passThrough(schema) {
6577
6915
  } };
6578
6916
  }
6579
6917
  /** A USAGE refusal naming what was wrong and what the tool takes. */
6580
- function refusal(tool, published, input, issues) {
6918
+ function refusal(tool, published, input, issues, retired) {
6581
6919
  const properties = published.properties ?? {};
6582
6920
  const takes = Object.keys(properties);
6583
6921
  const required = new Set(published.required ?? []);
6584
6922
  const signature = takes.length === 0 ? `${tool} takes no arguments.` : `${tool} takes ${spoken(takes.map((key) => required.has(key) ? `\`${key}\` (required)` : `\`${key}\``), "and")}.`;
6585
6923
  const unknown = issues.flatMap((issue) => issue.code === "unrecognized_keys" && issue.path.length === 0 ? issue.keys : []);
6924
+ const removed = unknown.filter((key) => Object.hasOwn(retired, key));
6925
+ if (removed.length > 0) return new CommsError("USAGE", `${tool} no longer takes ${spoken(removed.map((key) => `\`${key}\``), "or")}`, {
6926
+ hint: removed.map((key) => retired[key]).join(" "),
6927
+ details: {
6928
+ tool,
6929
+ retired: removed,
6930
+ unknown,
6931
+ takes
6932
+ }
6933
+ });
6586
6934
  if (unknown.length > 0) return new CommsError("USAGE", `${tool} does not take ${spoken(unknown.map((key) => `\`${key}\``), "or")}`, {
6587
6935
  hint: takes.length === 0 ? signature : `${signature} Leave out anything else.`,
6588
6936
  details: {
@@ -10096,4 +10444,4 @@ async function syncAccount(context, request) {
10096
10444
  });
10097
10445
  }
10098
10446
  //#endregion
10099
- export { serverInstallChange as $, CommsError as A, escapeForDisplay as B, checkStorePath as C, strictToolArguments as D, UNTRUSTED_NOTICE as E, approveChangeAtTerminal as F, lookupName as G, gatedChangeAtTerminal as H, canPrompt as I, openCore as J, nameAvailable as K, channelManifest as L, agentMarker as M, approvalKind as N, CHANNELS as O, approvalsOf as P, runCommand as Q, colorEnabled as R, storeIdentity as S, expandSource as T, installExitStatus as U, exemptFromUpdateGate as V, isCommsError as W, renderInstall as X, paint as Y, renderPrune as Z, chatRefOf as _, listChats as a, withFileLock as at, requireAccount as b, Presenter as c, draftMessage as d, serverPruneChange as et, Visibility as f, requireSupportedNode as g, WhatsAppIndex as h, CHAT_KINDS as i, updateToolGate as it, EXIT_CODES as j, CHANNEL_SERVERS as k, innerText as l, WhatsAppContext as m, whatsappStatus as n, truncateDisplay as nt, readChat as o, writeResult as ot, listKey as p, nameShapeProblem as q, probeStore as r, updateGateAtTerminal as rt, searchMessages as s, VERSION$1 as st, syncAccount as t, toCommsError as tt, composeDraft as u, PLATFORM as v, defaultStorePath as w, requireNamedConfig as x, newWhatsAppAccount as y, commandPathOf as z };
10447
+ export { renderPrune as $, CommsError as A, colorEnabled as B, checkStorePath as C, strictToolArguments as D, UNTRUSTED_NOTICE as E, approvalsOf as F, installExitStatus as G, escapeForDisplay as H, approveChangeAtTerminal as I, nameAvailable as J, isCommsError as K, canPrompt as L, absoluteSearchPath as M, agentMarker as N, CHANNELS as O, approvalKind as P, renderInstall as Q, channelManifest as R, storeIdentity as S, expandSource as T, exemptFromUpdateGate as U, commandPathOf as V, gatedChangeAtTerminal as W, openCore as X, nameShapeProblem as Y, paint as Z, chatRefOf as _, listChats as a, updateGateAtTerminal as at, requireAccount as b, Presenter as c, withFileLock as ct, draftMessage as d, runCommand as et, Visibility as f, requireSupportedNode as g, WhatsAppIndex as h, CHAT_KINDS as i, truncateDisplay as it, EXIT_CODES as j, CHANNEL_SERVERS as k, innerText as l, writeResult as lt, WhatsAppContext as m, whatsappStatus as n, serverPruneChange as nt, readChat as o, updateToolGate as ot, listKey as p, lookupName as q, probeStore as r, toCommsError as rt, searchMessages as s, windowsSystemProgram as st, syncAccount as t, serverInstallChange as tt, composeDraft as u, VERSION$1 as ut, PLATFORM as v, defaultStorePath as w, requireNamedConfig as x, newWhatsAppAccount as y, childEnvironment as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcomms/whatsapp",
3
- "version": "0.9.0",
3
+ "version": "0.11.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.9.0"
41
+ "@agentcomms/core": "0.11.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": "7db27256fda2e40fcc2a4958c50bf77bff871ef1"
90
+ "gitHead": "bcc1392f8380dbf523a910a87de652a18faead6f"
91
91
  }