wazap-mcp 0.16.0 → 0.18.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/README.md +93 -11
- package/dist/account-cli.js +1 -1
- package/dist/account-resolve.js +3 -1
- package/dist/accounts.js +22 -4
- package/dist/cli.js +37 -4
- package/dist/compact.js +15 -2
- package/dist/config.js +6 -1
- package/dist/connect.js +26 -6
- package/dist/deps.js +1 -0
- package/dist/doctor.js +56 -6
- package/dist/drafts.js +4 -1
- package/dist/errors.js +3 -0
- package/dist/gif.js +12 -5
- package/dist/ids.js +4 -1
- package/dist/index.js +23 -6
- package/dist/messages.js +44 -8
- package/dist/notes.js +20 -3
- package/dist/oauth.js +21 -6
- package/dist/outgoing-media.js +8 -22
- package/dist/pairing.js +2 -2
- package/dist/previews.js +20 -6
- package/dist/recall/engine.js +268 -0
- package/dist/recall/index.js +6 -0
- package/dist/recall/models.js +68 -0
- package/dist/recall/queue.js +183 -0
- package/dist/recall/settings.js +63 -0
- package/dist/recall/store.js +422 -0
- package/dist/recall/types.js +2 -0
- package/dist/safe-media.js +128 -0
- package/dist/sent-ids.js +40 -0
- package/dist/settings.js +53 -12
- package/dist/setup.js +32 -5
- package/dist/skills.js +3 -1
- package/dist/store.js +4 -1
- package/dist/tools.js +166 -19
- package/dist/transcribe/index.js +1 -1
- package/dist/transcribe/local.js +15 -1
- package/dist/transcribe/queue.js +21 -3
- package/dist/ui.js +1 -0
- package/dist/webhook.js +181 -24
- package/dist/whatsapp.js +399 -24
- package/dist/wizard.js +3 -0
- package/package.json +12 -3
- package/skills/whatsapp-recall/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -545,9 +545,9 @@ response still carries `account_id`. `link_account` needs an account that
|
|
|
545
545
|
already exists. Five accounts is advice, not a cap. One phone number is one
|
|
546
546
|
account.
|
|
547
547
|
|
|
548
|
-
An account can override the global webhook URL and
|
|
549
|
-
(`webhook_url`, `webhook_secret`).
|
|
550
|
-
with that account's id and name.
|
|
548
|
+
An account can override the global webhook URL, secret and event list in
|
|
549
|
+
`accounts.json` (`webhook_url`, `webhook_secret`, `webhook_events`).
|
|
550
|
+
`wazap webhook test --account work` posts with that account's id and name.
|
|
551
551
|
|
|
552
552
|
## Several clients at once
|
|
553
553
|
|
|
@@ -704,38 +704,119 @@ What to know before exposing it:
|
|
|
704
704
|
|
|
705
705
|
## Outbound webhook
|
|
706
706
|
|
|
707
|
-
|
|
708
|
-
is
|
|
707
|
+
Live events POST to one URL. Off by default. History sync is not posted.
|
|
708
|
+
Only `message_received` is posted unless you ask for more, because a consumer
|
|
709
|
+
that answers every POST without reading `event` would otherwise answer the
|
|
710
|
+
messages its own owner typed on the phone.
|
|
711
|
+
|
|
712
|
+
The `event` field names one of three. `message_received` is a message another
|
|
713
|
+
person sent. `message_sent` is a message this account sent itself, typed on
|
|
714
|
+
the phone or on another linked device; a message wazap sent through its own
|
|
715
|
+
tools is not announced, so a consumer can never be made to answer itself.
|
|
716
|
+
`connection` says the link came up, went down or expired.
|
|
717
|
+
|
|
718
|
+
Ask for the other two in `WAZAP_WEBHOOK_EVENTS`, comma-separated
|
|
719
|
+
(`message_received,connection`), or say `all` for the three of them. Case
|
|
720
|
+
does not matter and the spaces around a name are ignored. An unknown name
|
|
721
|
+
fails `wazap status`, doctor and setup. An account carries its own list as
|
|
722
|
+
`webhook_events` in `accounts.json`.
|
|
709
723
|
|
|
710
724
|
```bash
|
|
711
725
|
npx wazap-mcp config webhook on # asks for URL + secret (secret is not echoed)
|
|
712
726
|
npx wazap-mcp webhook test # POST a probe event
|
|
727
|
+
npx wazap-mcp webhook test --event connection # needs connection enabled
|
|
713
728
|
npx wazap-mcp webhook test --account work
|
|
714
729
|
npx wazap-mcp config webhook off
|
|
715
730
|
```
|
|
716
731
|
|
|
717
732
|
On without a URL or secret fails `wazap status`, doctor and setup. A failed
|
|
718
733
|
delivery retries twice (200 ms, then 500 ms), then sets `webhook.last_error`
|
|
719
|
-
and leaves WhatsApp and MCP running. An account may set `webhook_url
|
|
720
|
-
`webhook_secret` in `accounts.json`; those win over the
|
|
734
|
+
and leaves WhatsApp and MCP running. An account may set `webhook_url`,
|
|
735
|
+
`webhook_secret` and `webhook_events` in `accounts.json`; those win over the
|
|
736
|
+
global URL, secret and event list, and `config webhook off --account work`
|
|
737
|
+
clears all three.
|
|
738
|
+
|
|
739
|
+
`webhook test --event <name>` posts nothing and exits non-zero when that
|
|
740
|
+
event is not enabled, and says what to enable it with. While the webhook is
|
|
741
|
+
on, `wazap config` prints the events it posts on an `events:` line.
|
|
721
742
|
|
|
722
743
|
HMAC: `X-Wazap-Signature` is `sha256=<hex>`, HMAC-SHA256 of the exact raw
|
|
723
744
|
JSON body with the secret that signed it. Verify that raw body, not a
|
|
724
745
|
re-serialized object. HTTPS only, except `http://` on loopback.
|
|
725
746
|
|
|
747
|
+
`text` is a preview, cut at 2000 characters and ending in a single `…`.
|
|
748
|
+
`truncated` is true when it was cut. For `kind: "audio"`, `text` is the
|
|
749
|
+
transcription when wazap auto-transcribed the note itself, which it does for
|
|
750
|
+
incoming notes only; the event waits up to 60 seconds for those words. In
|
|
751
|
+
every other case `text` is the `[voice message · 0:42]` placeholder: a note
|
|
752
|
+
you recorded yourself, a note longer than 600 seconds, a note WhatsApp stated
|
|
753
|
+
no duration for, and a transcription that failed. `ts` is the original local
|
|
754
|
+
time with a numeric offset, kept for consumers already reading it, and
|
|
755
|
+
`timestamp` is the same instant in UTC.
|
|
756
|
+
|
|
757
|
+
Events are not guaranteed to arrive in the order they happened. A message held
|
|
758
|
+
for its transcript is overtaken by the messages behind it, so order by
|
|
759
|
+
`timestamp` and not by arrival. Connection events are the exception: they are
|
|
760
|
+
delivered in the order the link moved in.
|
|
761
|
+
|
|
762
|
+
A message another person sent:
|
|
763
|
+
|
|
726
764
|
```json
|
|
727
765
|
{
|
|
728
766
|
"event": "message_received",
|
|
729
|
-
"from": "
|
|
767
|
+
"from": "15550100",
|
|
730
768
|
"chat_id": "15550100@s.whatsapp.net",
|
|
731
|
-
"ts": "2026-09-
|
|
732
|
-
"
|
|
769
|
+
"ts": "2026-09-08T17:00:00+03:00",
|
|
770
|
+
"timestamp": "2026-09-08T14:00:00.000Z",
|
|
771
|
+
"text": "hello, or a preview of something longer",
|
|
772
|
+
"truncated": false,
|
|
773
|
+
"kind": "text",
|
|
774
|
+
"from_me": false,
|
|
775
|
+
"is_self_chat": false,
|
|
733
776
|
"message_id": "false_15550100@s.whatsapp.net_3EB0…",
|
|
734
777
|
"account_id": "default",
|
|
735
778
|
"account_name": "default"
|
|
736
779
|
}
|
|
737
780
|
```
|
|
738
781
|
|
|
782
|
+
A message sent from the phone, here in the "Message yourself" chat:
|
|
783
|
+
|
|
784
|
+
```json
|
|
785
|
+
{
|
|
786
|
+
"event": "message_sent",
|
|
787
|
+
"from": "15551234",
|
|
788
|
+
"chat_id": "15551234@s.whatsapp.net",
|
|
789
|
+
"ts": "2026-09-08T17:04:12+03:00",
|
|
790
|
+
"timestamp": "2026-09-08T14:04:12.000Z",
|
|
791
|
+
"text": "call the notary at 14:00 on Wednesday",
|
|
792
|
+
"truncated": false,
|
|
793
|
+
"kind": "text",
|
|
794
|
+
"from_me": true,
|
|
795
|
+
"is_self_chat": true,
|
|
796
|
+
"message_id": "true_15551234@s.whatsapp.net_3EB0…",
|
|
797
|
+
"account_id": "default",
|
|
798
|
+
"account_name": "default"
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
A connection change. `status` is `linked`, `disconnected` or `expired`;
|
|
803
|
+
`not_linked`, `linking` and `connecting` post nothing, and two changes that
|
|
804
|
+
mean the same status post once.
|
|
805
|
+
|
|
806
|
+
```json
|
|
807
|
+
{
|
|
808
|
+
"event": "connection",
|
|
809
|
+
"status": "expired",
|
|
810
|
+
"timestamp": "2026-09-08T14:10:00.000Z",
|
|
811
|
+
"account_id": "default",
|
|
812
|
+
"account_name": "default"
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
`connection` reports what the socket does while wazap is running. A clean
|
|
817
|
+
shutdown posts nothing, and a crash posts nothing either, so silence does not
|
|
818
|
+
mean the link is up. Poll `get_status` when you need to know that.
|
|
819
|
+
|
|
739
820
|
## Settings
|
|
740
821
|
|
|
741
822
|
| Variable | Default | Meaning |
|
|
@@ -759,9 +840,10 @@ re-serialized object. HTTPS only, except `http://` on loopback.
|
|
|
759
840
|
| `WAZAP_TRANSCRIBE_API_KEY` | unset | API key; `OPENAI_API_KEY` is the fallback. Never a flag. |
|
|
760
841
|
| `WAZAP_TRANSCRIBE_URL` | `https://api.openai.com/v1` | OpenAI-compatible base URL. |
|
|
761
842
|
| `WAZAP_TRANSCRIBE_MODEL` | `gpt-4o-mini-transcribe` | Model at that URL. |
|
|
762
|
-
| `WAZAP_WEBHOOK` | `off` | `on` posts
|
|
843
|
+
| `WAZAP_WEBHOOK` | `off` | `on` posts the enabled events to the webhook URL. |
|
|
763
844
|
| `WAZAP_WEBHOOK_URL` | unset | HTTPS endpoint. `http://` only on loopback. An account `webhook_url` wins. |
|
|
764
845
|
| `WAZAP_WEBHOOK_SECRET` | unset | Shared secret for `X-Wazap-Signature`. Never a flag. An account `webhook_secret` wins. |
|
|
846
|
+
| `WAZAP_WEBHOOK_EVENTS` | unset (`message_received`) | Which events to post, comma-separated, or `all`. An account `webhook_events` wins. |
|
|
765
847
|
|
|
766
848
|
Flags beat environment variables, which beat `<data-dir>/.env`.
|
|
767
849
|
|
package/dist/account-cli.js
CHANGED
|
@@ -38,7 +38,7 @@ export function describeStatusAccount(row) {
|
|
|
38
38
|
export function accountRows(config) {
|
|
39
39
|
const registry = AccountRegistry.load(config.dataDir);
|
|
40
40
|
return registry.all().map((record) => {
|
|
41
|
-
let linked
|
|
41
|
+
let linked;
|
|
42
42
|
try {
|
|
43
43
|
linked = readLinkedAccount(accountPaths(config.dataDir, record.id).authDir);
|
|
44
44
|
}
|
package/dist/account-resolve.js
CHANGED
|
@@ -136,7 +136,9 @@ export function renderGetStatus(s, writeTools, hub) {
|
|
|
136
136
|
`- **contacts named**: ${s.contacts_named}`,
|
|
137
137
|
`- **data dir**: ${s.data_dir} · **read-only**: ${s.read_only} · **write tools**: ${writeLine} · **rate limit**: ${s.rate_limit}/min`,
|
|
138
138
|
`- **versions**: wazap ${s.wazap_version}, baileys ${s.baileys_version}`,
|
|
139
|
-
s.pairing
|
|
139
|
+
s.pairing
|
|
140
|
+
? `- **pairing code**: ${s.pairing.code} for ${s.pairing.phone_masked}, until ${s.pairing.expires_at}`
|
|
141
|
+
: null,
|
|
140
142
|
webhookStatusLine(s.webhook),
|
|
141
143
|
s.last_error ? `- **last error**: ${s.last_error}` : null,
|
|
142
144
|
s.hint ? `- **hint**: ${s.hint}` : null,
|
package/dist/accounts.js
CHANGED
|
@@ -2,7 +2,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, wri
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { readLinkedAccount } from "./auth-state.js";
|
|
4
4
|
import { accountPaths, paths } from "./config.js";
|
|
5
|
-
import { WazapError } from "./errors.js";
|
|
5
|
+
import { WazapError, asWazapError } from "./errors.js";
|
|
6
|
+
import { parseWebhookEvents } from "./webhook.js";
|
|
6
7
|
export const DEFAULT_ACCOUNT_ID = "default";
|
|
7
8
|
export const ACCOUNT_ID_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
8
9
|
const FIX_LIST = "Run `wazap account list`";
|
|
@@ -72,10 +73,13 @@ function parseAccountRecord(value, file) {
|
|
|
72
73
|
}
|
|
73
74
|
record.rate_limit = value.rate_limit;
|
|
74
75
|
}
|
|
75
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
...record,
|
|
78
|
+
...webhookFields(value.id, value.webhook_url, value.webhook_secret, value.webhook_events, ` in ${file}`),
|
|
79
|
+
};
|
|
76
80
|
}
|
|
77
81
|
/** Shared by load and `setWebhook` so a writer cannot persist what load refuses. */
|
|
78
|
-
function webhookFields(id, url, secret, where = "", fix = "Fix or remove accounts.json") {
|
|
82
|
+
function webhookFields(id, url, secret, events, where = "", fix = "Fix or remove accounts.json") {
|
|
79
83
|
const fields = {};
|
|
80
84
|
if (url !== undefined) {
|
|
81
85
|
if (typeof url !== "string" || url.trim() === "") {
|
|
@@ -89,6 +93,19 @@ function webhookFields(id, url, secret, where = "", fix = "Fix or remove account
|
|
|
89
93
|
}
|
|
90
94
|
fields.webhook_secret = secret;
|
|
91
95
|
}
|
|
96
|
+
if (events !== undefined) {
|
|
97
|
+
if (typeof events !== "string" || events.trim() === "") {
|
|
98
|
+
throw new WazapError("INVALID_ID", `Account "${id}"${where} has a bad webhook_events.`, fix);
|
|
99
|
+
}
|
|
100
|
+
const trimmed = events.trim();
|
|
101
|
+
try {
|
|
102
|
+
parseWebhookEvents(trimmed);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
throw new WazapError("INVALID_ID", `Account "${id}"${where} has a bad webhook_events: ${asWazapError(err).message}`, fix);
|
|
106
|
+
}
|
|
107
|
+
fields.webhook_events = trimmed;
|
|
108
|
+
}
|
|
92
109
|
return fields;
|
|
93
110
|
}
|
|
94
111
|
function parseAccountsFile(value, file) {
|
|
@@ -200,7 +217,7 @@ export class AccountRegistry {
|
|
|
200
217
|
setWebhook(id, webhook) {
|
|
201
218
|
this.commit(this.withAccount(id, (account) => ({
|
|
202
219
|
...account,
|
|
203
|
-
...webhookFields(id, webhook.url, webhook.secret, "", "Set a non-empty webhook URL or secret"),
|
|
220
|
+
...webhookFields(id, webhook.url, webhook.secret, undefined, "", "Set a non-empty webhook URL or secret"),
|
|
204
221
|
})));
|
|
205
222
|
}
|
|
206
223
|
/** Drop the per-account override so the account follows the global webhook again. */
|
|
@@ -209,6 +226,7 @@ export class AccountRegistry {
|
|
|
209
226
|
const next = { ...account };
|
|
210
227
|
delete next.webhook_url;
|
|
211
228
|
delete next.webhook_secret;
|
|
229
|
+
delete next.webhook_events;
|
|
212
230
|
return next;
|
|
213
231
|
}));
|
|
214
232
|
}
|
package/dist/cli.js
CHANGED
|
@@ -23,13 +23,14 @@ import { lockHolder, releaseLock, writeLock } from "./lock.js";
|
|
|
23
23
|
import { log, logError, say } from "./logger.js";
|
|
24
24
|
import { clockLabel, formatAge } from "./messages.js";
|
|
25
25
|
import { oauthProblem } from "./oauth.js";
|
|
26
|
+
import { downloadEmbed, embedModelSpec, readRecallSettings, } from "./recall/index.js";
|
|
26
27
|
import { PAIRING_TIMEOUT_MS, linkSession, prettyCode, settledAccount, startPairing } from "./pairing.js";
|
|
27
28
|
import { runHttp, runStdio, startLoopbackEndpoint } from "./server.js";
|
|
28
29
|
import { fetchHealth, serviceHolding } from "./service.js";
|
|
29
30
|
import { applyWrites } from "./settings.js";
|
|
30
31
|
import { MODELS, downloadModel, maskKey, modelSpec, readTranscribeSettings, stripPasted, transcribeFile, transcribeReady, } from "./transcribe/index.js";
|
|
31
32
|
import { bold, box, brand, humanLayout, dim, fail, fix, info, maskNumber, next, ok, openScreen, qrSavedLine, shortPath, spinner, tilde, warn, } from "./ui.js";
|
|
32
|
-
import { loginWizardSteps, maybeWizard, wizDim, wizFail, wizInfo, wizOk, wizWarn
|
|
33
|
+
import { loginWizardSteps, maybeWizard, wizDim, wizFail, wizInfo, wizOk, wizWarn } from "./wizard.js";
|
|
33
34
|
import { WhatsAppService } from "./whatsapp.js";
|
|
34
35
|
/** How long a probe waits for the socket to settle; tests shorten it through the environment. */
|
|
35
36
|
const LIVE_TIMEOUT_MS = Number.parseInt(process.env.WAZAP_LIVE_TIMEOUT_MS ?? "", 10) || 15_000;
|
|
@@ -106,7 +107,9 @@ export async function runStatus(config) {
|
|
|
106
107
|
/** Today's phrasing, kept verbatim so pipes and log captures keep parsing. */
|
|
107
108
|
function plainStatus(report) {
|
|
108
109
|
const lines = [`data dir: ${report.data_dir}`];
|
|
109
|
-
const credsNote = report.credentials_readable
|
|
110
|
+
const credsNote = report.credentials_readable
|
|
111
|
+
? ""
|
|
112
|
+
: " (credentials unreadable — run `wazap logout` then `wazap login`)";
|
|
110
113
|
lines.push(`linked: ${report.linked ? "yes" : "no"}${credsNote}`);
|
|
111
114
|
if (report.account)
|
|
112
115
|
lines.push(`account: ${describeAccount(report.account)}`);
|
|
@@ -326,6 +329,32 @@ export async function downloadTranscribeModel(settings, spec) {
|
|
|
326
329
|
throw err;
|
|
327
330
|
}
|
|
328
331
|
}
|
|
332
|
+
/** `wazap embed download`. */
|
|
333
|
+
export async function runEmbed(config) {
|
|
334
|
+
const [verb] = config.args;
|
|
335
|
+
if (verb !== "download") {
|
|
336
|
+
throw new WazapError("INVALID_ID", `Cannot run \`wazap embed ${config.args.join(" ")}\`.`, "Run `wazap embed download`");
|
|
337
|
+
}
|
|
338
|
+
await ensureDeps([DEPS.llama], config);
|
|
339
|
+
const settings = readRecallSettings(process.env, config.dataDir);
|
|
340
|
+
await downloadEmbedModel(settings, config.modelName);
|
|
341
|
+
}
|
|
342
|
+
/** The same check-then-fetch dance downloadTranscribeModel does, for the embed table. */
|
|
343
|
+
export async function downloadEmbedModel(settings, modelName) {
|
|
344
|
+
const spec = embedModelSpec(modelName ?? settings.model);
|
|
345
|
+
const spin = spinner(`Checking ${spec.file}…`);
|
|
346
|
+
try {
|
|
347
|
+
const result = await downloadEmbed(settings.modelsDir, spec, (progress) => {
|
|
348
|
+
const percent = Math.floor((progress.received / progress.total) * 100);
|
|
349
|
+
spin.update(`Downloading ${spec.file} — ${mib(progress.received)} / ${mib(progress.total)} MiB (${percent}%)`);
|
|
350
|
+
});
|
|
351
|
+
spin.stop(ok(`${spec.file} (${mib(spec.bytes)} MiB) ${result.alreadyPresent ? "already present" : "verified"}`));
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
spin.stop();
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
329
358
|
/** What identifies each provider on screen. Keyed like PROVIDERS, never branched on. */
|
|
330
359
|
const PROVIDER_ROWS = {
|
|
331
360
|
local: (settings) => [
|
|
@@ -617,7 +646,10 @@ export async function linkAndSync(config, announce = () => { }, w = null) {
|
|
|
617
646
|
announce("Link your phone");
|
|
618
647
|
let account;
|
|
619
648
|
try {
|
|
620
|
-
account =
|
|
649
|
+
account =
|
|
650
|
+
phone === null
|
|
651
|
+
? await linkByQr(selected.paths, waiting, w)
|
|
652
|
+
: await linkByCode(selected.paths.authDir, phone, waiting, w);
|
|
621
653
|
}
|
|
622
654
|
catch (err) {
|
|
623
655
|
waiting.stop();
|
|
@@ -732,7 +764,8 @@ export async function yieldSession(config, lockFile, why = "pairing") {
|
|
|
732
764
|
return () => { };
|
|
733
765
|
const held = serviceHolding(config.dataDir, running);
|
|
734
766
|
if (held === null) {
|
|
735
|
-
throw leftoverRefusal(config) ??
|
|
767
|
+
throw (leftoverRefusal(config) ??
|
|
768
|
+
new WazapError("WHATSAPP_ERROR", `wazap is running (pid ${running}).`, leftoverFix(running)));
|
|
736
769
|
}
|
|
737
770
|
say(info(`Stopping the wazap service for ${why}`));
|
|
738
771
|
held.supervisor.stop(held.record);
|
package/dist/compact.js
CHANGED
|
@@ -30,7 +30,13 @@ export function compactConversations(conversations) {
|
|
|
30
30
|
last.message_ids.push(m.message_id);
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
|
-
lines.push({
|
|
33
|
+
lines.push({
|
|
34
|
+
timestamp: m.timestamp,
|
|
35
|
+
sender: m.sender.id,
|
|
36
|
+
from_me: m.from_me,
|
|
37
|
+
text: m.text,
|
|
38
|
+
message_ids: [m.message_id],
|
|
39
|
+
});
|
|
34
40
|
// The name rides on the line, once, for the renderer.
|
|
35
41
|
lines[lines.length - 1].name = m.from_me
|
|
36
42
|
? "me"
|
|
@@ -40,7 +46,14 @@ export function compactConversations(conversations) {
|
|
|
40
46
|
}
|
|
41
47
|
if (lines.length === 0 && dropped.media === 0 && dropped.wordless === 0)
|
|
42
48
|
continue;
|
|
43
|
-
out.push({
|
|
49
|
+
out.push({
|
|
50
|
+
chat_id: c.chat_id,
|
|
51
|
+
chat_name: c.chat_name,
|
|
52
|
+
type: c.type,
|
|
53
|
+
...(c.note ? { note: c.note } : {}),
|
|
54
|
+
lines,
|
|
55
|
+
dropped,
|
|
56
|
+
});
|
|
44
57
|
}
|
|
45
58
|
return out;
|
|
46
59
|
}
|
package/dist/config.js
CHANGED
|
@@ -50,6 +50,7 @@ const COMMAND_ARGS = {
|
|
|
50
50
|
// No positional means the first available provider; `off` takes the tunnel down.
|
|
51
51
|
expose: [0, 1],
|
|
52
52
|
transcribe: [1, 2],
|
|
53
|
+
embed: [1],
|
|
53
54
|
update: [0],
|
|
54
55
|
webhook: [1],
|
|
55
56
|
account: [1, 2],
|
|
@@ -67,8 +68,9 @@ const COMMAND_USAGE = {
|
|
|
67
68
|
skills: "Run `wazap skills install [<harness>]`",
|
|
68
69
|
service: "Run `wazap service install|status|start|stop|restart|logs|uninstall`",
|
|
69
70
|
transcribe: "Run `wazap transcribe download` or `wazap transcribe test <audio file>`",
|
|
71
|
+
embed: "Run `wazap embed download`",
|
|
70
72
|
contacts: "Run `wazap contacts resync`",
|
|
71
|
-
config: "Run `wazap config`, `wazap config writes on|off`, `wazap config transcribe local|openai|off`, or `wazap config webhook on|off`",
|
|
73
|
+
config: "Run `wazap config`, `wazap config writes on|off`, `wazap config transcribe local|openai|off`, `wazap config recall local|off`, or `wazap config webhook on|off`",
|
|
72
74
|
webhook: "Run `wazap webhook test`",
|
|
73
75
|
account: ACCOUNT_USAGE,
|
|
74
76
|
migrate: MIGRATE_USAGE,
|
|
@@ -163,6 +165,7 @@ export function parseCli(argv = process.argv.slice(2)) {
|
|
|
163
165
|
service: { type: "boolean" },
|
|
164
166
|
expose: { type: "boolean" },
|
|
165
167
|
yes: { type: "boolean", short: "y" },
|
|
168
|
+
event: { type: "string" },
|
|
166
169
|
account: { type: "string" },
|
|
167
170
|
name: { type: "string" },
|
|
168
171
|
help: { type: "boolean", short: "h" },
|
|
@@ -224,6 +227,7 @@ export function parseCli(argv = process.argv.slice(2)) {
|
|
|
224
227
|
rateLimit: sourceOf("WAZAP_RATE_LIMIT", false),
|
|
225
228
|
transcribe: sourceOf("WAZAP_TRANSCRIBE", false),
|
|
226
229
|
webhook: sourceOf("WAZAP_WEBHOOK", false),
|
|
230
|
+
recall: sourceOf("WAZAP_RECALL", false),
|
|
227
231
|
},
|
|
228
232
|
command,
|
|
229
233
|
explicitCommand: first !== undefined,
|
|
@@ -245,6 +249,7 @@ export function parseCli(argv = process.argv.slice(2)) {
|
|
|
245
249
|
keepRunning: values.expose === true ? "expose" : values.service === true ? "service" : null,
|
|
246
250
|
accountId: values.account,
|
|
247
251
|
accountName: values.name,
|
|
252
|
+
webhookEvent: values.event,
|
|
248
253
|
},
|
|
249
254
|
};
|
|
250
255
|
}
|
package/dist/connect.js
CHANGED
|
@@ -135,8 +135,18 @@ export function whereInstalled(binPath = process.argv[1] ?? "", pathEnv = proces
|
|
|
135
135
|
const script = binPath === "" ? "" : resolve(binPath);
|
|
136
136
|
if (isNpxPath(binPath))
|
|
137
137
|
return { kind: "npx", script };
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const onPath = commandPath("wazap", pathEnv, exists);
|
|
139
|
+
if (onPath) {
|
|
140
|
+
try {
|
|
141
|
+
if (realpathSync(onPath) === realpathSync(script))
|
|
142
|
+
return { kind: "global", script };
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// A package path or a direct launcher can still be classified when inspecting another host.
|
|
146
|
+
if (/[/\\]node_modules[/\\]wazap(?:-mcp)?[/\\]/.test(script) || resolve(onPath) === script)
|
|
147
|
+
return { kind: "global", script };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
140
150
|
return { kind: "checkout", script };
|
|
141
151
|
}
|
|
142
152
|
const GLOBAL_FIX = "run `npm i -g wazap-mcp` yourself (sudo on some Linux installs), then `wazap setup` again";
|
|
@@ -145,7 +155,12 @@ export function installGlobally(version = WAZAP_VERSION, npm = "npm") {
|
|
|
145
155
|
const result = spawnSync(npm, ["install", "-g", `wazap-mcp@${version}`], { stdio: "inherit" });
|
|
146
156
|
if (result.error !== undefined || result.status !== 0) {
|
|
147
157
|
const detail = result.error === undefined ? `exit ${result.status ?? -1}` : result.error.message;
|
|
148
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
name: "install",
|
|
160
|
+
state: "fail",
|
|
161
|
+
detail: `npm install -g wazap-mcp@${version} failed (${detail})`,
|
|
162
|
+
fix: GLOBAL_FIX,
|
|
163
|
+
};
|
|
149
164
|
}
|
|
150
165
|
return { name: "install", state: "ok", detail: `wazap-mcp@${version} installed globally` };
|
|
151
166
|
}
|
|
@@ -224,8 +239,9 @@ export const GUI_PATH = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin";
|
|
|
224
239
|
export function mcpEntry(config, spec, install = whereInstalled()) {
|
|
225
240
|
const entry = entryFor(install);
|
|
226
241
|
// The global `wazap` bin is a symlink into the package, and launchd's PATH has
|
|
227
|
-
// neither it nor npx,
|
|
228
|
-
|
|
242
|
+
// neither it, nor npx, nor the bare `node` a checkout entry would name, so a
|
|
243
|
+
// GUI client gets this Node and the script behind it.
|
|
244
|
+
if (spec.gui && (entry.command === "wazap" || install.kind === "checkout")) {
|
|
229
245
|
entry.command = process.execPath;
|
|
230
246
|
entry.args = [realpathSync(install.script)];
|
|
231
247
|
}
|
|
@@ -244,7 +260,11 @@ export function launchCheck(spec, entry, pathEnv = GUI_PATH, exists, platform =
|
|
|
244
260
|
if (platform !== "darwin")
|
|
245
261
|
return { name: "launch", state: "info", detail: "not checked on this platform" };
|
|
246
262
|
if (isAbsolute(entry.command) || commandOnPath(entry.command, pathEnv, exists)) {
|
|
247
|
-
return {
|
|
263
|
+
return {
|
|
264
|
+
name: "launch",
|
|
265
|
+
state: "ok",
|
|
266
|
+
detail: `${spec.describe} can start \`${entry.command}\` without your shell PATH`,
|
|
267
|
+
};
|
|
248
268
|
}
|
|
249
269
|
return {
|
|
250
270
|
name: "launch",
|
package/dist/deps.js
CHANGED
|
@@ -12,6 +12,7 @@ import { brand, info } from "./ui.js";
|
|
|
12
12
|
export const DEPS = {
|
|
13
13
|
whisper: { binary: "whisper-cli", brew: "whisper-cpp", why: "transcribes voice messages locally" },
|
|
14
14
|
ffmpeg: { binary: "ffmpeg", brew: "ffmpeg", why: "converts voice notes for whisper" },
|
|
15
|
+
llama: { binary: "llama-server", brew: "llama.cpp", why: "embeds messages for local semantic recall" },
|
|
15
16
|
tailscale: { binary: "tailscale", brew: "tailscale", why: "gives wazap a public https URL" },
|
|
16
17
|
cloudflared: { binary: "cloudflared", brew: "cloudflared", why: "gives wazap a public https URL" },
|
|
17
18
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { accessSync, constants, statSync } from "node:fs";
|
|
2
2
|
import { AccountRegistry, accountPolicy, anyAccountLinked, resolveAccount } from "./accounts.js";
|
|
3
3
|
import { readLinkedAccount } from "./auth-state.js";
|
|
4
|
-
import { WAZAP_VERSION, WRITES_ENABLE_FIX, WRITE_TOKEN_NOTE, accountPaths, isRemoteHttp, paths } from "./config.js";
|
|
4
|
+
import { WAZAP_VERSION, WRITES_ENABLE_FIX, WRITE_TOKEN_NOTE, accountPaths, isRemoteHttp, paths, } from "./config.js";
|
|
5
5
|
import { asWazapError } from "./errors.js";
|
|
6
6
|
import { lockHolder, lockPid } from "./lock.js";
|
|
7
7
|
import { oauthProblem, readGrants } from "./oauth.js";
|
|
8
|
+
import { EMBED_MODELS, embedModelPath, embedReady, readRecallSettings } from "./recall/index.js";
|
|
8
9
|
import { installedService } from "./service.js";
|
|
9
10
|
import { detectedTargets, skillState } from "./skills.js";
|
|
10
11
|
import { MODELS, findWhisper, localProvider, maskKey, modelPath, readTranscribeSettings, which, } from "./transcribe/index.js";
|
|
@@ -25,6 +26,7 @@ const CHECKS = [
|
|
|
25
26
|
checkSkills,
|
|
26
27
|
checkOAuth,
|
|
27
28
|
checkTranscribe,
|
|
29
|
+
checkRecall,
|
|
28
30
|
checkWebhook,
|
|
29
31
|
checkUpdate,
|
|
30
32
|
];
|
|
@@ -34,7 +36,7 @@ export async function runChecks(config) {
|
|
|
34
36
|
const checks = [];
|
|
35
37
|
for (const check of CHECKS)
|
|
36
38
|
checks.push(...[await check(config)].flat());
|
|
37
|
-
let linked
|
|
39
|
+
let linked;
|
|
38
40
|
try {
|
|
39
41
|
linked = anyAccountLinked(config.dataDir);
|
|
40
42
|
}
|
|
@@ -81,7 +83,12 @@ function checkDataDir(config) {
|
|
|
81
83
|
return { name: "data dir", state: "info", detail: `${dir} does not exist yet (login creates it)` };
|
|
82
84
|
}
|
|
83
85
|
if (!stat.isDirectory()) {
|
|
84
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
name: "data dir",
|
|
88
|
+
state: "fail",
|
|
89
|
+
detail: `${dir} is not a directory`,
|
|
90
|
+
fix: "move it aside or use --data-dir",
|
|
91
|
+
};
|
|
85
92
|
}
|
|
86
93
|
const mode = stat.mode & 0o777;
|
|
87
94
|
if (process.platform !== "win32" && mode !== 0o700) {
|
|
@@ -96,7 +103,12 @@ function checkDataDir(config) {
|
|
|
96
103
|
accessSync(dir, constants.W_OK);
|
|
97
104
|
}
|
|
98
105
|
catch {
|
|
99
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
name: "data dir",
|
|
108
|
+
state: "fail",
|
|
109
|
+
detail: `${dir} is not writable`,
|
|
110
|
+
fix: "fix its ownership or permissions",
|
|
111
|
+
};
|
|
100
112
|
}
|
|
101
113
|
return { name: "data dir", state: "ok", detail: `${dir} (0700, writable)` };
|
|
102
114
|
}
|
|
@@ -165,7 +177,11 @@ function checkCredentials(config) {
|
|
|
165
177
|
if (linkedIds.length === 0)
|
|
166
178
|
return { name: "credentials", state: "info", detail: "no account linked yet" };
|
|
167
179
|
// The number is deliberately absent: status is the thing people screenshot.
|
|
168
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
name: "credentials",
|
|
182
|
+
state: "ok",
|
|
183
|
+
detail: records.length > 1 ? `readable (${linkedIds.join(", ")})` : "readable",
|
|
184
|
+
};
|
|
169
185
|
}
|
|
170
186
|
function checkWrites(config) {
|
|
171
187
|
const selected = resolveAccount(config.dataDir, config.accountId);
|
|
@@ -278,6 +294,35 @@ async function localChecks(settings) {
|
|
|
278
294
|
: { name: "model", state: "ok", detail: `${spec.file} (${Math.round(size / MIB)} MiB)` },
|
|
279
295
|
];
|
|
280
296
|
}
|
|
297
|
+
const RECALL_OFF_FIX = "run `wazap config recall local` to search messages by meaning";
|
|
298
|
+
/**
|
|
299
|
+
* Off is quiet; on reports the sidecar binary and the model file, the two
|
|
300
|
+
* things `embed download` plus an install can repair.
|
|
301
|
+
*/
|
|
302
|
+
async function checkRecall(config) {
|
|
303
|
+
let settings;
|
|
304
|
+
try {
|
|
305
|
+
settings = readRecallSettings(process.env, config.dataDir);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
const failure = asWazapError(err);
|
|
309
|
+
return [{ name: "recall", state: "fail", detail: failure.message, fix: failure.fix }];
|
|
310
|
+
}
|
|
311
|
+
if (!settings.enabled)
|
|
312
|
+
return [{ name: "recall", state: "info", detail: "off", fix: RECALL_OFF_FIX }];
|
|
313
|
+
const spec = EMBED_MODELS[settings.model];
|
|
314
|
+
const size = fileSize(embedModelPath(settings.modelsDir, spec));
|
|
315
|
+
const readiness = await embedReady(settings, spec);
|
|
316
|
+
return [
|
|
317
|
+
{ name: "recall", state: "ok", detail: `local (${settings.model})` },
|
|
318
|
+
readiness.ok
|
|
319
|
+
? { name: "llama-server", state: "ok", detail: settings.embedUrl ?? "found" }
|
|
320
|
+
: { name: "llama-server", state: "fail", detail: readiness.detail, fix: readiness.fix },
|
|
321
|
+
size === null && settings.embedUrl === null
|
|
322
|
+
? { name: "embed model", state: "fail", detail: `${spec.file} is not downloaded`, fix: "run `wazap embed download`" }
|
|
323
|
+
: { name: "embed model", state: "ok", detail: `${spec.file} (${Math.round((size ?? 0) / MIB)} MiB)` },
|
|
324
|
+
];
|
|
325
|
+
}
|
|
281
326
|
/** W1 webhook: off is quiet; on without a URL or secret is a visible fail. */
|
|
282
327
|
export function webhookCheck(env = process.env) {
|
|
283
328
|
const settings = readWebhookSettings(env);
|
|
@@ -341,6 +386,11 @@ async function checkUpdate() {
|
|
|
341
386
|
if (latest === null)
|
|
342
387
|
return { name: "update", state: "info", detail: "update check skipped (no answer)" };
|
|
343
388
|
return isNewer(latest, WAZAP_VERSION)
|
|
344
|
-
? {
|
|
389
|
+
? {
|
|
390
|
+
name: "update",
|
|
391
|
+
state: "info",
|
|
392
|
+
detail: `${latest} is out (running ${WAZAP_VERSION})`,
|
|
393
|
+
fix: "run `wazap update`",
|
|
394
|
+
}
|
|
345
395
|
: { name: "update", state: "ok", detail: `${WAZAP_VERSION} is current` };
|
|
346
396
|
}
|
package/dist/drafts.js
CHANGED
|
@@ -142,6 +142,9 @@ function formatNumber(digits) {
|
|
|
142
142
|
const raw = digits.startsWith("+") ? digits.slice(1) : digits;
|
|
143
143
|
if (!/^\d+$/.test(raw) || raw.length < 4)
|
|
144
144
|
return digits.startsWith("+") ? digits : `+${digits}`;
|
|
145
|
-
const rest = raw
|
|
145
|
+
const rest = raw
|
|
146
|
+
.slice(2)
|
|
147
|
+
.match(/.{1,3}/g)
|
|
148
|
+
?.join(" ") ?? raw.slice(2);
|
|
146
149
|
return `+${raw.slice(0, 2)} ${rest}`;
|
|
147
150
|
}
|
package/dist/errors.js
CHANGED
|
@@ -29,6 +29,7 @@ export const ERROR_GUIDE = {
|
|
|
29
29
|
NOT_ADMIN: "The linked account is not an admin of that group, so this action is refused. Do not retry.",
|
|
30
30
|
GROUP_ANNOUNCEMENT_ONLY: "Only admins may post in that group. Do not retry.",
|
|
31
31
|
MEDIA_UNAVAILABLE: "The media expired on WhatsApp's servers or was never synced. Do not retry; ask the sender to resend.",
|
|
32
|
+
MEDIA_ACCESS_DENIED: "The URL is not a public http(s) address, or resolves to a private or internal one. Pass a public URL or download the file and use file_path.",
|
|
32
33
|
FILE_NOT_FOUND: "The local path does not exist on the machine running wazap. Check the path with the user.",
|
|
33
34
|
FILE_TOO_LARGE: "The file is too large. Chat media may be 100 MB; a profile picture may be 10 MB. Send a smaller file.",
|
|
34
35
|
INVALID_IMAGE: "The file is not a JPEG, PNG or WebP. Pass a photo via file_path or url; GIF, video and documents are refused.",
|
|
@@ -41,6 +42,8 @@ export const ERROR_GUIDE = {
|
|
|
41
42
|
RATE_LIMITED: "Too many writes too fast. Wait the number of seconds in the fix, then retry once.",
|
|
42
43
|
TRANSCRIBE_UNAVAILABLE: "Transcription is off, or its binaries or model are missing. Tell the user to run the command in the fix; do not retry.",
|
|
43
44
|
TRANSCRIBE_FAILED: "The transcription provider ran and failed. Read the message; retry once at most.",
|
|
45
|
+
RECALL_UNAVAILABLE: "Semantic recall is off, or llama.cpp or the embedding model is missing. Tell the user to run the command in the fix; do not retry.",
|
|
46
|
+
RECALL_FAILED: "The embedding backend ran and failed. Read the message; retry once at most.",
|
|
44
47
|
TIMEOUT: "WhatsApp did not answer in time. Retry once; if it fails again, call get_status.",
|
|
45
48
|
SERVICE_ERROR: "wazap's own background service could not be managed. This is a machine problem, not a WhatsApp one: read the fix and tell the user.",
|
|
46
49
|
DRAFT_NOT_FOUND: "That draft_id is unknown or was already sent. Call the send tool again to draft, show the new preview, then confirm_send.",
|
package/dist/gif.js
CHANGED
|
@@ -25,11 +25,18 @@ export async function gifToMp4(gif) {
|
|
|
25
25
|
const output = join(dir, "out.mp4");
|
|
26
26
|
await writeFile(input, gif);
|
|
27
27
|
const args = [
|
|
28
|
-
"-nostdin",
|
|
29
|
-
"-
|
|
30
|
-
"
|
|
31
|
-
"-
|
|
32
|
-
"-
|
|
28
|
+
"-nostdin",
|
|
29
|
+
"-loglevel",
|
|
30
|
+
"error",
|
|
31
|
+
"-y",
|
|
32
|
+
"-i",
|
|
33
|
+
input,
|
|
34
|
+
"-movflags",
|
|
35
|
+
"faststart",
|
|
36
|
+
"-pix_fmt",
|
|
37
|
+
"yuv420p",
|
|
38
|
+
"-vf",
|
|
39
|
+
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
33
40
|
"-an",
|
|
34
41
|
output,
|
|
35
42
|
];
|
package/dist/ids.js
CHANGED
|
@@ -2,7 +2,10 @@ import { WazapError } from "./errors.js";
|
|
|
2
2
|
const PHONE_EXAMPLE = "Use international format, e.g. +15550100";
|
|
3
3
|
/** Digits of a phone number in international format, or INVALID_PHONE. */
|
|
4
4
|
export function normalizePhone(input) {
|
|
5
|
-
const digits = input
|
|
5
|
+
const digits = input
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/^\+/, "")
|
|
8
|
+
.replace(/[\s\-().]/g, "");
|
|
6
9
|
if (!/^\d+$/.test(digits) || digits.startsWith("0") || digits.length < 8 || digits.length > 15) {
|
|
7
10
|
throw new WazapError("INVALID_PHONE", `"${input.trim()}" is not a phone number in international format.`, PHONE_EXAMPLE);
|
|
8
11
|
}
|