wazap-mcp 0.9.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.
@@ -0,0 +1,222 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { delimiter, dirname, join, resolve } from "node:path";
5
+ import { defaultDataDir } from "./config.js";
6
+ import { WazapError } from "./errors.js";
7
+ import { say } from "./logger.js";
8
+ function claudeDesktopFile() {
9
+ if (process.platform === "darwin") {
10
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
11
+ }
12
+ if (process.platform === "win32") {
13
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
14
+ }
15
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
16
+ }
17
+ export const CLIENTS = [
18
+ {
19
+ name: "claude-code",
20
+ describe: "Claude Code",
21
+ file: () => null,
22
+ format: "command",
23
+ keyPath: [],
24
+ next: "Run `claude mcp list` to confirm.",
25
+ },
26
+ {
27
+ name: "claude-desktop",
28
+ describe: "Claude Desktop",
29
+ file: claudeDesktopFile,
30
+ format: "json",
31
+ keyPath: ["mcpServers", "whatsapp"],
32
+ next: "Restart Claude Desktop.",
33
+ },
34
+ {
35
+ name: "cursor",
36
+ describe: "Cursor",
37
+ file: () => join(homedir(), ".cursor", "mcp.json"),
38
+ format: "json",
39
+ keyPath: ["mcpServers", "whatsapp"],
40
+ next: "Reload the Cursor window.",
41
+ },
42
+ {
43
+ name: "codex",
44
+ describe: "Codex CLI",
45
+ file: () => join(homedir(), ".codex", "config.toml"),
46
+ format: "toml",
47
+ keyPath: ["mcp_servers", "whatsapp"],
48
+ next: "Restart Codex.",
49
+ },
50
+ {
51
+ name: "vscode",
52
+ describe: "VS Code",
53
+ file: () => join(process.cwd(), ".vscode", "mcp.json"),
54
+ format: "json",
55
+ keyPath: ["servers", "whatsapp"],
56
+ extra: { type: "stdio" },
57
+ next: "Written to ./.vscode/mcp.json for this workspace. Reload the VS Code window.",
58
+ },
59
+ {
60
+ name: "gemini",
61
+ describe: "Gemini CLI",
62
+ file: () => join(homedir(), ".gemini", "settings.json"),
63
+ format: "json",
64
+ keyPath: ["mcpServers", "whatsapp"],
65
+ next: "Restart the Gemini CLI.",
66
+ },
67
+ ];
68
+ export const CLIENT_NAMES = CLIENTS.map((client) => client.name).join(", ");
69
+ export const CONNECT_HINT = `Next: wazap connect claude-code (or ${CLIENTS.slice(1)
70
+ .map((client) => client.name)
71
+ .join(", ")})`;
72
+ export function isNpxPath(binPath) {
73
+ return /[\\/]_npx[\\/]/.test(binPath);
74
+ }
75
+ const PATH_NAMES = process.platform === "win32" ? ["wazap.cmd", "wazap"] : ["wazap"];
76
+ export function onPath(pathEnv, exists = existsSync) {
77
+ return pathEnv
78
+ .split(delimiter)
79
+ .filter(Boolean)
80
+ .some((dir) => PATH_NAMES.some((name) => exists(join(dir, name))));
81
+ }
82
+ /**
83
+ * How the client should launch wazap, from how it was launched now: through
84
+ * npx, as a global binary on PATH, or straight from a checkout that is on
85
+ * neither. A checkout written as `wazap` would point the client at a command
86
+ * that does not exist.
87
+ */
88
+ export function launcher(binPath, pathEnv, exists) {
89
+ if (isNpxPath(binPath))
90
+ return { command: "npx", args: ["-y", "wazap-mcp"] };
91
+ if (onPath(pathEnv, exists))
92
+ return { command: "wazap", args: [] };
93
+ return { command: "node", args: [resolve(binPath)] };
94
+ }
95
+ export function mcpEntry(config) {
96
+ const entry = launcher(process.argv[1] ?? "", process.env.PATH ?? "");
97
+ if (config.dataDir !== defaultDataDir())
98
+ entry.args.push("--data-dir", config.dataDir);
99
+ if (config.readOnly)
100
+ entry.args.push("--read-only");
101
+ return entry;
102
+ }
103
+ const WRITERS = {
104
+ command: runClientCommand,
105
+ json: writeJsonEntry,
106
+ toml: writeTomlEntry,
107
+ };
108
+ export function runConnect(config) {
109
+ const name = config.args[0] ?? "";
110
+ const spec = CLIENTS.find((client) => client.name === name);
111
+ if (!spec) {
112
+ throw new WazapError("INVALID_ID", `Unknown client "${name}".`, `Pick one of: ${CLIENT_NAMES}`);
113
+ }
114
+ WRITERS[spec.format](spec, mcpEntry(config), config.dryRun);
115
+ }
116
+ /** Null only when the file is absent; an unreadable file must never be overwritten. */
117
+ function readTextOrNull(file) {
118
+ try {
119
+ return readFileSync(file, "utf8");
120
+ }
121
+ catch (err) {
122
+ if (err.code === "ENOENT")
123
+ return null;
124
+ throw new WazapError("INVALID_ID", `${file} cannot be read.`, "Fix its permissions or move the file aside, then run this again.");
125
+ }
126
+ }
127
+ function setIn(doc, keyPath, value) {
128
+ let node = doc;
129
+ for (const key of keyPath.slice(0, -1)) {
130
+ const child = node[key];
131
+ if (child === null || typeof child !== "object" || Array.isArray(child))
132
+ node[key] = {};
133
+ node = node[key];
134
+ }
135
+ node[keyPath[keyPath.length - 1]] = value;
136
+ }
137
+ function writeJsonEntry(spec, entry, dryRun) {
138
+ const file = spec.file();
139
+ const current = readTextOrNull(file);
140
+ let doc = {};
141
+ if (current !== null && current.trim() !== "") {
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(current);
145
+ }
146
+ catch {
147
+ throw new WazapError("INVALID_ID", `${file} is not valid JSON.`, "Fix the JSON or move the file aside, then run this again.");
148
+ }
149
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
150
+ throw new WazapError("INVALID_ID", `${file} is not a JSON object.`, "Fix the JSON or move the file aside, then run this again.");
151
+ }
152
+ doc = parsed;
153
+ }
154
+ const value = { ...spec.extra, command: entry.command, args: entry.args };
155
+ setIn(doc, spec.keyPath, value);
156
+ apply(spec, file, `${JSON.stringify(doc, null, 2)}\n`, current, dryRun, JSON.stringify(value, null, 2));
157
+ }
158
+ function writeTomlEntry(spec, entry, dryRun) {
159
+ const file = spec.file();
160
+ const current = readTextOrNull(file);
161
+ const header = `[${spec.keyPath.join(".")}]`;
162
+ const args = entry.args.map((arg) => JSON.stringify(arg)).join(", ");
163
+ const block = `${header}\ncommand = ${JSON.stringify(entry.command)}\nargs = [${args}]\n`;
164
+ apply(spec, file, spliceTomlTable(current ?? "", header, block), current, dryRun, block.trimEnd());
165
+ }
166
+ /** Replace the table under `header`, from its header line to the next one, or append it. */
167
+ function spliceTomlTable(text, header, block) {
168
+ const lines = text.split("\n");
169
+ const start = lines.findIndex((line) => line.trim() === header);
170
+ if (start === -1) {
171
+ const base = text.trimEnd();
172
+ return base === "" ? block : `${base}\n\n${block}`;
173
+ }
174
+ let end = start + 1;
175
+ while (end < lines.length && !lines[end].trimStart().startsWith("["))
176
+ end++;
177
+ // Blank lines and comments just above the next header introduce that table.
178
+ while (end > start + 1 && /^\s*(#|$)/.test(lines[end - 1]))
179
+ end--;
180
+ const merged = [...lines.slice(0, start), ...block.trimEnd().split("\n"), ...lines.slice(end)].join("\n");
181
+ return merged.endsWith("\n") ? merged : `${merged}\n`;
182
+ }
183
+ function apply(spec, file, next, current, dryRun, shown) {
184
+ if (dryRun) {
185
+ say(`${spec.describe}: would write ${file}`);
186
+ say(shown);
187
+ say(`Next: ${spec.next}`);
188
+ return;
189
+ }
190
+ if (next === current) {
191
+ say(`${spec.describe}: ${file} already has this entry.`);
192
+ say(`Next: ${spec.next}`);
193
+ return;
194
+ }
195
+ mkdirSync(dirname(file), { recursive: true });
196
+ if (current !== null && !existsSync(`${file}.bak`))
197
+ copyFileSync(file, `${file}.bak`);
198
+ writeFileSync(file, next);
199
+ say(`${spec.describe}: wrote ${file}`);
200
+ say(shown);
201
+ say(`Next: ${spec.next}`);
202
+ }
203
+ function runClientCommand(spec, entry, dryRun) {
204
+ const argv = ["mcp", "add", "whatsapp", "--", entry.command, ...entry.args];
205
+ const shown = `claude ${argv.join(" ")}`;
206
+ if (dryRun) {
207
+ say(`${spec.describe}: would run`);
208
+ say(shown);
209
+ say(`Next: ${spec.next}`);
210
+ return;
211
+ }
212
+ const result = spawnSync("claude", argv, { stdio: "inherit" });
213
+ if (result.error !== undefined) {
214
+ say("`claude` is not on PATH. Run this yourself where Claude Code is installed:");
215
+ say(shown);
216
+ process.exit(1);
217
+ }
218
+ if (result.status !== 0)
219
+ process.exit(result.status ?? 1);
220
+ say(`${spec.describe}: registered the whatsapp MCP server.`);
221
+ say(`Next: ${spec.next}`);
222
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,114 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { readLinkedAccount } from "./auth-state.js";
3
+ import { WAZAP_VERSION, paths } from "./config.js";
4
+ import { lockHolder, lockPid } from "./lock.js";
5
+ export const MARK = { ok: "✓", fail: "✗", info: "–" };
6
+ const UPDATE_TIMEOUT_MS = 2_000;
7
+ const MIN_NODE_MAJOR = 20;
8
+ const CHECKS = [checkNode, checkDataDir, checkLock, checkCredentials, checkWrites, checkUpdate];
9
+ export async function runChecks(config) {
10
+ const checks = [];
11
+ for (const check of CHECKS)
12
+ checks.push(await check(config));
13
+ return checks;
14
+ }
15
+ export function checkLine(check) {
16
+ return `${MARK[check.state]} ${check.name}: ${check.detail}${check.fix ? ` — ${check.fix}` : ""}`;
17
+ }
18
+ function checkNode() {
19
+ const version = process.versions.node;
20
+ const major = Number.parseInt(version.split(".")[0], 10);
21
+ return major >= MIN_NODE_MAJOR
22
+ ? { name: "node", state: "ok", detail: version }
23
+ : { name: "node", state: "fail", detail: `${version} is too old`, fix: `install Node ${MIN_NODE_MAJOR} or newer` };
24
+ }
25
+ function checkDataDir(config) {
26
+ const dir = config.dataDir;
27
+ let stat;
28
+ try {
29
+ stat = statSync(dir);
30
+ }
31
+ catch {
32
+ return { name: "data dir", state: "info", detail: `${dir} does not exist yet (login creates it)` };
33
+ }
34
+ if (!stat.isDirectory()) {
35
+ return { name: "data dir", state: "fail", detail: `${dir} is not a directory`, fix: "move it aside or use --data-dir" };
36
+ }
37
+ const mode = stat.mode & 0o777;
38
+ if (process.platform !== "win32" && mode !== 0o700) {
39
+ return {
40
+ name: "data dir",
41
+ state: "fail",
42
+ detail: `${dir} is mode ${mode.toString(8).padStart(4, "0")}, not 0700`,
43
+ fix: `run \`chmod 700 ${dir}\``,
44
+ };
45
+ }
46
+ try {
47
+ accessSync(dir, constants.W_OK);
48
+ }
49
+ catch {
50
+ return { name: "data dir", state: "fail", detail: `${dir} is not writable`, fix: "fix its ownership or permissions" };
51
+ }
52
+ return { name: "data dir", state: "ok", detail: `${dir} (0700, writable)` };
53
+ }
54
+ function checkLock(config) {
55
+ const lockFile = paths(config.dataDir).lockFile;
56
+ const alive = lockHolder(lockFile);
57
+ if (alive !== null)
58
+ return { name: "lock", state: "ok", detail: `held by a running server (pid ${alive})` };
59
+ const recorded = lockPid(lockFile);
60
+ if (recorded !== null) {
61
+ return { name: "lock", state: "info", detail: `stale (pid ${recorded} is gone); the next start reclaims it` };
62
+ }
63
+ return { name: "lock", state: "info", detail: "none" };
64
+ }
65
+ function checkCredentials(config) {
66
+ const authDir = paths(config.dataDir).authDir;
67
+ try {
68
+ const account = readLinkedAccount(authDir);
69
+ return account === null
70
+ ? { name: "credentials", state: "info", detail: "no account linked yet" }
71
+ : { name: "credentials", state: "ok", detail: `readable (${account.number})` };
72
+ }
73
+ catch (err) {
74
+ const wazap = err;
75
+ return { name: "credentials", state: "fail", detail: wazap.message, fix: wazap.fix };
76
+ }
77
+ }
78
+ function checkWrites(config) {
79
+ return {
80
+ name: "writes",
81
+ state: "ok",
82
+ detail: `${config.readOnly ? "off" : "on"} (${config.sources.readOnly})`,
83
+ };
84
+ }
85
+ /** Version comparison over the numeric release fields; prereleases sort as their release. */
86
+ export function isNewer(candidate, current) {
87
+ const parts = (version) => version.split(/[.\-+]/, 3).map((piece) => Number.parseInt(piece, 10) || 0);
88
+ const [a, b] = [parts(candidate), parts(current)];
89
+ for (let i = 0; i < 3; i++) {
90
+ if ((a[i] ?? 0) !== (b[i] ?? 0))
91
+ return (a[i] ?? 0) > (b[i] ?? 0);
92
+ }
93
+ return false;
94
+ }
95
+ async function checkUpdate() {
96
+ if (process.env.WAZAP_NO_UPDATE_CHECK === "1") {
97
+ return { name: "update", state: "info", detail: "update check skipped (WAZAP_NO_UPDATE_CHECK=1)" };
98
+ }
99
+ try {
100
+ const response = await fetch("https://registry.npmjs.org/wazap-mcp/latest", {
101
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS),
102
+ });
103
+ if (!response.ok) {
104
+ return { name: "update", state: "info", detail: `update check skipped (registry answered ${response.status})` };
105
+ }
106
+ const { version } = (await response.json());
107
+ return isNewer(version, WAZAP_VERSION)
108
+ ? { name: "update", state: "info", detail: `${version} is out (running ${WAZAP_VERSION})`, fix: "run `npx wazap-mcp@latest`" }
109
+ : { name: "update", state: "ok", detail: `${WAZAP_VERSION} is current` };
110
+ }
111
+ catch {
112
+ return { name: "update", state: "info", detail: "update check skipped (offline)" };
113
+ }
114
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,48 @@
1
+ export class WazapError extends Error {
2
+ code;
3
+ fix;
4
+ constructor(code, message, fix) {
5
+ super(message);
6
+ this.name = "WazapError";
7
+ this.code = code;
8
+ this.fix = fix;
9
+ }
10
+ }
11
+ export const RELINK_FIX = "Run `npx wazap-mcp login`";
12
+ export const RESET_FIX = "Run `npx wazap-mcp logout` then `npx wazap-mcp login`";
13
+ /** What an agent should do about each code. Rendered by the `learn` tool. */
14
+ export const ERROR_GUIDE = {
15
+ NOT_LINKED: "No WhatsApp account is linked. Tell the user to run `npx wazap-mcp login`; do not retry.",
16
+ SESSION_EXPIRED: "The account was unlinked from the phone. Tell the user to run `npx wazap-mcp login`; do not retry.",
17
+ SESSION_CORRUPT: "Stored credentials are unreadable. Tell the user to run `npx wazap-mcp logout` then `npx wazap-mcp login`.",
18
+ NOT_CONNECTED: "The socket is still connecting or reconnecting. Call get_status, wait, retry once.",
19
+ SYNC_IN_PROGRESS: "History sync has not finished. Retry in a few seconds; earlier messages may be missing until then.",
20
+ INVALID_PHONE: "The number is not in international format. Ask the user for a number with a country code, e.g. +40722123456.",
21
+ INVALID_ID: "The id is not a WhatsApp chat, contact or group id. Use an id exactly as returned by another tool.",
22
+ NOT_ON_WHATSAPP: "That number has no WhatsApp account. Do not retry; confirm the number with the user.",
23
+ CHAT_NOT_FOUND: "No such chat is known. Call list_chats or search_contacts to get a valid chat_id.",
24
+ MESSAGE_NOT_FOUND: "No such message is loaded. Use a message_id from read_messages or search_messages.",
25
+ CONTACT_NOT_FOUND: "No such contact is known. Call search_contacts first.",
26
+ GROUP_NOT_FOUND: "No such group is known, or the id is not a group id (it must end in @g.us).",
27
+ NOT_A_PARTICIPANT: "The linked account is not in that group. Do not retry.",
28
+ NOT_ADMIN: "The linked account is not an admin of that group, so this action is refused. Do not retry.",
29
+ GROUP_ANNOUNCEMENT_ONLY: "Only admins may post in that group. Do not retry.",
30
+ MEDIA_UNAVAILABLE: "The media expired on WhatsApp's servers or was never synced. Do not retry; ask the sender to resend.",
31
+ FILE_NOT_FOUND: "The local path does not exist on the machine running wazap. Check the path with the user.",
32
+ FILE_TOO_LARGE: "The file exceeds WhatsApp's 100 MB limit. Send a smaller file.",
33
+ URL_FETCH_FAILED: "The URL could not be fetched. Check it, or download the file first and pass file_path.",
34
+ TEXT_TOO_LONG: "The text exceeds WhatsApp's limit. Split it into several messages.",
35
+ EDIT_WINDOW_EXPIRED: "WhatsApp only allows editing within 15 minutes of sending. Send a correction instead.",
36
+ RETRACT_WINDOW_EXPIRED: "WhatsApp only allows deleting for everyone within 2 days. Do not retry.",
37
+ NOT_OWN_MESSAGE: "This action only works on messages the linked account sent. Do not retry.",
38
+ READ_ONLY: "wazap runs read-only, so writes are refused. Tell the user to restart without WAZAP_READ_ONLY.",
39
+ RATE_LIMITED: "Too many writes too fast. Wait the number of seconds in the fix, then retry once.",
40
+ TIMEOUT: "WhatsApp did not answer in time. Retry once; if it fails again, call get_status.",
41
+ WHATSAPP_ERROR: "WhatsApp rejected the operation. Read the message; do not blindly retry.",
42
+ };
43
+ /** Any thrown value as a WazapError, so no tool ever surfaces a raw error. */
44
+ export function asWazapError(err) {
45
+ if (err instanceof WazapError)
46
+ return err;
47
+ return new WazapError("WHATSAPP_ERROR", err instanceof Error ? err.message : String(err));
48
+ }
package/dist/ids.js ADDED
@@ -0,0 +1,50 @@
1
+ import { WazapError } from "./errors.js";
2
+ const PHONE_EXAMPLE = "Use international format, e.g. +40722123456";
3
+ /** Digits of a phone number in international format, or INVALID_PHONE. */
4
+ export function normalizePhone(input) {
5
+ const digits = input.trim().replace(/^\+/, "").replace(/[\s\-().]/g, "");
6
+ if (!/^\d+$/.test(digits) || digits.startsWith("0") || digits.length < 8 || digits.length > 15) {
7
+ throw new WazapError("INVALID_PHONE", `"${input.trim()}" is not a phone number in international format.`, PHONE_EXAMPLE);
8
+ }
9
+ return digits;
10
+ }
11
+ export function isGroupId(jid) {
12
+ return jid.endsWith("@g.us");
13
+ }
14
+ /**
15
+ * Canonicalize anything a caller may pass as a chat id.
16
+ * Individuals become `<digits>@s.whatsapp.net`, groups stay `<id>@g.us`.
17
+ * A `@lid` is translated through `lidToPn` when the mapping is known; without
18
+ * one the lid is kept, because it still addresses the chat.
19
+ */
20
+ export function resolveChatId(input, lidToPn) {
21
+ const trimmed = (input ?? "").trim();
22
+ if (!trimmed)
23
+ throw new WazapError("INVALID_ID", "Empty chat id.", "Pass an id from list_chats or search_contacts");
24
+ const at = trimmed.lastIndexOf("@");
25
+ if (at === -1)
26
+ return `${normalizePhone(trimmed)}@s.whatsapp.net`;
27
+ const user = trimmed.slice(0, at).split(":")[0];
28
+ const domain = trimmed.slice(at + 1).toLowerCase();
29
+ if (domain === "g.us") {
30
+ if (!/^[\w.@-]+$/.test(user))
31
+ throw new WazapError("INVALID_ID", `"${trimmed}" is not a valid group id.`);
32
+ return `${user}@g.us`;
33
+ }
34
+ if (domain === "lid") {
35
+ const mapped = lidToPn?.(`${user}@lid`);
36
+ if (mapped)
37
+ return `${digitsOrThrow(mapped, trimmed)}@s.whatsapp.net`;
38
+ return `${user}@lid`;
39
+ }
40
+ if (domain === "s.whatsapp.net" || domain === "c.us") {
41
+ return `${digitsOrThrow(user, trimmed)}@s.whatsapp.net`;
42
+ }
43
+ throw new WazapError("INVALID_ID", `"${trimmed}" is not a WhatsApp id.`, "Expected a phone number, <digits>@s.whatsapp.net or <id>@g.us");
44
+ }
45
+ function digitsOrThrow(value, original) {
46
+ const digits = value.split("@")[0].split(":")[0];
47
+ if (!/^\d+$/.test(digits))
48
+ throw new WazapError("INVALID_ID", `"${original}" is not a WhatsApp id.`);
49
+ return digits;
50
+ }
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { BANNER } from "./banner.js";
3
+ import { runGreet, runLogin, runLogout, runServe, runStatus } from "./cli.js";
4
+ import { WAZAP_VERSION, parseCli, pickDefaultAction } from "./config.js";
5
+ import { CLIENT_NAMES, runConnect } from "./connect.js";
6
+ import { runConfig } from "./settings.js";
7
+ import { WazapError } from "./errors.js";
8
+ import { say } from "./logger.js";
9
+ const USAGE = `${BANNER}
10
+
11
+ Usage:
12
+ wazap [serve] [--http] [--host <host>] [--port <port>] Run the MCP server (default: stdio)
13
+ wazap login [--phone +40722123456] [--qr] Link a WhatsApp account
14
+ wazap connect <client> [--dry-run] Register wazap with an MCP client
15
+ wazap config [writes on|off] Show the effective settings, or allow/refuse writes
16
+ wazap status [--live] [--json] Check the install, the session and the server
17
+ wazap logout Unlink and delete local credentials
18
+
19
+ Clients for wazap connect: ${CLIENT_NAMES}.
20
+
21
+ Options:
22
+ --data-dir <path> Where wazap keeps its data (default ~/.wazap, or $WAZAP_DATA_DIR)
23
+ --read-only Refuse every write; the write tools are not registered at all
24
+ --http Serve Streamable HTTP instead of stdio
25
+ --host <host> HTTP bind address (default 127.0.0.1)
26
+ --port <port> HTTP port (default 8766)
27
+ --phone <number> Phone number in international format, for login
28
+ --qr Log in by QR code instead of a pairing code
29
+ --dry-run With connect: print what would be written, and write nothing
30
+ --live With status: reach WhatsApp for real, then close the connection
31
+ --json With status: print the whole report as one JSON object on stdout
32
+ --writes Allow the agent to write, without login asking
33
+ --no-writes Keep the agent read-only, without login asking
34
+ -y, --yes Do not ask anything at the end of login
35
+ -h, --help Show this help
36
+ -v, --version Show the version
37
+
38
+ Environment: WAZAP_DATA_DIR, WAZAP_READ_ONLY, WAZAP_SYNC_FULL_HISTORY, WAZAP_PERSIST_HISTORY,
39
+ WAZAP_TRANSPORT, WAZAP_HOST, WAZAP_PORT, WAZAP_READ_TOKEN, WAZAP_WRITE_TOKEN, WAZAP_RATE_LIMIT,
40
+ WAZAP_NO_UPDATE_CHECK.
41
+ An optional <data-dir>/.env is loaded if present.`;
42
+ async function main() {
43
+ const invocation = parseCli();
44
+ if (invocation.kind === "help") {
45
+ say(USAGE);
46
+ return;
47
+ }
48
+ if (invocation.kind === "version") {
49
+ say(WAZAP_VERSION);
50
+ return;
51
+ }
52
+ const { config } = invocation;
53
+ switch (config.command) {
54
+ case "serve":
55
+ if (pickDefaultAction(config, process.stdin.isTTY === true, process.stderr.isTTY === true) === "greet") {
56
+ await runGreet(config);
57
+ return;
58
+ }
59
+ await runServe(config);
60
+ return;
61
+ case "login":
62
+ await runLogin(config);
63
+ return;
64
+ case "connect":
65
+ runConnect(config);
66
+ return;
67
+ case "config":
68
+ runConfig(config);
69
+ return;
70
+ case "status":
71
+ await runStatus(config);
72
+ return;
73
+ case "logout":
74
+ await runLogout(config);
75
+ return;
76
+ }
77
+ }
78
+ main().catch((err) => {
79
+ if (err instanceof WazapError) {
80
+ say(err.message);
81
+ if (err.fix)
82
+ say(err.fix);
83
+ }
84
+ else {
85
+ say(err instanceof Error ? err.message : String(err));
86
+ }
87
+ process.exit(1);
88
+ });
package/dist/lock.js ADDED
@@ -0,0 +1,42 @@
1
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ /** Pid recorded in the lock file, whether or not that process is still alive. */
4
+ export function lockPid(lockFile) {
5
+ let pid;
6
+ try {
7
+ pid = Number.parseInt(readFileSync(lockFile, "utf8").trim(), 10);
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
13
+ }
14
+ /** Pid of the live process holding the lock, or null if free (missing or stale). */
15
+ export function lockHolder(lockFile) {
16
+ const pid = lockPid(lockFile);
17
+ if (pid === null)
18
+ return null;
19
+ try {
20
+ process.kill(pid, 0);
21
+ return pid;
22
+ }
23
+ catch (err) {
24
+ // EPERM means the pid exists but belongs to another user, so it is alive.
25
+ return err.code === "EPERM" ? pid : null;
26
+ }
27
+ }
28
+ export function writeLock(lockFile) {
29
+ mkdirSync(dirname(lockFile), { recursive: true, mode: 0o700 });
30
+ writeFileSync(lockFile, `${process.pid}\n`, { mode: 0o600 });
31
+ }
32
+ /** Remove the lock, but only if it is still ours. */
33
+ export function releaseLock(lockFile) {
34
+ try {
35
+ if (Number.parseInt(readFileSync(lockFile, "utf8").trim(), 10) !== process.pid)
36
+ return;
37
+ unlinkSync(lockFile);
38
+ }
39
+ catch {
40
+ /* already gone */
41
+ }
42
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * stdout is the MCP JSON-RPC channel, so every human-readable line goes to
3
+ * stderr. Anything else on stdout corrupts the protocol stream.
4
+ */
5
+ const PREFIX = "[wazap]";
6
+ export function log(...args) {
7
+ console.error(PREFIX, ...args);
8
+ }
9
+ export function logError(context, err) {
10
+ const message = err instanceof Error ? err.message : String(err);
11
+ console.error(`${PREFIX} ERROR (${context}):`, message);
12
+ }
13
+ /** CLI output for a human at a terminal. Also stderr, for the same reason. */
14
+ export function say(...args) {
15
+ console.error(...args);
16
+ }