@agentcomms/whatsapp 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as renderPrune, A as CommsError, B as colorEnabled, C as checkStorePath, F as approvalsOf, G as installExitStatus, H as escapeForDisplay, I as approveChangeAtTerminal, J as nameAvailable, K as isCommsError, L as canPrompt, M as absoluteSearchPath, N as agentMarker, O as CHANNELS, P as approvalKind, Q as renderInstall, R as channelManifest, S as storeIdentity, T as expandSource, U as exemptFromUpdateGate, V as commandPathOf, W as gatedChangeAtTerminal, X as openCore, Y as nameShapeProblem, Z as paint, _ as chatRefOf, a as listChats, at as updateGateAtTerminal, b as requireAccount, c as Presenter, ct as withFileLock, d as draftMessage, et as runCommand, f as Visibility, g as requireSupportedNode, h as WhatsAppIndex, i as CHAT_KINDS, it as truncateDisplay, j as EXIT_CODES, k as CHANNEL_SERVERS, l as innerText, lt as writeResult, m as WhatsAppContext, n as whatsappStatus, nt as serverPruneChange, o as readChat, p as listKey, q as lookupName, r as probeStore, s as searchMessages, st as windowsSystemProgram, t as syncAccount, tt as serverInstallChange, ut as VERSION, v as PLATFORM, w as defaultStorePath, x as requireNamedConfig, y as newWhatsAppAccount, z as childEnvironment } from "./sync-
|
|
2
|
+
import { $ as renderPrune, A as CommsError, B as colorEnabled, C as checkStorePath, F as approvalsOf, G as installExitStatus, H as escapeForDisplay, I as approveChangeAtTerminal, J as nameAvailable, K as isCommsError, L as canPrompt, M as absoluteSearchPath, N as agentMarker, O as CHANNELS, P as approvalKind, Q as renderInstall, R as channelManifest, S as storeIdentity, T as expandSource, U as exemptFromUpdateGate, V as commandPathOf, W as gatedChangeAtTerminal, X as openCore, Y as nameShapeProblem, Z as paint, _ as chatRefOf, a as listChats, at as updateGateAtTerminal, b as requireAccount, c as Presenter, ct as withFileLock, d as draftMessage, et as runCommand, f as Visibility, g as requireSupportedNode, h as WhatsAppIndex, i as CHAT_KINDS, it as truncateDisplay, j as EXIT_CODES, k as CHANNEL_SERVERS, l as innerText, lt as writeResult, m as WhatsAppContext, n as whatsappStatus, nt as serverPruneChange, o as readChat, p as listKey, q as lookupName, r as probeStore, s as searchMessages, st as windowsSystemProgram, t as syncAccount, tt as serverInstallChange, ut as VERSION, v as PLATFORM, w as defaultStorePath, x as requireNamedConfig, y as newWhatsAppAccount, z as childEnvironment } from "./sync-C2_cXhZ0.mjs";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import path, { delimiter, join } from "node:path";
|
|
5
5
|
import fs, { accessSync, constants as constants$1 } from "node:fs";
|
|
6
|
-
import childProcess, { spawn } from "node:child_process";
|
|
7
6
|
import { stripVTControlCharacters } from "node:util";
|
|
7
|
+
import childProcess, { spawn } from "node:child_process";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
|
|
@@ -3534,7 +3534,7 @@ a macOS dialog may be waiting) · 77 permission needed (macOS privacy) · 78 con
|
|
|
3534
3534
|
ran = true;
|
|
3535
3535
|
exitCode = await runCommand(output(), async () => {
|
|
3536
3536
|
requireSupportedNode();
|
|
3537
|
-
const { startWhatsAppStdioServer } = await import("./stdio-entry-
|
|
3537
|
+
const { startWhatsAppStdioServer } = await import("./stdio-entry-irMynM6w.mjs");
|
|
3538
3538
|
await startWhatsAppStdioServer({
|
|
3539
3539
|
env,
|
|
3540
3540
|
...flags.account ? { account: String(flags.account) } : {}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { u as composeDraft, ut as VERSION } from "./sync-
|
|
2
|
-
import { t as createWhatsAppMcpServer } from "./server-
|
|
1
|
+
import { u as composeDraft, ut as VERSION } from "./sync-C2_cXhZ0.mjs";
|
|
2
|
+
import { t as createWhatsAppMcpServer } from "./server-ag_r78jB.mjs";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
/**
|
|
5
5
|
* The library entry of `@agentcomms/whatsapp`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as strictToolArguments, E as UNTRUSTED_NOTICE, a as listChats, d as draftMessage, i as CHAT_KINDS, m as WhatsAppContext, n as whatsappStatus, o as readChat, ot as updateToolGate, rt as toCommsError, s as searchMessages, t as syncAccount, ut as VERSION } from "./sync-
|
|
1
|
+
import { D as strictToolArguments, E as UNTRUSTED_NOTICE, a as listChats, d as draftMessage, i as CHAT_KINDS, m as WhatsAppContext, n as whatsappStatus, o as readChat, ot as updateToolGate, rt as toCommsError, s as searchMessages, t as syncAccount, ut as VERSION } from "./sync-C2_cXhZ0.mjs";
|
|
2
2
|
import { c as boolean, h as number, x as string } from "./schemas-DiGKNDNq.mjs";
|
|
3
3
|
import { t as McpServer } from "./mcp-DXXb3Vv3-Co5BzjYe.mjs";
|
|
4
4
|
//#region src/mcp/server.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createWhatsAppMcpServer } from "./server-
|
|
1
|
+
import { t as createWhatsAppMcpServer } from "./server-ag_r78jB.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
|
|
@@ -6,14 +6,14 @@ import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto
|
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import { homedir, platform, userInfo } from "node:os";
|
|
8
8
|
import { constants as constants$1 } from "node:fs";
|
|
9
|
-
import { execFile, spawn } from "node:child_process";
|
|
10
9
|
import { styleText } from "node:util";
|
|
10
|
+
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.12.
|
|
14
|
+
const VERSION$1 = "0.12.2";
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region ../core/dist/update-check-
|
|
16
|
+
//#region ../core/dist/update-check-B-WYPZae.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
|
|
@@ -392,9 +392,15 @@ async function takeOverStale(lockPath, staleMs) {
|
|
|
392
392
|
async function withFileLock(lockPath, fn, options = {}) {
|
|
393
393
|
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
394
394
|
const staleMs = options.staleMs ?? 3e4;
|
|
395
|
-
const
|
|
395
|
+
const started = Date.now();
|
|
396
|
+
let deadline = started + timeoutMs;
|
|
397
|
+
/** `maxWaitMs`'s end: fixed when the wait begins, whatever the lock does. */
|
|
398
|
+
const limit = options.maxWaitMs === void 0 ? Number.POSITIVE_INFINITY : started + options.maxWaitMs;
|
|
396
399
|
const token = randomBytes(12).toString("hex");
|
|
397
400
|
let lastCode = "EEXIST";
|
|
401
|
+
/** The token of the holder this caller saw last, and how many times it has seen the lock change hands. */
|
|
402
|
+
let holder;
|
|
403
|
+
let handOvers = 0;
|
|
398
404
|
await ensurePrivateDir(dirname(lockPath));
|
|
399
405
|
for (;;) try {
|
|
400
406
|
const handle = await open(lockPath, "wx", 384);
|
|
@@ -409,10 +415,20 @@ async function withFileLock(lockPath, fn, options = {}) {
|
|
|
409
415
|
const code = error.code;
|
|
410
416
|
if (!CONTENDED.has(code ?? "")) throw error;
|
|
411
417
|
lastCode = code ?? lastCode;
|
|
412
|
-
if (code === "EEXIST"
|
|
413
|
-
await
|
|
414
|
-
|
|
418
|
+
if (code === "EEXIST") {
|
|
419
|
+
const body = await readLock(lockPath);
|
|
420
|
+
if (await isStale(lockPath, body, staleMs)) {
|
|
421
|
+
await takeOverStale(lockPath, staleMs);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const seen = typeof body?.token === "string" ? body.token : void 0;
|
|
425
|
+
if (seen !== void 0 && seen !== holder) {
|
|
426
|
+
if (holder !== void 0) handOvers += 1;
|
|
427
|
+
holder = seen;
|
|
428
|
+
if (options.timeoutPerHolder) deadline = Date.now() + timeoutMs;
|
|
429
|
+
}
|
|
415
430
|
}
|
|
431
|
+
if (Date.now() > limit) throw new CommsError("LOCK_TIMEOUT", `gave up waiting for ${lockPath}: the wait hit its overall limit of ${(limit - started) / 1e3} s`, { hint: (handOvers > 0 ? `It changed hands ${handOvers} time(s) while this waited, never to this process: busy rather than stuck. Retry in a moment.` : "Retry in a moment. If it persists and no other process is running, delete the lock file.") + (lastCode === "EEXIST" ? "" : ` (last error: ${lastCode})`) });
|
|
416
432
|
if (Date.now() > deadline) throw new CommsError("LOCK_TIMEOUT", `another agent-communications process is holding ${lockPath}`, { hint: `Retry in a moment. If it persists and no other process is running, delete the lock file.` + (lastCode === "EEXIST" ? "" : ` (last error: ${lastCode})`) });
|
|
417
433
|
await setTimeout$1(25 + Math.floor(Math.random() * 50));
|
|
418
434
|
}
|
|
@@ -2135,6 +2151,32 @@ function refusalFor(record) {
|
|
|
2135
2151
|
const kind = approvalKind(record);
|
|
2136
2152
|
return kind === "change" ? refuseChange : kind === "download" ? refuseDownload : refuse;
|
|
2137
2153
|
}
|
|
2154
|
+
/**
|
|
2155
|
+
* A claim whose caller was cancelled before it changed anything (`ClaimOptions.signal`).
|
|
2156
|
+
*
|
|
2157
|
+
* `USAGE` and `cancelled:`, the words every cancellation in this repository is reported in, with `details.reason`
|
|
2158
|
+
* saying so for a caller that branches on it — and in the record's own kind: a question is not an approval, and
|
|
2159
|
+
* nothing is sent by answering one. The record is as it was, and the hint says so.
|
|
2160
|
+
*/
|
|
2161
|
+
function cancelledClaim(record) {
|
|
2162
|
+
const kind = approvalKind(record);
|
|
2163
|
+
if (kind === "download") return new CommsError("USAGE", "cancelled: nothing was saved", {
|
|
2164
|
+
hint: "The question was not used: the same call, made again with the same answer, can still use it until it expires.",
|
|
2165
|
+
details: {
|
|
2166
|
+
choiceId: record.approvalId,
|
|
2167
|
+
state: record.state,
|
|
2168
|
+
reason: "cancelled"
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
return new CommsError("USAGE", `cancelled: ${kind === "change" ? "nothing was changed" : "nothing was sent"}`, {
|
|
2172
|
+
hint: "The approval was not used: the same call, made again, can still use it until it expires.",
|
|
2173
|
+
details: {
|
|
2174
|
+
approvalId: record.approvalId,
|
|
2175
|
+
state: record.state,
|
|
2176
|
+
reason: "cancelled"
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2138
2180
|
var ApprovalStore = class {
|
|
2139
2181
|
directory;
|
|
2140
2182
|
#now;
|
|
@@ -2338,6 +2380,7 @@ var ApprovalStore = class {
|
|
|
2338
2380
|
const result = await this.#transition(approvalId, (current) => {
|
|
2339
2381
|
this.#requireKind(current, "send");
|
|
2340
2382
|
if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
|
|
2383
|
+
if (options.signal?.aborted) throw cancelledClaim(current);
|
|
2341
2384
|
const voidWith = (code, reason) => {
|
|
2342
2385
|
failure = {
|
|
2343
2386
|
code,
|
|
@@ -2448,6 +2491,7 @@ var ApprovalStore = class {
|
|
|
2448
2491
|
const result = await this.#transition(approvalId, (current) => {
|
|
2449
2492
|
this.#requireKind(current, "change");
|
|
2450
2493
|
if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
|
|
2494
|
+
if (options.signal?.aborted) throw cancelledClaim(current);
|
|
2451
2495
|
const voidWith = (reason) => {
|
|
2452
2496
|
failure = {
|
|
2453
2497
|
code: "APPROVAL_VOID",
|
|
@@ -2575,6 +2619,9 @@ var ApprovalStore = class {
|
|
|
2575
2619
|
* gave it in the conversation. Under `confirm`, only a question the person answered at a terminal or in a trusted
|
|
2576
2620
|
* form can be claimed; a pending one is refused, and left as it is, so they can still answer it. One used, voided or
|
|
2577
2621
|
* expired is refused as an approval in that state is.
|
|
2622
|
+
*
|
|
2623
|
+
* `options.signal` is the download's cancellation, asked under the lock as a send's is (`ClaimOptions.signal`): a
|
|
2624
|
+
* download cancelled while this waited saves nothing and leaves the question open, the answer still unused.
|
|
2578
2625
|
*/
|
|
2579
2626
|
async claimForDownload(approvalId, live, options = {}) {
|
|
2580
2627
|
const digest = downloadDigest(live);
|
|
@@ -2582,6 +2629,7 @@ var ApprovalStore = class {
|
|
|
2582
2629
|
const result = await this.#transition(approvalId, (current) => {
|
|
2583
2630
|
this.#requireKind(current, "download");
|
|
2584
2631
|
if (current.state !== "pending" && current.state !== "approved") throw this.#stateError(current);
|
|
2632
|
+
if (options.signal?.aborted) throw cancelledClaim(current);
|
|
2585
2633
|
const voidWith = (reason) => {
|
|
2586
2634
|
failure = {
|
|
2587
2635
|
code: "APPROVAL_VOID",
|
|
@@ -2850,6 +2898,219 @@ function nameAvailable(config, kind, name, platform) {
|
|
|
2850
2898
|
}
|
|
2851
2899
|
return { ok: true };
|
|
2852
2900
|
}
|
|
2901
|
+
function okEnvelope(data) {
|
|
2902
|
+
return {
|
|
2903
|
+
ok: true,
|
|
2904
|
+
schemaVersion: 1,
|
|
2905
|
+
data
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
function errorEnvelope(error) {
|
|
2909
|
+
const body = {
|
|
2910
|
+
code: error.code,
|
|
2911
|
+
message: error.message
|
|
2912
|
+
};
|
|
2913
|
+
if (error.hint !== void 0) body.hint = error.hint;
|
|
2914
|
+
if (error.details !== void 0) body.details = error.details;
|
|
2915
|
+
return {
|
|
2916
|
+
ok: false,
|
|
2917
|
+
schemaVersion: 1,
|
|
2918
|
+
error: body
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
const defaultStreams = {
|
|
2922
|
+
stdout: process.stdout,
|
|
2923
|
+
stderr: process.stderr,
|
|
2924
|
+
stdin: process.stdin
|
|
2925
|
+
};
|
|
2926
|
+
function colorEnabled(env, stream, flag) {
|
|
2927
|
+
if (flag === false) return false;
|
|
2928
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
2929
|
+
if (env.TERM === "dumb") return false;
|
|
2930
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
|
|
2931
|
+
return Boolean(stream.isTTY);
|
|
2932
|
+
}
|
|
2933
|
+
/** True when a human could answer a prompt: both ends are terminals, no --json/--no-input, not CI. */
|
|
2934
|
+
function canPrompt(env, streams, options) {
|
|
2935
|
+
if (options.json || options.noInput) return false;
|
|
2936
|
+
if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
|
|
2937
|
+
return Boolean(streams.stdin?.isTTY && streams.stdout.isTTY);
|
|
2938
|
+
}
|
|
2939
|
+
/** Environment variables well-known coding agents set. Used only as a speed bump, never as a security boundary. */
|
|
2940
|
+
const AGENT_MARKERS = [
|
|
2941
|
+
"CLAUDECODE",
|
|
2942
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
2943
|
+
"CODEX_SANDBOX",
|
|
2944
|
+
"CODEX_HOME",
|
|
2945
|
+
"CURSOR_AGENT",
|
|
2946
|
+
"GEMINI_CLI",
|
|
2947
|
+
"AGENT_COMMS_AGENT"
|
|
2948
|
+
];
|
|
2949
|
+
function agentMarker(env) {
|
|
2950
|
+
for (const name of AGENT_MARKERS) if (env[name] !== void 0 && env[name] !== "") return name;
|
|
2951
|
+
return null;
|
|
2952
|
+
}
|
|
2953
|
+
function paint(color, format, text) {
|
|
2954
|
+
return color ? styleText(format, text, { validateStream: false }) : text;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* A command line for a person to copy and run, each word quoted only where the shell it is pasted into would need it.
|
|
2958
|
+
* Every command this package prints to be run — a change to run again with its approval, a folder to take out, an
|
|
2959
|
+
* entry to register again or remove — is quoted here, so no printer quotes for a shell of its own.
|
|
2960
|
+
*
|
|
2961
|
+
* Everywhere but Windows that shell is a POSIX one, and a word goes in single quotes, inside which nothing is special
|
|
2962
|
+
* but the quote itself. Windows has two shells, and neither reads single quotes that way: cmd.exe does not take them
|
|
2963
|
+
* as quotes at all, so `'C:\Profiles\First Last\outgoing'` reached the command as two words, quote marks and all, and
|
|
2964
|
+
* PowerShell does, but escapes a quote inside them by doubling it rather than as `'\''`. A command printed on Windows
|
|
2965
|
+
* has to be safe in both, because nothing says which one it will be pasted into (CUE-306) — and safe is not enough:
|
|
2966
|
+
* the program has to receive the same words from either. Three readers stand between the line and the program. cmd.exe
|
|
2967
|
+
* hands the program the line as it is, and the program's own parser (the C runtime's, Node's) splits it. PowerShell
|
|
2968
|
+
* reads the line itself, a double-quoted word with its backslashes as plain characters, and then writes a new command
|
|
2969
|
+
* line for the program: Windows PowerShell 5.1 the old way ("Legacy" in about_Parsing, "Passing arguments to native
|
|
2970
|
+
* applications") — a word quoted only when it holds whitespace, as it is, and an empty word dropped — and PowerShell
|
|
2971
|
+
* 7.3 and later its own way ("Standard") — quoted when it must be, with every backslash before a quote doubled. For a
|
|
2972
|
+
* `.cmd` script, which is how npm installs `agentcomms`, `claude` and `codex` on Windows, PowerShell 7 goes back to the
|
|
2973
|
+
* old way, and cmd.exe then reads that new line. So on Windows:
|
|
2974
|
+
*
|
|
2975
|
+
* - A word of letters, digits and `_ + = : . / \`, with `@` and `-` anywhere but first, is left as it is: no reader
|
|
2976
|
+
* does anything with it, and a backslash is an ordinary character to all of them, at the end too. A first `@` is
|
|
2977
|
+
* splatting to PowerShell, and a `,` its array operator — two words — so a word with either is quoted. So is a word
|
|
2978
|
+
* that starts with a digit, which PowerShell may read as a number (`1kb`, `0x10`), and one that starts with `-` but
|
|
2979
|
+
* is not a plain option, which PowerShell may read as a parameter of its own and split (`-name.x`).
|
|
2980
|
+
* - A word that double quotes bring through all three readers whole goes in double quotes. Inside them cmd.exe reads
|
|
2981
|
+
* `& | < > ^ ( )` and spaces as ordinary characters, and PowerShell reads everything as ordinary but `$`, the
|
|
2982
|
+
* backtick and a double quote. What is left special in one or the other is kept out: a double quote, which ends the
|
|
2983
|
+
* quoting in both — and PowerShell takes the curly ones, `“ ” „`, for one too; `$` and the backtick, PowerShell's
|
|
2984
|
+
* expansion and escape; `%`, which cmd.exe expands as `%NAME%` before it looks at quotes at all; `!`, which it
|
|
2985
|
+
* expands as `!NAME!` inside quotes too wherever delayed expansion is on, and which then makes a `^` inside quotes an
|
|
2986
|
+
* escape; and any control or formatting character — a line break ends the command in cmd.exe even inside quotes, a
|
|
2987
|
+
* tab pasted into cmd.exe can complete a file name, and a right-to-left override shows a line other than the one
|
|
2988
|
+
* that runs. Three more cannot come through: an empty word, which Windows PowerShell drops; a word ending in a
|
|
2989
|
+
* backslash, which the C runtime reads with the closing quote as `\"` — and doubled for it, PowerShell 7 passes both
|
|
2990
|
+
* backslashes on where cmd.exe and Windows PowerShell pass one; and a word with `& | < > ^ ( )` but no whitespace,
|
|
2991
|
+
* which PowerShell passes to a `.cmd` script unquoted, for cmd.exe to run `&whoami` from or to split at `|`.
|
|
2992
|
+
* - With any other word in it, no line is printed at all (`line` is null), and the printers show the command's words
|
|
2993
|
+
* as JSON instead, saying it has to be typed. Not a line with the word left out: a placeholder in its place was
|
|
2994
|
+
* still a command that ran — `claude mcp remove NAME` removed whatever entry was called `NAME`, and an install
|
|
2995
|
+
* hint's `--force` replaced it — and before that, a word quoted for PowerShell's single quotes, which cmd.exe reads
|
|
2996
|
+
* as characters, ran `whoami` from a server named `$x&whoami&`. See `commandAsJson` for why the JSON runs nothing.
|
|
2997
|
+
*
|
|
2998
|
+
* Everywhere else there is always a line: single quotes make any word safe.
|
|
2999
|
+
*/
|
|
3000
|
+
function shellCommand(words, platform = process.platform) {
|
|
3001
|
+
if (platform !== "win32") return {
|
|
3002
|
+
words,
|
|
3003
|
+
line: words.map(posixShellWord).join(" "),
|
|
3004
|
+
platform
|
|
3005
|
+
};
|
|
3006
|
+
const printed = words.map(windowsShellWord);
|
|
3007
|
+
return {
|
|
3008
|
+
words,
|
|
3009
|
+
line: printed.includes(null) ? null : printed.join(" "),
|
|
3010
|
+
platform
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
/** The same command with more words at its end — the approval it is to be run again with — for the same shell. */
|
|
3014
|
+
function withWords(command, ...more) {
|
|
3015
|
+
return shellCommand([...command.words, ...more], command.platform);
|
|
3016
|
+
}
|
|
3017
|
+
function posixShellWord(word) {
|
|
3018
|
+
return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
3019
|
+
}
|
|
3020
|
+
/**
|
|
3021
|
+
* The word as cmd.exe, Windows PowerShell and PowerShell 7 all hand it to a program, or null when no printing of it
|
|
3022
|
+
* does (see `shellCommand`). Bare: a plain option, or ordinary characters that do not start like a number.
|
|
3023
|
+
*/
|
|
3024
|
+
function windowsShellWord(word) {
|
|
3025
|
+
if (/^(?:--?[A-Za-z][A-Za-z0-9-]*|(?!\+?\.?\d)[\w+=:./\\][\w@+=:./\\-]*)$/.test(word)) return word;
|
|
3026
|
+
if (word === "" || word.endsWith("\\")) return null;
|
|
3027
|
+
if (/["$`%!\u201C-\u201E]|[\p{C}\p{Zl}\p{Zp}]/u.test(word)) return null;
|
|
3028
|
+
if (/[&|<>^()]/.test(word) && !/\s/.test(word)) return null;
|
|
3029
|
+
return `"${word}"`;
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* The words of a command as a JSON array, for a command that cannot be printed as a line: something to read, and to
|
|
3033
|
+
* parse, and nothing to run.
|
|
3034
|
+
*
|
|
3035
|
+
* Pasted into either Windows shell by mistake, it runs nothing. PowerShell refuses it before running anything: a `[`
|
|
3036
|
+
* opens a type name, and a quoted string is none. cmd.exe looks for a program called `["agentcomms"` or the like, and
|
|
3037
|
+
* finds none — and nothing in the line is anything else to it: every character it acts on, inside quotes or out (`%`,
|
|
3038
|
+
* `!`), is written as a `\u` escape, and so is every double quote inside a word, so the quotes cmd.exe sees are the
|
|
3039
|
+
* JSON's own, in pairs, and `&`, `|`, `<`, `>`, `^` and the parentheses are only ever inside them, where it reads them
|
|
3040
|
+
* as characters. `$` and the backtick are escaped too, so that PowerShell would expand nothing even if it read on, and
|
|
3041
|
+
* so is everything outside printable ASCII — a line break, a curly quote, a right-to-left override. A backslash is
|
|
3042
|
+
* doubled, as JSON has it.
|
|
3043
|
+
*/
|
|
3044
|
+
function commandAsJson(words) {
|
|
3045
|
+
const word = (text) => {
|
|
3046
|
+
let json = "";
|
|
3047
|
+
for (let index = 0; index < text.length; index++) {
|
|
3048
|
+
const code = text.charCodeAt(index);
|
|
3049
|
+
const char = text[index];
|
|
3050
|
+
if (char === "\\") json += "\\\\";
|
|
3051
|
+
else if (code >= 32 && code <= 126 && !"\"%!$`".includes(char)) json += char;
|
|
3052
|
+
else json += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
3053
|
+
}
|
|
3054
|
+
return `"${json}"`;
|
|
3055
|
+
};
|
|
3056
|
+
return `[${words.map(word).join(",")}]`;
|
|
3057
|
+
}
|
|
3058
|
+
/** What is said beside a command shown as JSON. */
|
|
3059
|
+
const TO_TYPE = "the command's words, written as JSON: one of them cannot be quoted the same way for cmd.exe and for PowerShell, so type the command yourself, with that word quoted for the shell you use";
|
|
3060
|
+
/** A command in backticks, for a sentence — as its words in JSON, saying it has to be typed, when it has no line. */
|
|
3061
|
+
function inlineCommand(command) {
|
|
3062
|
+
return command.line === null ? `\`${commandAsJson(command.words)}\` (${TO_TYPE})` : `\`${command.line}\``;
|
|
3063
|
+
}
|
|
3064
|
+
/** A command as text of its own — a list's line, a field's value — or its words in JSON, saying it has to be typed. */
|
|
3065
|
+
function commandText(command) {
|
|
3066
|
+
return command.line === null ? `${commandAsJson(command.words)} (${TO_TYPE})` : command.line;
|
|
3067
|
+
}
|
|
3068
|
+
/** Writes a successful result: the envelope with --json, otherwise the human rendering. */
|
|
3069
|
+
function writeResult(data, options, human, streams = defaultStreams) {
|
|
3070
|
+
if (options.json) {
|
|
3071
|
+
streams.stdout.write(`${JSON.stringify(okEnvelope(data))}\n`);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const text = human(data);
|
|
3075
|
+
if (text) streams.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
3076
|
+
}
|
|
3077
|
+
/** Writes an error and returns the exit code to use. */
|
|
3078
|
+
function writeError(error, options, streams = defaultStreams) {
|
|
3079
|
+
const commsError = toCommsError(error);
|
|
3080
|
+
if (options.json) streams.stdout.write(`${JSON.stringify(errorEnvelope(commsError))}\n`);
|
|
3081
|
+
else {
|
|
3082
|
+
streams.stderr.write(`${paint(options.color, "red", "error")}: ${commsError.message}\n`);
|
|
3083
|
+
if (commsError.hint) streams.stderr.write(`${paint(options.color, "dim", "hint")}: ${commsError.hint}\n`);
|
|
3084
|
+
}
|
|
3085
|
+
return commsError.exitCode;
|
|
3086
|
+
}
|
|
3087
|
+
/** Runs a command body and exits with the documented code. Unexpected errors keep only their message. */
|
|
3088
|
+
async function runCommand(options, body, streams = defaultStreams) {
|
|
3089
|
+
try {
|
|
3090
|
+
await body();
|
|
3091
|
+
return EXIT_CODES.OK;
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
return writeError(error, options, streams);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* The first two steps of `requirePerson` — an agent is refused, then anything without a terminal — for a command
|
|
3098
|
+
* whose code is not its own to make up.
|
|
3099
|
+
*
|
|
3100
|
+
* `agentcomms approve` asks for a code the approval store issued and will check, so it cannot use `askChallenge`;
|
|
3101
|
+
* but it refuses in exactly the same order and words, because it is the same gate.
|
|
3102
|
+
*/
|
|
3103
|
+
function refuseUnlessPerson(env, streams, gate) {
|
|
3104
|
+
const marker = agentMarker(env);
|
|
3105
|
+
if (marker) throw new CommsError("LOOSENING_REFUSED", gate.refusedToAgent, {
|
|
3106
|
+
hint: `Ask the user to run \`${gate.command}\` in their own terminal.`,
|
|
3107
|
+
details: { marker }
|
|
3108
|
+
});
|
|
3109
|
+
const prompting = {};
|
|
3110
|
+
if (gate.json !== void 0) prompting.json = gate.json;
|
|
3111
|
+
if (gate.noInput !== void 0) prompting.noInput = gate.noInput;
|
|
3112
|
+
if (!canPrompt(env, streams, prompting)) throw new CommsError("LOOSENING_REFUSED", gate.refusedWithoutTerminal, { hint: `Run \`${gate.command}\` directly in a terminal.` });
|
|
3113
|
+
}
|
|
2853
3114
|
/**
|
|
2854
3115
|
* Enough of TOML to read codex's `config.toml`, and a refusal for anything else.
|
|
2855
3116
|
*
|
|
@@ -3923,13 +4184,13 @@ function snippetFor(client, name, entry) {
|
|
|
3923
4184
|
* Project-scoped entries are not looked at: every write here targets user scope, so they are not what would be
|
|
3924
4185
|
* replaced.
|
|
3925
4186
|
*/
|
|
3926
|
-
function claimName(product, options, name, existing) {
|
|
4187
|
+
function claimName(product, options, name, existing, platform) {
|
|
3927
4188
|
const taken = existing.filter((server) => server.client === options.client && server.name === name && server.scope !== "project");
|
|
3928
4189
|
const foreign = taken.find((server) => !isProductServer(server, product));
|
|
3929
4190
|
if (foreign) {
|
|
3930
4191
|
const what = foreign.packageName ?? (foreign.url ? displayUrl(foreign.url) : void 0) ?? (foreign.command || "something else");
|
|
3931
4192
|
const other = name === product.binary ? `${product.binary}-${product.defaultServerName}` : product.binary;
|
|
3932
|
-
throw new CommsError("CONFIG", `${options.client} already has an MCP server called "${name}", and it is not this one (it runs ${what})`, { hint: `Register this one under another name:
|
|
4193
|
+
throw new CommsError("CONFIG", `${options.client} already has an MCP server called "${name}", and it is not this one (it runs ${what})`, { hint: `Register this one under another name: ${inlineCommand(installCommand$1(product, options, other, [], platform))}. --force does not replace a server this did not install.` });
|
|
3933
4194
|
}
|
|
3934
4195
|
if (taken.length > 0 && !options.force) {
|
|
3935
4196
|
for (const pin of PIN_OPTIONS) {
|
|
@@ -3937,14 +4198,14 @@ function claimName(product, options, name, existing) {
|
|
|
3937
4198
|
const served = taken.map((server) => product.narrowingOf(server.args)[pin]).find(Boolean);
|
|
3938
4199
|
if (!wanted || !served || served === wanted) continue;
|
|
3939
4200
|
const second = `${product.defaultServerName}-${wanted.split("/")[0]}`;
|
|
3940
|
-
throw new CommsError("CONFIG", `${options.client} already has this server registered as "${name}"`, { hint: `That entry serves ${served}; to serve ${wanted} as well, register a second entry under its own name:
|
|
4201
|
+
throw new CommsError("CONFIG", `${options.client} already has this server registered as "${name}"`, { hint: `That entry serves ${served}; to serve ${wanted} as well, register a second entry under its own name: ${inlineCommand(installCommand$1(product, options, second === name ? `${name}-2` : second, [], platform))}.` });
|
|
3941
4202
|
}
|
|
3942
4203
|
const npx = taken.some((server) => server.args.some((argument) => argument.startsWith(`${product.npxPackage}@`)));
|
|
3943
4204
|
const again = {
|
|
3944
4205
|
...keepNarrowing(product, options, taken).options,
|
|
3945
4206
|
launcher: options.launcher ?? (npx ? "npx" : void 0)
|
|
3946
4207
|
};
|
|
3947
|
-
throw new CommsError("CONFIG", `${options.client} already has this server registered as "${name}"`, { hint: `Pass --force to replace it — that is how an upgrade reaches a client:
|
|
4208
|
+
throw new CommsError("CONFIG", `${options.client} already has this server registered as "${name}"`, { hint: `Pass --force to replace it — that is how an upgrade reaches a client: ${inlineCommand(installCommand$1(product, again, name, ["--force"], platform))}.` });
|
|
3948
4209
|
}
|
|
3949
4210
|
return taken;
|
|
3950
4211
|
}
|
|
@@ -3987,8 +4248,11 @@ function keepNarrowing(product, options, previous) {
|
|
|
3987
4248
|
* workspace on the machine; for Gmail it dropped `--inbox` and `--read-only` the same way. Doctor's repair already
|
|
3988
4249
|
* rebuilds these flags for exactly that reason. The product's `serverArgs` are the install command's own flags for
|
|
3989
4250
|
* the pin and the narrowing, so they are repeated as they are.
|
|
4251
|
+
*
|
|
4252
|
+
* A pin kept from the entry being replaced was read from the client's file, which may hold anything, so on Windows the
|
|
4253
|
+
* command can come back with no line to paste; every hint gives it with `inlineCommand`, which shows it as words then.
|
|
3990
4254
|
*/
|
|
3991
|
-
function installCommand$1(product, options, name, extra
|
|
4255
|
+
function installCommand$1(product, options, name, extra, platform) {
|
|
3992
4256
|
const words = [
|
|
3993
4257
|
product.binary,
|
|
3994
4258
|
"mcp",
|
|
@@ -4000,7 +4264,7 @@ function installCommand$1(product, options, name, extra = []) {
|
|
|
4000
4264
|
words.push(...product.serverArgs(options));
|
|
4001
4265
|
if (options.launcher && options.launcher !== "managed") words.push("--launcher", options.launcher);
|
|
4002
4266
|
words.push(...extra);
|
|
4003
|
-
return words
|
|
4267
|
+
return shellCommand(words, platform);
|
|
4004
4268
|
}
|
|
4005
4269
|
/**
|
|
4006
4270
|
* A command's exit status and what it printed, kept here and never shown: codex prints an entry's env. Started as
|
|
@@ -4043,8 +4307,13 @@ function capture$1(env, command, args) {
|
|
|
4043
4307
|
* or a failure that is not "no such server", stops the install: not knowing is not the same as nothing there.
|
|
4044
4308
|
* Nothing codex prints is repeated, because its answer carries the entry's env.
|
|
4045
4309
|
*/
|
|
4046
|
-
async function codexRegistration(env, binary, name, path) {
|
|
4047
|
-
const unknown = () => new CommsError("CONFIG", `codex would not say what it has registered as "${name}", so nothing was written`, { hint: `Look with
|
|
4310
|
+
async function codexRegistration(env, binary, name, path, platform) {
|
|
4311
|
+
const unknown = () => new CommsError("CONFIG", `codex would not say what it has registered as "${name}", so nothing was written`, { hint: `Look with ${inlineCommand(shellCommand([
|
|
4312
|
+
"codex",
|
|
4313
|
+
"mcp",
|
|
4314
|
+
"get",
|
|
4315
|
+
name
|
|
4316
|
+
], platform))}. If it is not an older copy of this server, choose another --name; \`--print\` shows the entry to add by hand.` });
|
|
4048
4317
|
let answer;
|
|
4049
4318
|
try {
|
|
4050
4319
|
answer = await capture$1(env, binary, [
|
|
@@ -4147,10 +4416,10 @@ async function preflightInstall(context, product, options) {
|
|
|
4147
4416
|
const blind = scan.unreadable.find((file) => file.path === own);
|
|
4148
4417
|
if (blind) throw new CommsError("CONFIG", `${blind.path} could not be read (${blind.reason}), so what is already registered there cannot be checked; nothing was written`, { hint: "Fix the file, or add the entry by hand: `--print` shows it without writing anything." });
|
|
4149
4418
|
}
|
|
4150
|
-
let previous = writes ? claimName(product, options, name, existing) : [];
|
|
4419
|
+
let previous = writes ? claimName(product, options, name, existing, context.platform) : [];
|
|
4151
4420
|
if (writes && binary && options.client === "codex") {
|
|
4152
|
-
const reported = await codexRegistration(context.env, binary, name, configPath ?? "codex");
|
|
4153
|
-
if (reported) previous = claimName(product, options, name, [reported]);
|
|
4421
|
+
const reported = await codexRegistration(context.env, binary, name, configPath ?? "codex", context.platform);
|
|
4422
|
+
if (reported) previous = claimName(product, options, name, [reported], context.platform);
|
|
4154
4423
|
}
|
|
4155
4424
|
const { options: effective, kept } = keepNarrowing(product, options, previous);
|
|
4156
4425
|
const node = await resolveNode(context.env);
|
|
@@ -4277,7 +4546,19 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4277
4546
|
const { cliName, binary, own, configPath, writes } = target;
|
|
4278
4547
|
const warnings = (product.warnAbout?.(existing.filter((server) => server.client === options.client)) ?? []).filter(Boolean);
|
|
4279
4548
|
if (kept.length > 0) {
|
|
4280
|
-
const removal = options.client === "claude-code" ?
|
|
4549
|
+
const removal = options.client === "claude-code" ? inlineCommand(shellCommand([
|
|
4550
|
+
"claude",
|
|
4551
|
+
"mcp",
|
|
4552
|
+
"remove",
|
|
4553
|
+
name,
|
|
4554
|
+
"--scope",
|
|
4555
|
+
"user"
|
|
4556
|
+
], context.platform)) : options.client === "codex" ? inlineCommand(shellCommand([
|
|
4557
|
+
"codex",
|
|
4558
|
+
"mcp",
|
|
4559
|
+
"remove",
|
|
4560
|
+
name
|
|
4561
|
+
], context.platform)) : `delete "${name}" from ${configPath}`;
|
|
4281
4562
|
warnings.push(`Kept ${kept.join(" ")} from the "${name}" entry this replaced, because this install did not say otherwise. To register it wider on purpose, remove that entry first (${removal}), then install without them.`);
|
|
4282
4563
|
}
|
|
4283
4564
|
if (!writes) {
|
|
@@ -4372,7 +4653,7 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4372
4653
|
restored = false;
|
|
4373
4654
|
}
|
|
4374
4655
|
throw new CommsError("CONFIG", restored ? `could not register "${name}"; the previous entry was put back` : `could not register "${name}", and the previous entry could not be put back either — ${cliName} now has no server called "${name}"`, {
|
|
4375
|
-
hint: restored ? "Check the client is not running, then try again." : `Re-register it with
|
|
4656
|
+
hint: restored ? "Check the client is not running, then try again." : `Re-register it with ${inlineCommand(installCommand$1(product, effective, name, [], context.platform))}. The old entry is in ${backupPath}.`,
|
|
4376
4657
|
cause: error
|
|
4377
4658
|
});
|
|
4378
4659
|
}
|
|
@@ -4381,7 +4662,17 @@ async function mcpInstall(context, product, options, planned) {
|
|
|
4381
4662
|
} catch (error) {
|
|
4382
4663
|
const message = error instanceof CommsError ? error.message : String(error);
|
|
4383
4664
|
if (/already exists/i.test(message)) throw new CommsError("CONFIG", `${cliName} already has an MCP server called "${name}", somewhere this could not read`, {
|
|
4384
|
-
hint: `Look at it with
|
|
4665
|
+
hint: `Look at it with ${inlineCommand(shellCommand([
|
|
4666
|
+
cliName,
|
|
4667
|
+
"mcp",
|
|
4668
|
+
"get",
|
|
4669
|
+
name
|
|
4670
|
+
], context.platform))}. If it is an older ${product.binary}, remove it with ${inlineCommand(shellCommand([
|
|
4671
|
+
cliName,
|
|
4672
|
+
"mcp",
|
|
4673
|
+
"remove",
|
|
4674
|
+
name
|
|
4675
|
+
], context.platform))} and run this again; if not, choose another --name.`,
|
|
4385
4676
|
cause: error
|
|
4386
4677
|
});
|
|
4387
4678
|
throw error;
|
|
@@ -4717,19 +5008,32 @@ function rivalPatterns(packages) {
|
|
|
4717
5008
|
function escapeRegExp(text) {
|
|
4718
5009
|
return text.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
|
|
4719
5010
|
}
|
|
4720
|
-
|
|
5011
|
+
/**
|
|
5012
|
+
* The command that removes a registered server, in its client's own terms — the name as the client's file has it,
|
|
5013
|
+
* which may be anything, so on Windows it can have no line to paste, and is shown as its words, to be typed
|
|
5014
|
+
* (`shellCommand`). Never as a line with the name left out: that removed whatever entry had the stand-in's name.
|
|
5015
|
+
*/
|
|
5016
|
+
function removalCommand(client, name, platform) {
|
|
5017
|
+
return commandText(shellCommand([
|
|
5018
|
+
client,
|
|
5019
|
+
"mcp",
|
|
5020
|
+
"remove",
|
|
5021
|
+
name
|
|
5022
|
+
], platform));
|
|
5023
|
+
}
|
|
5024
|
+
function gmailRemoval({ client, name: server, path, scope }, platform) {
|
|
4721
5025
|
if (scope === "project") return `remove "${server}" from the project entry in ${path} by hand`;
|
|
4722
5026
|
switch (client) {
|
|
4723
|
-
case "claude-code": return
|
|
4724
|
-
case "codex": return
|
|
5027
|
+
case "claude-code": return removalCommand("claude", server, platform);
|
|
5028
|
+
case "codex": return removalCommand("codex", server, platform);
|
|
4725
5029
|
default: return `remove the "${server}" entry from ${path}, then restart ${client}`;
|
|
4726
5030
|
}
|
|
4727
5031
|
}
|
|
4728
5032
|
/**
|
|
4729
5033
|
* Registered servers that are one of `packages` — another server for the same service whose send tools no approval
|
|
4730
|
-
* step gates — each with why it matters and how to remove it.
|
|
5034
|
+
* step gates — each with why it matters and how to remove it. `platform` is the shell the removal is quoted for.
|
|
4731
5035
|
*/
|
|
4732
|
-
function findRivalPackageServers(servers, packages) {
|
|
5036
|
+
function findRivalPackageServers(servers, packages, platform = process.platform) {
|
|
4733
5037
|
const patterns = rivalPatterns(packages);
|
|
4734
5038
|
const findings = [];
|
|
4735
5039
|
for (const server of servers) {
|
|
@@ -4740,7 +5044,7 @@ function findRivalPackageServers(servers, packages) {
|
|
|
4740
5044
|
...server,
|
|
4741
5045
|
packageName: known.name,
|
|
4742
5046
|
reason: `${known.name} exposes send tools that no approval step gates`,
|
|
4743
|
-
removal: gmailRemoval(server)
|
|
5047
|
+
removal: gmailRemoval(server, platform)
|
|
4744
5048
|
});
|
|
4745
5049
|
}
|
|
4746
5050
|
return findings;
|
|
@@ -4866,107 +5170,6 @@ function requireChannelManifest(channel) {
|
|
|
4866
5170
|
if (manifest === void 0) throw notAChannel(channel);
|
|
4867
5171
|
return manifest;
|
|
4868
5172
|
}
|
|
4869
|
-
function okEnvelope(data) {
|
|
4870
|
-
return {
|
|
4871
|
-
ok: true,
|
|
4872
|
-
schemaVersion: 1,
|
|
4873
|
-
data
|
|
4874
|
-
};
|
|
4875
|
-
}
|
|
4876
|
-
function errorEnvelope(error) {
|
|
4877
|
-
const body = {
|
|
4878
|
-
code: error.code,
|
|
4879
|
-
message: error.message
|
|
4880
|
-
};
|
|
4881
|
-
if (error.hint !== void 0) body.hint = error.hint;
|
|
4882
|
-
if (error.details !== void 0) body.details = error.details;
|
|
4883
|
-
return {
|
|
4884
|
-
ok: false,
|
|
4885
|
-
schemaVersion: 1,
|
|
4886
|
-
error: body
|
|
4887
|
-
};
|
|
4888
|
-
}
|
|
4889
|
-
const defaultStreams = {
|
|
4890
|
-
stdout: process.stdout,
|
|
4891
|
-
stderr: process.stderr,
|
|
4892
|
-
stdin: process.stdin
|
|
4893
|
-
};
|
|
4894
|
-
function colorEnabled(env, stream, flag) {
|
|
4895
|
-
if (flag === false) return false;
|
|
4896
|
-
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
4897
|
-
if (env.TERM === "dumb") return false;
|
|
4898
|
-
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
|
|
4899
|
-
return Boolean(stream.isTTY);
|
|
4900
|
-
}
|
|
4901
|
-
/** True when a human could answer a prompt: both ends are terminals, no --json/--no-input, not CI. */
|
|
4902
|
-
function canPrompt(env, streams, options) {
|
|
4903
|
-
if (options.json || options.noInput) return false;
|
|
4904
|
-
if (env.CI && env.CI !== "0" && env.CI !== "false") return false;
|
|
4905
|
-
return Boolean(streams.stdin?.isTTY && streams.stdout.isTTY);
|
|
4906
|
-
}
|
|
4907
|
-
/** Environment variables well-known coding agents set. Used only as a speed bump, never as a security boundary. */
|
|
4908
|
-
const AGENT_MARKERS = [
|
|
4909
|
-
"CLAUDECODE",
|
|
4910
|
-
"CLAUDE_CODE_ENTRYPOINT",
|
|
4911
|
-
"CODEX_SANDBOX",
|
|
4912
|
-
"CODEX_HOME",
|
|
4913
|
-
"CURSOR_AGENT",
|
|
4914
|
-
"GEMINI_CLI",
|
|
4915
|
-
"AGENT_COMMS_AGENT"
|
|
4916
|
-
];
|
|
4917
|
-
function agentMarker(env) {
|
|
4918
|
-
for (const name of AGENT_MARKERS) if (env[name] !== void 0 && env[name] !== "") return name;
|
|
4919
|
-
return null;
|
|
4920
|
-
}
|
|
4921
|
-
function paint(color, format, text) {
|
|
4922
|
-
return color ? styleText(format, text, { validateStream: false }) : text;
|
|
4923
|
-
}
|
|
4924
|
-
/** Writes a successful result: the envelope with --json, otherwise the human rendering. */
|
|
4925
|
-
function writeResult(data, options, human, streams = defaultStreams) {
|
|
4926
|
-
if (options.json) {
|
|
4927
|
-
streams.stdout.write(`${JSON.stringify(okEnvelope(data))}\n`);
|
|
4928
|
-
return;
|
|
4929
|
-
}
|
|
4930
|
-
const text = human(data);
|
|
4931
|
-
if (text) streams.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
4932
|
-
}
|
|
4933
|
-
/** Writes an error and returns the exit code to use. */
|
|
4934
|
-
function writeError(error, options, streams = defaultStreams) {
|
|
4935
|
-
const commsError = toCommsError(error);
|
|
4936
|
-
if (options.json) streams.stdout.write(`${JSON.stringify(errorEnvelope(commsError))}\n`);
|
|
4937
|
-
else {
|
|
4938
|
-
streams.stderr.write(`${paint(options.color, "red", "error")}: ${commsError.message}\n`);
|
|
4939
|
-
if (commsError.hint) streams.stderr.write(`${paint(options.color, "dim", "hint")}: ${commsError.hint}\n`);
|
|
4940
|
-
}
|
|
4941
|
-
return commsError.exitCode;
|
|
4942
|
-
}
|
|
4943
|
-
/** Runs a command body and exits with the documented code. Unexpected errors keep only their message. */
|
|
4944
|
-
async function runCommand(options, body, streams = defaultStreams) {
|
|
4945
|
-
try {
|
|
4946
|
-
await body();
|
|
4947
|
-
return EXIT_CODES.OK;
|
|
4948
|
-
} catch (error) {
|
|
4949
|
-
return writeError(error, options, streams);
|
|
4950
|
-
}
|
|
4951
|
-
}
|
|
4952
|
-
/**
|
|
4953
|
-
* The first two steps of `requirePerson` — an agent is refused, then anything without a terminal — for a command
|
|
4954
|
-
* whose code is not its own to make up.
|
|
4955
|
-
*
|
|
4956
|
-
* `agentcomms approve` asks for a code the approval store issued and will check, so it cannot use `askChallenge`;
|
|
4957
|
-
* but it refuses in exactly the same order and words, because it is the same gate.
|
|
4958
|
-
*/
|
|
4959
|
-
function refuseUnlessPerson(env, streams, gate) {
|
|
4960
|
-
const marker = agentMarker(env);
|
|
4961
|
-
if (marker) throw new CommsError("LOOSENING_REFUSED", gate.refusedToAgent, {
|
|
4962
|
-
hint: `Ask the user to run \`${gate.command}\` in their own terminal.`,
|
|
4963
|
-
details: { marker }
|
|
4964
|
-
});
|
|
4965
|
-
const prompting = {};
|
|
4966
|
-
if (gate.json !== void 0) prompting.json = gate.json;
|
|
4967
|
-
if (gate.noInput !== void 0) prompting.noInput = gate.noInput;
|
|
4968
|
-
if (!canPrompt(env, streams, prompting)) throw new CommsError("LOOSENING_REFUSED", gate.refusedWithoutTerminal, { hint: `Run \`${gate.command}\` directly in a terminal.` });
|
|
4969
|
-
}
|
|
4970
5173
|
/**
|
|
4971
5174
|
* One renderer for every surface a send preview is shown on — the chat, an elicitation form, a terminal. Text the
|
|
4972
5175
|
* draft's author controls (body, subject, display names, file names, link text) must not be able to change how the
|
|
@@ -5420,21 +5623,25 @@ function renderChangePreview(record) {
|
|
|
5420
5623
|
* it on the second call, with its id.
|
|
5421
5624
|
*
|
|
5422
5625
|
* A change that loosens no setting and does nothing irreversible is applied directly: asking a person to agree to
|
|
5423
|
-
* something that needs no agreement teaches them to agree without reading. It refuses an approval id
|
|
5424
|
-
*
|
|
5425
|
-
* call claiming an approval through (design 2026-09-28 §2),
|
|
5426
|
-
* approval the store holds — one an agent had just had prepared for
|
|
5427
|
-
* or a cancellation made where the call itself was stopped.
|
|
5626
|
+
* something that needs no agreement teaches them to agree without reading. It refuses an approval id — in the change's
|
|
5627
|
+
* own words when it has them (`refuseApproval`) — as a report refuses one (`refuseApprovalWithoutChange`), rather than
|
|
5628
|
+
* applying and dropping it: the update check's stop lets a call claiming an approval through (design 2026-09-28 §2),
|
|
5629
|
+
* and one that is then never claimed would carry any approval the store holds — one an agent had just had prepared for
|
|
5630
|
+
* "not now", say — past the stop, to a tightening or a cancellation made where the call itself was stopped.
|
|
5428
5631
|
*/
|
|
5429
5632
|
async function gatedChange(core, change, options) {
|
|
5430
5633
|
const request = await change.plan(await core.config.load());
|
|
5431
5634
|
const loosens = classifyChange(request.before, request.after).loosened.length > 0;
|
|
5432
5635
|
const acts = (request.effects ?? []).length > 0;
|
|
5433
5636
|
if (!loosens && !acts) {
|
|
5434
|
-
if (options.approvalId)
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5637
|
+
if (options.approvalId) {
|
|
5638
|
+
const refusal = await change.refuseApproval?.(options.approvalId);
|
|
5639
|
+
if (refusal !== void 0) throw refusal;
|
|
5640
|
+
throw new CommsError("USAGE", "nothing was changed: as things stand this change needs no approval, so it takes none", {
|
|
5641
|
+
hint: "Make the same call again without the approval: it applies at once and asks nobody. An approval is spent only on the change it was prepared for.",
|
|
5642
|
+
details: { approvalId: options.approvalId }
|
|
5643
|
+
});
|
|
5644
|
+
}
|
|
5438
5645
|
return {
|
|
5439
5646
|
status: "applied",
|
|
5440
5647
|
result: await change.apply(void 0, request)
|
|
@@ -5476,8 +5683,10 @@ async function gatedChangeAtTerminal(core, change, options) {
|
|
|
5476
5683
|
const { prepared } = first;
|
|
5477
5684
|
if (!(agentMarker(options.env) === null && canPrompt(options.env, streams, { json: options.output.json === true }))) {
|
|
5478
5685
|
if (options.output.json !== true) streams.stdout.write(`${prepared.preview}\n\n`);
|
|
5686
|
+
const carrying = [options.approvalFlag ?? "--approval", prepared.approvalId];
|
|
5687
|
+
const { command } = options;
|
|
5479
5688
|
throw new CommsError("APPROVAL_PENDING", `this change needs approval first: ${prepared.summary}`, {
|
|
5480
|
-
hint: approvalHint(prepared, `${
|
|
5689
|
+
hint: approvalHint(prepared, typeof command === "string" ? `${command} ${carrying.join(" ")}` : withWords(command, ...carrying), approveCommand),
|
|
5481
5690
|
details: {
|
|
5482
5691
|
approvalId: prepared.approvalId,
|
|
5483
5692
|
policy: prepared.policy,
|
|
@@ -5511,7 +5720,8 @@ async function gatedChangeAtTerminal(core, change, options) {
|
|
|
5511
5720
|
* its registration step and says why in its report — so the two say it in the same words.
|
|
5512
5721
|
*/
|
|
5513
5722
|
function approvalHint(prepared, rerun, approveCommand) {
|
|
5514
|
-
|
|
5723
|
+
const run = typeof rerun === "string" ? `\`${rerun}\`` : inlineCommand(rerun);
|
|
5724
|
+
return prepared.policy === "confirm" ? `Show the person the preview. They run \`${approveCommandOf({ approveCommand })} ${prepared.approvalId}\`; then run ${run}.` : `Show the person the preview. Once they say yes, run ${run}.`;
|
|
5515
5725
|
}
|
|
5516
5726
|
/**
|
|
5517
5727
|
* Approves a configuration change at a terminal, the way a person approves a send: read the preview, type the code.
|
|
@@ -6048,7 +6258,7 @@ function isPrerelease(version) {
|
|
|
6048
6258
|
return VERSION_PATTERN.exec(version)?.[4] !== void 0;
|
|
6049
6259
|
}
|
|
6050
6260
|
/** The package version, read from package.json at build time. */
|
|
6051
|
-
const VERSION = "0.12.
|
|
6261
|
+
const VERSION = "0.12.2";
|
|
6052
6262
|
/**
|
|
6053
6263
|
* Registering, pruning and listing the MCP servers of every channel, from the core.
|
|
6054
6264
|
*
|
|
@@ -6306,7 +6516,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
6306
6516
|
};
|
|
6307
6517
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
6308
6518
|
packageName: channelServer(request.channel).packageName,
|
|
6309
|
-
version: "0.12.
|
|
6519
|
+
version: "0.12.2"
|
|
6310
6520
|
};
|
|
6311
6521
|
const product = {
|
|
6312
6522
|
packageName,
|
|
@@ -6930,7 +7140,7 @@ async function askLine(streams, question) {
|
|
|
6930
7140
|
}
|
|
6931
7141
|
channelServer("core").packageName;
|
|
6932
7142
|
//#endregion
|
|
6933
|
-
//#region ../core/dist/tool-arguments-
|
|
7143
|
+
//#region ../core/dist/tool-arguments-BMmJnuY4.mjs
|
|
6934
7144
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
6935
7145
|
const TARGET = "draft-2020-12";
|
|
6936
7146
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcomms/whatsapp",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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.12.
|
|
41
|
+
"@agentcomms/core": "0.12.2"
|
|
42
42
|
},
|
|
43
43
|
"agentcomms": {
|
|
44
44
|
"contract": 1,
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"typecheck": "tsc -p tsconfig.json",
|
|
88
88
|
"test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test --test-timeout=600000 \"test/**/*.test.ts\""
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "224763228ecc63ce403f0d12988865cfaa981f2a"
|
|
91
91
|
}
|