@rubytech/create-maxy 1.0.460 → 1.0.462
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/package.json +1 -1
- package/payload/maxy/server.js +333 -322
- package/payload/platform/plugins/admin/skills/skill-builder/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/skill-builder/references/pdf-generation.md +30 -0
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +1 -1
- package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +1 -1
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +40 -18
- package/payload/platform/templates/specialists/agents/browser-specialist.md +1 -1
package/package.json
CHANGED
package/payload/maxy/server.js
CHANGED
|
@@ -5102,7 +5102,7 @@ function getMcpServers(accountId, enabledPlugins) {
|
|
|
5102
5102
|
// the always-on Chromium instance started by vnc.sh on display :99.
|
|
5103
5103
|
"plugin_playwright_playwright": {
|
|
5104
5104
|
command: "npx",
|
|
5105
|
-
args: ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://127.0.0.1:9222"]
|
|
5105
|
+
args: ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://127.0.0.1:9222", "--caps", "pdf"]
|
|
5106
5106
|
}
|
|
5107
5107
|
};
|
|
5108
5108
|
if (process.env.TELEGRAM_PUBLIC_BOT_TOKEN) {
|
|
@@ -23691,6 +23691,282 @@ var WhatsAppConfigSchema = external_exports.object({
|
|
|
23691
23691
|
});
|
|
23692
23692
|
});
|
|
23693
23693
|
|
|
23694
|
+
// app/lib/whatsapp/config-persist.ts
|
|
23695
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "fs";
|
|
23696
|
+
import { resolve as resolve9, join as join5 } from "path";
|
|
23697
|
+
var TAG4 = "[whatsapp:config]";
|
|
23698
|
+
function configPath(accountDir) {
|
|
23699
|
+
return resolve9(accountDir, "account.json");
|
|
23700
|
+
}
|
|
23701
|
+
function readConfig(accountDir) {
|
|
23702
|
+
const path2 = configPath(accountDir);
|
|
23703
|
+
if (!existsSync10(path2)) throw new Error(`account.json not found at ${path2}`);
|
|
23704
|
+
return JSON.parse(readFileSync10(path2, "utf-8"));
|
|
23705
|
+
}
|
|
23706
|
+
function writeConfig(accountDir, config2) {
|
|
23707
|
+
const path2 = configPath(accountDir);
|
|
23708
|
+
writeFileSync6(path2, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
23709
|
+
}
|
|
23710
|
+
function reloadManagerConfig(accountDir) {
|
|
23711
|
+
try {
|
|
23712
|
+
const config2 = readConfig(accountDir);
|
|
23713
|
+
reloadConfig(config2);
|
|
23714
|
+
console.error(`${TAG4} reloaded manager config`);
|
|
23715
|
+
} catch (err) {
|
|
23716
|
+
console.error(`${TAG4} manager config reload failed: ${String(err)}`);
|
|
23717
|
+
}
|
|
23718
|
+
}
|
|
23719
|
+
var E164_PATTERN = /^\+\d{7,15}$/;
|
|
23720
|
+
function migrateAdminPhones(wa) {
|
|
23721
|
+
if (wa.adminPhones !== void 0) return 0;
|
|
23722
|
+
if (!Array.isArray(wa.allowFrom)) return 0;
|
|
23723
|
+
const allowFrom = wa.allowFrom;
|
|
23724
|
+
const phones = [];
|
|
23725
|
+
const remaining = [];
|
|
23726
|
+
for (const entry of allowFrom) {
|
|
23727
|
+
if (E164_PATTERN.test(entry)) {
|
|
23728
|
+
phones.push(entry);
|
|
23729
|
+
} else {
|
|
23730
|
+
remaining.push(entry);
|
|
23731
|
+
}
|
|
23732
|
+
}
|
|
23733
|
+
if (phones.length === 0) return 0;
|
|
23734
|
+
wa.adminPhones = phones;
|
|
23735
|
+
wa.allowFrom = remaining;
|
|
23736
|
+
console.error(`${TAG4} migrated ${phones.length} phone(s) from allowFrom to adminPhones`);
|
|
23737
|
+
return phones.length;
|
|
23738
|
+
}
|
|
23739
|
+
function persistAfterPairing(accountDir, accountId, selfPhone) {
|
|
23740
|
+
try {
|
|
23741
|
+
const config2 = readConfig(accountDir);
|
|
23742
|
+
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
23743
|
+
config2.whatsapp = {};
|
|
23744
|
+
}
|
|
23745
|
+
const wa = config2.whatsapp;
|
|
23746
|
+
if (!wa.accounts || typeof wa.accounts !== "object") {
|
|
23747
|
+
wa.accounts = {};
|
|
23748
|
+
}
|
|
23749
|
+
const accounts = wa.accounts;
|
|
23750
|
+
if (!accounts[accountId] || typeof accounts[accountId] !== "object") {
|
|
23751
|
+
accounts[accountId] = { name: "Main" };
|
|
23752
|
+
}
|
|
23753
|
+
if (selfPhone) {
|
|
23754
|
+
const normalized = selfPhone.startsWith("+") ? selfPhone : `+${selfPhone}`;
|
|
23755
|
+
if (!Array.isArray(wa.adminPhones)) {
|
|
23756
|
+
wa.adminPhones = [];
|
|
23757
|
+
}
|
|
23758
|
+
const adminPhones = wa.adminPhones;
|
|
23759
|
+
if (!adminPhones.includes(normalized)) {
|
|
23760
|
+
adminPhones.push(normalized);
|
|
23761
|
+
console.error(`${TAG4} added selfPhone=${normalized} to adminPhones`);
|
|
23762
|
+
}
|
|
23763
|
+
} else {
|
|
23764
|
+
console.error(`${TAG4} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
|
|
23765
|
+
}
|
|
23766
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23767
|
+
if (!parsed.success) {
|
|
23768
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
23769
|
+
console.error(`${TAG4} validation failed after pairing: ${msg}`);
|
|
23770
|
+
return { ok: false, error: `Validation failed: ${msg}` };
|
|
23771
|
+
}
|
|
23772
|
+
config2.whatsapp = parsed.data;
|
|
23773
|
+
writeConfig(accountDir, config2);
|
|
23774
|
+
console.error(`${TAG4} persisted after pairing account=${accountId} phone=${selfPhone ?? "null"}`);
|
|
23775
|
+
reloadManagerConfig(accountDir);
|
|
23776
|
+
return { ok: true };
|
|
23777
|
+
} catch (err) {
|
|
23778
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23779
|
+
console.error(`${TAG4} persist failed account=${accountId}: ${msg}`);
|
|
23780
|
+
return { ok: false, error: msg };
|
|
23781
|
+
}
|
|
23782
|
+
}
|
|
23783
|
+
function addAdminPhone(accountDir, phone) {
|
|
23784
|
+
const normalized = phone.trim();
|
|
23785
|
+
if (!E164_PATTERN.test(normalized)) {
|
|
23786
|
+
return { ok: false, error: `Invalid phone format "${normalized}". Expected E.164 (e.g. +441234567890).` };
|
|
23787
|
+
}
|
|
23788
|
+
try {
|
|
23789
|
+
const config2 = readConfig(accountDir);
|
|
23790
|
+
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
23791
|
+
config2.whatsapp = {};
|
|
23792
|
+
}
|
|
23793
|
+
const wa = config2.whatsapp;
|
|
23794
|
+
migrateAdminPhones(wa);
|
|
23795
|
+
if (!Array.isArray(wa.adminPhones)) {
|
|
23796
|
+
wa.adminPhones = [];
|
|
23797
|
+
}
|
|
23798
|
+
const adminPhones = wa.adminPhones;
|
|
23799
|
+
if (adminPhones.includes(normalized)) {
|
|
23800
|
+
return { ok: true, message: `Phone ${normalized} is already in the admin list.` };
|
|
23801
|
+
}
|
|
23802
|
+
adminPhones.push(normalized);
|
|
23803
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23804
|
+
if (!parsed.success) {
|
|
23805
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
23806
|
+
return { ok: false, error: `Validation failed: ${msg}` };
|
|
23807
|
+
}
|
|
23808
|
+
config2.whatsapp = parsed.data;
|
|
23809
|
+
writeConfig(accountDir, config2);
|
|
23810
|
+
console.error(`${TAG4} added admin phone=${normalized}`);
|
|
23811
|
+
reloadManagerConfig(accountDir);
|
|
23812
|
+
return { ok: true, message: `Added ${normalized} as admin phone. Messages from this number will route to the admin agent.` };
|
|
23813
|
+
} catch (err) {
|
|
23814
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23815
|
+
console.error(`${TAG4} addAdminPhone failed: ${msg}`);
|
|
23816
|
+
return { ok: false, error: msg };
|
|
23817
|
+
}
|
|
23818
|
+
}
|
|
23819
|
+
function removeAdminPhone(accountDir, phone) {
|
|
23820
|
+
const normalized = phone.trim();
|
|
23821
|
+
try {
|
|
23822
|
+
const config2 = readConfig(accountDir);
|
|
23823
|
+
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
23824
|
+
return { ok: true, message: `No WhatsApp config exists \u2014 nothing to remove.` };
|
|
23825
|
+
}
|
|
23826
|
+
const wa = config2.whatsapp;
|
|
23827
|
+
migrateAdminPhones(wa);
|
|
23828
|
+
if (!Array.isArray(wa.adminPhones)) {
|
|
23829
|
+
return { ok: true, message: `No admin phones configured \u2014 nothing to remove.` };
|
|
23830
|
+
}
|
|
23831
|
+
const adminPhones = wa.adminPhones;
|
|
23832
|
+
const idx = adminPhones.indexOf(normalized);
|
|
23833
|
+
if (idx === -1) {
|
|
23834
|
+
return { ok: true, message: `Phone ${normalized} is not in the admin list.` };
|
|
23835
|
+
}
|
|
23836
|
+
adminPhones.splice(idx, 1);
|
|
23837
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23838
|
+
if (!parsed.success) {
|
|
23839
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
23840
|
+
return { ok: false, error: `Validation failed: ${msg}` };
|
|
23841
|
+
}
|
|
23842
|
+
config2.whatsapp = parsed.data;
|
|
23843
|
+
writeConfig(accountDir, config2);
|
|
23844
|
+
console.error(`${TAG4} removed admin phone=${normalized}`);
|
|
23845
|
+
reloadManagerConfig(accountDir);
|
|
23846
|
+
return { ok: true, message: `Removed ${normalized} from admin phones. Messages from this number will now route to the public agent.` };
|
|
23847
|
+
} catch (err) {
|
|
23848
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23849
|
+
console.error(`${TAG4} removeAdminPhone failed: ${msg}`);
|
|
23850
|
+
return { ok: false, error: msg };
|
|
23851
|
+
}
|
|
23852
|
+
}
|
|
23853
|
+
function readAdminPhones(accountDir) {
|
|
23854
|
+
try {
|
|
23855
|
+
const config2 = readConfig(accountDir);
|
|
23856
|
+
const wa = config2.whatsapp;
|
|
23857
|
+
if (!wa) return [];
|
|
23858
|
+
const migrated = migrateAdminPhones(wa);
|
|
23859
|
+
if (migrated > 0) {
|
|
23860
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23861
|
+
if (parsed.success) {
|
|
23862
|
+
config2.whatsapp = parsed.data;
|
|
23863
|
+
writeConfig(accountDir, config2);
|
|
23864
|
+
reloadManagerConfig(accountDir);
|
|
23865
|
+
}
|
|
23866
|
+
}
|
|
23867
|
+
if (!Array.isArray(wa.adminPhones)) return [];
|
|
23868
|
+
return wa.adminPhones.filter((p) => typeof p === "string");
|
|
23869
|
+
} catch {
|
|
23870
|
+
return [];
|
|
23871
|
+
}
|
|
23872
|
+
}
|
|
23873
|
+
function setPublicAgent(accountDir, slug) {
|
|
23874
|
+
const trimmed = slug.trim();
|
|
23875
|
+
if (!trimmed) {
|
|
23876
|
+
return { ok: false, error: "Agent slug cannot be empty." };
|
|
23877
|
+
}
|
|
23878
|
+
const agentConfigPath = join5(accountDir, "agents", trimmed, "config.json");
|
|
23879
|
+
if (!existsSync10(agentConfigPath)) {
|
|
23880
|
+
return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
23881
|
+
}
|
|
23882
|
+
try {
|
|
23883
|
+
const config2 = readConfig(accountDir);
|
|
23884
|
+
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
23885
|
+
config2.whatsapp = {};
|
|
23886
|
+
}
|
|
23887
|
+
const wa = config2.whatsapp;
|
|
23888
|
+
wa.publicAgent = trimmed;
|
|
23889
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23890
|
+
if (!parsed.success) {
|
|
23891
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
23892
|
+
return { ok: false, error: `Validation failed: ${msg}` };
|
|
23893
|
+
}
|
|
23894
|
+
config2.whatsapp = parsed.data;
|
|
23895
|
+
writeConfig(accountDir, config2);
|
|
23896
|
+
console.error(`${TAG4} publicAgent set to ${trimmed}`);
|
|
23897
|
+
reloadManagerConfig(accountDir);
|
|
23898
|
+
return { ok: true, message: `Public agent set to "${trimmed}". WhatsApp messages from non-admin phones will be handled by this agent.` };
|
|
23899
|
+
} catch (err) {
|
|
23900
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23901
|
+
console.error(`${TAG4} setPublicAgent failed: ${msg}`);
|
|
23902
|
+
return { ok: false, error: msg };
|
|
23903
|
+
}
|
|
23904
|
+
}
|
|
23905
|
+
function getPublicAgent(accountDir) {
|
|
23906
|
+
try {
|
|
23907
|
+
const config2 = readConfig(accountDir);
|
|
23908
|
+
const wa = config2.whatsapp;
|
|
23909
|
+
if (!wa) return null;
|
|
23910
|
+
const slug = wa.publicAgent;
|
|
23911
|
+
if (typeof slug === "string" && slug.trim()) {
|
|
23912
|
+
return slug.trim();
|
|
23913
|
+
}
|
|
23914
|
+
return null;
|
|
23915
|
+
} catch {
|
|
23916
|
+
return null;
|
|
23917
|
+
}
|
|
23918
|
+
}
|
|
23919
|
+
function updateConfig(accountDir, fields) {
|
|
23920
|
+
const fieldNames = Object.keys(fields);
|
|
23921
|
+
if (fieldNames.length === 0) {
|
|
23922
|
+
return { ok: false, error: "No fields provided to update." };
|
|
23923
|
+
}
|
|
23924
|
+
try {
|
|
23925
|
+
const config2 = readConfig(accountDir);
|
|
23926
|
+
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
23927
|
+
config2.whatsapp = {};
|
|
23928
|
+
}
|
|
23929
|
+
const wa = config2.whatsapp;
|
|
23930
|
+
migrateAdminPhones(wa);
|
|
23931
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
23932
|
+
wa[key] = value;
|
|
23933
|
+
}
|
|
23934
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23935
|
+
if (!parsed.success) {
|
|
23936
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
23937
|
+
console.error(`${TAG4} update validation failed: ${msg}`);
|
|
23938
|
+
return { ok: false, error: `Validation failed: ${msg}` };
|
|
23939
|
+
}
|
|
23940
|
+
config2.whatsapp = parsed.data;
|
|
23941
|
+
writeConfig(accountDir, config2);
|
|
23942
|
+
console.error(`${TAG4} updated fields=[${fieldNames.join(",")}]`);
|
|
23943
|
+
reloadManagerConfig(accountDir);
|
|
23944
|
+
return { ok: true, message: `Updated WhatsApp config: ${fieldNames.join(", ")}.` };
|
|
23945
|
+
} catch (err) {
|
|
23946
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23947
|
+
console.error(`${TAG4} updateConfig failed: ${msg}`);
|
|
23948
|
+
return { ok: false, error: msg };
|
|
23949
|
+
}
|
|
23950
|
+
}
|
|
23951
|
+
function getConfig(accountDir) {
|
|
23952
|
+
try {
|
|
23953
|
+
const config2 = readConfig(accountDir);
|
|
23954
|
+
const wa = config2.whatsapp;
|
|
23955
|
+
if (!wa) return null;
|
|
23956
|
+
const migrated = migrateAdminPhones(wa);
|
|
23957
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
23958
|
+
if (!parsed.success) return wa;
|
|
23959
|
+
if (migrated > 0) {
|
|
23960
|
+
config2.whatsapp = parsed.data;
|
|
23961
|
+
writeConfig(accountDir, config2);
|
|
23962
|
+
reloadManagerConfig(accountDir);
|
|
23963
|
+
}
|
|
23964
|
+
return parsed.data;
|
|
23965
|
+
} catch {
|
|
23966
|
+
return null;
|
|
23967
|
+
}
|
|
23968
|
+
}
|
|
23969
|
+
|
|
23694
23970
|
// app/lib/whatsapp/normalize.ts
|
|
23695
23971
|
var WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
|
|
23696
23972
|
var WHATSAPP_LID_RE = /^(\d+)@lid$/i;
|
|
@@ -23976,7 +24252,7 @@ function isDuplicateInbound(key) {
|
|
|
23976
24252
|
}
|
|
23977
24253
|
|
|
23978
24254
|
// app/lib/whatsapp/outbound/send.ts
|
|
23979
|
-
var
|
|
24255
|
+
var TAG5 = "[whatsapp:outbound]";
|
|
23980
24256
|
async function sendTextMessage(sock, to, text, opts) {
|
|
23981
24257
|
try {
|
|
23982
24258
|
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
@@ -23988,11 +24264,11 @@ async function sendTextMessage(sock, to, text, opts) {
|
|
|
23988
24264
|
const messageId = result?.key?.id;
|
|
23989
24265
|
if (messageId) {
|
|
23990
24266
|
trackAgentSentMessage(messageId);
|
|
23991
|
-
console.error(`${
|
|
24267
|
+
console.error(`${TAG5} sent text to=${jid} id=${messageId}`);
|
|
23992
24268
|
}
|
|
23993
24269
|
return { success: true, messageId: messageId ?? void 0 };
|
|
23994
24270
|
} catch (err) {
|
|
23995
|
-
console.error(`${
|
|
24271
|
+
console.error(`${TAG5} send failed to=${to}: ${String(err)}`);
|
|
23996
24272
|
return { success: false, error: String(err) };
|
|
23997
24273
|
}
|
|
23998
24274
|
}
|
|
@@ -24012,13 +24288,13 @@ async function sendReadReceipt(sock, chatJid, messageIds, participant) {
|
|
|
24012
24288
|
// app/lib/whatsapp/inbound/media.ts
|
|
24013
24289
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
24014
24290
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
24015
|
-
import { join as
|
|
24291
|
+
import { join as join6 } from "path";
|
|
24016
24292
|
import {
|
|
24017
24293
|
downloadMediaMessage,
|
|
24018
24294
|
downloadContentFromMessage,
|
|
24019
24295
|
normalizeMessageContent as normalizeMessageContent2
|
|
24020
24296
|
} from "@whiskeysockets/baileys";
|
|
24021
|
-
var
|
|
24297
|
+
var TAG6 = "[whatsapp:media]";
|
|
24022
24298
|
var MEDIA_DIR = "/tmp/maxy-media";
|
|
24023
24299
|
function mimeToExt(mimetype) {
|
|
24024
24300
|
const map2 = {
|
|
@@ -24074,47 +24350,47 @@ async function downloadInboundMedia(msg, sock, opts) {
|
|
|
24074
24350
|
}
|
|
24075
24351
|
);
|
|
24076
24352
|
if (!buffer || buffer.length === 0) {
|
|
24077
|
-
console.error(`${
|
|
24353
|
+
console.error(`${TAG6} primary download returned empty, trying direct fallback`);
|
|
24078
24354
|
const downloadable = getDownloadableContent(content);
|
|
24079
24355
|
if (downloadable) {
|
|
24080
24356
|
try {
|
|
24081
24357
|
const stream = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
|
|
24082
24358
|
buffer = await streamToBuffer(stream);
|
|
24083
24359
|
} catch (fallbackErr) {
|
|
24084
|
-
console.error(`${
|
|
24360
|
+
console.error(`${TAG6} direct download fallback failed: ${String(fallbackErr)}`);
|
|
24085
24361
|
}
|
|
24086
24362
|
}
|
|
24087
24363
|
}
|
|
24088
24364
|
if (!buffer || buffer.length === 0) {
|
|
24089
|
-
console.error(`${
|
|
24365
|
+
console.error(`${TAG6} download failed: empty buffer for ${mimetype ?? "unknown"}`);
|
|
24090
24366
|
return void 0;
|
|
24091
24367
|
}
|
|
24092
24368
|
if (buffer.length > maxBytes) {
|
|
24093
24369
|
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
|
|
24094
24370
|
const limitMB = (maxBytes / (1024 * 1024)).toFixed(0);
|
|
24095
|
-
console.error(`${
|
|
24371
|
+
console.error(`${TAG6} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
|
|
24096
24372
|
return void 0;
|
|
24097
24373
|
}
|
|
24098
24374
|
await mkdir2(MEDIA_DIR, { recursive: true });
|
|
24099
24375
|
const ext = mimeToExt(mimetype ?? "application/octet-stream");
|
|
24100
24376
|
const filename = `${randomUUID6()}.${ext}`;
|
|
24101
|
-
const filePath =
|
|
24377
|
+
const filePath = join6(MEDIA_DIR, filename);
|
|
24102
24378
|
await writeFile2(filePath, buffer);
|
|
24103
24379
|
const sizeKB = (buffer.length / 1024).toFixed(0);
|
|
24104
|
-
console.error(`${
|
|
24380
|
+
console.error(`${TAG6} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
|
|
24105
24381
|
return {
|
|
24106
24382
|
path: filePath,
|
|
24107
24383
|
mimetype: mimetype ?? "application/octet-stream",
|
|
24108
24384
|
size: buffer.length
|
|
24109
24385
|
};
|
|
24110
24386
|
} catch (err) {
|
|
24111
|
-
console.error(`${
|
|
24387
|
+
console.error(`${TAG6} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
|
|
24112
24388
|
return void 0;
|
|
24113
24389
|
}
|
|
24114
24390
|
}
|
|
24115
24391
|
|
|
24116
24392
|
// app/lib/whatsapp/inbound/debounce.ts
|
|
24117
|
-
var
|
|
24393
|
+
var TAG7 = "[whatsapp:debounce]";
|
|
24118
24394
|
function createInboundDebouncer(opts) {
|
|
24119
24395
|
const { debounceMs, buildKey, onFlush, onError } = opts;
|
|
24120
24396
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -24126,7 +24402,7 @@ function createInboundDebouncer(opts) {
|
|
|
24126
24402
|
pending.delete(key);
|
|
24127
24403
|
const batchSize = batch.entries.length;
|
|
24128
24404
|
try {
|
|
24129
|
-
console.error(`${
|
|
24405
|
+
console.error(`${TAG7} debounce flush key=${key} batchSize=${batchSize}`);
|
|
24130
24406
|
const result = onFlush(batch.entries);
|
|
24131
24407
|
if (result && typeof result.catch === "function") {
|
|
24132
24408
|
result.catch(onError);
|
|
@@ -24184,7 +24460,7 @@ function createInboundDebouncer(opts) {
|
|
|
24184
24460
|
}
|
|
24185
24461
|
|
|
24186
24462
|
// app/lib/whatsapp/manager.ts
|
|
24187
|
-
var
|
|
24463
|
+
var TAG8 = "[whatsapp:manager]";
|
|
24188
24464
|
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
24189
24465
|
var connections = /* @__PURE__ */ new Map();
|
|
24190
24466
|
var configDir = null;
|
|
@@ -24193,7 +24469,7 @@ var onInboundMessage = null;
|
|
|
24193
24469
|
var initialized = false;
|
|
24194
24470
|
async function init(opts) {
|
|
24195
24471
|
if (initialized) {
|
|
24196
|
-
console.error(`${
|
|
24472
|
+
console.error(`${TAG8} already initialized`);
|
|
24197
24473
|
return;
|
|
24198
24474
|
}
|
|
24199
24475
|
configDir = opts.configDir;
|
|
@@ -24201,20 +24477,20 @@ async function init(opts) {
|
|
|
24201
24477
|
loadConfig(opts.accountConfig);
|
|
24202
24478
|
const accountIds = listCredentialAccountIds(configDir);
|
|
24203
24479
|
if (accountIds.length === 0) {
|
|
24204
|
-
console.error(`${
|
|
24480
|
+
console.error(`${TAG8} init: no stored WhatsApp credentials found`);
|
|
24205
24481
|
initialized = true;
|
|
24206
24482
|
return;
|
|
24207
24483
|
}
|
|
24208
|
-
console.error(`${
|
|
24484
|
+
console.error(`${TAG8} init: found ${accountIds.length} credentialed account(s): ${accountIds.join(", ")}`);
|
|
24209
24485
|
initialized = true;
|
|
24210
24486
|
for (const accountId of accountIds) {
|
|
24211
24487
|
const accountCfg = whatsAppConfig.accounts?.[accountId];
|
|
24212
24488
|
if (accountCfg?.enabled === false) {
|
|
24213
|
-
console.error(`${
|
|
24489
|
+
console.error(`${TAG8} skipping disabled account=${accountId}`);
|
|
24214
24490
|
continue;
|
|
24215
24491
|
}
|
|
24216
24492
|
startConnection(accountId).catch((err) => {
|
|
24217
|
-
console.error(`${
|
|
24493
|
+
console.error(`${TAG8} failed to auto-start account=${accountId}: ${formatError(err)}`);
|
|
24218
24494
|
});
|
|
24219
24495
|
}
|
|
24220
24496
|
}
|
|
@@ -24223,7 +24499,7 @@ async function startConnection(accountId) {
|
|
|
24223
24499
|
const authDir = resolveAuthDir(configDir, accountId);
|
|
24224
24500
|
const hasAuth = await authExists(authDir);
|
|
24225
24501
|
if (!hasAuth) {
|
|
24226
|
-
console.error(`${
|
|
24502
|
+
console.error(`${TAG8} no credentials for account=${accountId}`);
|
|
24227
24503
|
return;
|
|
24228
24504
|
}
|
|
24229
24505
|
await stopConnection(accountId);
|
|
@@ -24258,11 +24534,11 @@ async function stopConnection(accountId) {
|
|
|
24258
24534
|
conn.sock.ev.removeAllListeners("creds.update");
|
|
24259
24535
|
conn.sock.ws?.close?.();
|
|
24260
24536
|
} catch (err) {
|
|
24261
|
-
console.warn(`${
|
|
24537
|
+
console.warn(`${TAG8} socket cleanup error during stop account=${accountId}: ${String(err)}`);
|
|
24262
24538
|
}
|
|
24263
24539
|
}
|
|
24264
24540
|
connections.delete(accountId);
|
|
24265
|
-
console.error(`${
|
|
24541
|
+
console.error(`${TAG8} stopped account=${accountId}`);
|
|
24266
24542
|
}
|
|
24267
24543
|
function getStatus() {
|
|
24268
24544
|
return Array.from(connections.values()).map((conn) => ({
|
|
@@ -24301,13 +24577,13 @@ async function registerLoginSocket(accountId, sock, authDir) {
|
|
|
24301
24577
|
connections.set(accountId, conn);
|
|
24302
24578
|
try {
|
|
24303
24579
|
await sock.sendPresenceUpdate("available");
|
|
24304
|
-
console.error(`${
|
|
24580
|
+
console.error(`${TAG8} presence set to available account=${accountId}`);
|
|
24305
24581
|
} catch (err) {
|
|
24306
|
-
console.error(`${
|
|
24582
|
+
console.error(`${TAG8} presence update failed account=${accountId}: ${String(err)}`);
|
|
24307
24583
|
}
|
|
24308
24584
|
monitorInbound(conn);
|
|
24309
24585
|
watchForDisconnect(conn);
|
|
24310
|
-
console.error(`${
|
|
24586
|
+
console.error(`${TAG8} registered login socket for account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
|
|
24311
24587
|
}
|
|
24312
24588
|
function reloadConfig(accountConfig) {
|
|
24313
24589
|
loadConfig(accountConfig);
|
|
@@ -24317,22 +24593,24 @@ async function shutdown() {
|
|
|
24317
24593
|
const ids = Array.from(connections.keys());
|
|
24318
24594
|
await Promise.all(ids.map((id) => stopConnection(id)));
|
|
24319
24595
|
initialized = false;
|
|
24320
|
-
console.error(`${
|
|
24596
|
+
console.error(`${TAG8} shutdown complete`);
|
|
24321
24597
|
}
|
|
24322
24598
|
function loadConfig(accountConfig) {
|
|
24323
24599
|
try {
|
|
24324
24600
|
const raw2 = accountConfig?.whatsapp;
|
|
24325
24601
|
if (raw2 && typeof raw2 === "object") {
|
|
24326
|
-
const
|
|
24602
|
+
const wa = raw2;
|
|
24603
|
+
migrateAdminPhones(wa);
|
|
24604
|
+
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24327
24605
|
if (parsed.success) {
|
|
24328
24606
|
whatsAppConfig = parsed.data;
|
|
24329
24607
|
} else {
|
|
24330
|
-
console.error(`${
|
|
24608
|
+
console.error(`${TAG8} config validation failed: ${parsed.error.message}`);
|
|
24331
24609
|
whatsAppConfig = {};
|
|
24332
24610
|
}
|
|
24333
24611
|
}
|
|
24334
24612
|
} catch (err) {
|
|
24335
|
-
console.error(`${
|
|
24613
|
+
console.error(`${TAG8} config load error: ${String(err)}`);
|
|
24336
24614
|
whatsAppConfig = {};
|
|
24337
24615
|
}
|
|
24338
24616
|
}
|
|
@@ -24340,13 +24618,13 @@ async function connectWithReconnect(conn) {
|
|
|
24340
24618
|
const maxAttempts = MAX_RECONNECT_ATTEMPTS;
|
|
24341
24619
|
while (!conn.abortController.signal.aborted) {
|
|
24342
24620
|
try {
|
|
24343
|
-
console.error(`${
|
|
24621
|
+
console.error(`${TAG8} connecting account=${conn.accountId} attempt=${conn.reconnectAttempts}`);
|
|
24344
24622
|
const sock = await createWaSocket({
|
|
24345
24623
|
authDir: conn.authDir,
|
|
24346
24624
|
silent: true
|
|
24347
24625
|
});
|
|
24348
24626
|
conn.sock = sock;
|
|
24349
|
-
console.error(`${
|
|
24627
|
+
console.error(`${TAG8} socket created account=${conn.accountId} \u2014 waiting for connection`);
|
|
24350
24628
|
await waitForConnection(sock);
|
|
24351
24629
|
const selfId = readSelfId(conn.authDir);
|
|
24352
24630
|
conn.connected = true;
|
|
@@ -24356,12 +24634,12 @@ async function connectWithReconnect(conn) {
|
|
|
24356
24634
|
conn.reconnectAttempts = 0;
|
|
24357
24635
|
conn.lastError = void 0;
|
|
24358
24636
|
conn.lidMapping = sock.signalRepository?.lidMapping ?? null;
|
|
24359
|
-
console.error(`${
|
|
24637
|
+
console.error(`${TAG8} connected account=${conn.accountId} phone=${selfId.e164 ?? "unknown"}`);
|
|
24360
24638
|
try {
|
|
24361
24639
|
await sock.sendPresenceUpdate("available");
|
|
24362
|
-
console.error(`${
|
|
24640
|
+
console.error(`${TAG8} presence set to available account=${conn.accountId}`);
|
|
24363
24641
|
} catch (err) {
|
|
24364
|
-
console.error(`${
|
|
24642
|
+
console.error(`${TAG8} presence update failed account=${conn.accountId}: ${String(err)}`);
|
|
24365
24643
|
}
|
|
24366
24644
|
if (conn.debouncer) {
|
|
24367
24645
|
conn.debouncer.destroy();
|
|
@@ -24376,19 +24654,19 @@ async function connectWithReconnect(conn) {
|
|
|
24376
24654
|
conn.sock = null;
|
|
24377
24655
|
const classification = classifyDisconnect(err);
|
|
24378
24656
|
conn.lastError = classification.message;
|
|
24379
|
-
console.error(`${
|
|
24657
|
+
console.error(`${TAG8} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
|
|
24380
24658
|
if (!classification.shouldRetry) {
|
|
24381
|
-
console.error(`${
|
|
24659
|
+
console.error(`${TAG8} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
|
|
24382
24660
|
return;
|
|
24383
24661
|
}
|
|
24384
24662
|
conn.reconnectAttempts++;
|
|
24385
24663
|
if (conn.reconnectAttempts > maxAttempts) {
|
|
24386
|
-
console.error(`${
|
|
24664
|
+
console.error(`${TAG8} max retries exceeded for account=${conn.accountId}`);
|
|
24387
24665
|
conn.lastError = `Max reconnect attempts (${maxAttempts}) exceeded`;
|
|
24388
24666
|
return;
|
|
24389
24667
|
}
|
|
24390
24668
|
const delay = computeBackoff(conn.reconnectAttempts);
|
|
24391
|
-
console.error(`${
|
|
24669
|
+
console.error(`${TAG8} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${conn.reconnectAttempts}/${maxAttempts})`);
|
|
24392
24670
|
await new Promise((resolve17) => {
|
|
24393
24671
|
const timer = setTimeout(resolve17, delay);
|
|
24394
24672
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
@@ -24418,11 +24696,11 @@ function watchForDisconnect(conn) {
|
|
|
24418
24696
|
conn.sock.ev.on("connection.update", (update) => {
|
|
24419
24697
|
if (update.connection === "close") {
|
|
24420
24698
|
if (connections.get(conn.accountId) !== conn) return;
|
|
24421
|
-
console.error(`${
|
|
24699
|
+
console.error(`${TAG8} socket disconnected for account=${conn.accountId}`);
|
|
24422
24700
|
conn.connected = false;
|
|
24423
24701
|
conn.sock = null;
|
|
24424
24702
|
connectWithReconnect(conn).catch((err) => {
|
|
24425
|
-
console.error(`${
|
|
24703
|
+
console.error(`${TAG8} reconnection failed for account=${conn.accountId}: ${formatError(err)}`);
|
|
24426
24704
|
});
|
|
24427
24705
|
}
|
|
24428
24706
|
});
|
|
@@ -24431,7 +24709,7 @@ function monitorInbound(conn) {
|
|
|
24431
24709
|
if (!conn.sock || !onInboundMessage) return;
|
|
24432
24710
|
const sock = conn.sock;
|
|
24433
24711
|
const debounceMs = whatsAppConfig.accounts?.[conn.accountId]?.debounceMs ?? whatsAppConfig.debounceMs ?? 0;
|
|
24434
|
-
console.error(`${
|
|
24712
|
+
console.error(`${TAG8} monitorInbound started account=${conn.accountId} debounceMs=${debounceMs}`);
|
|
24435
24713
|
conn.debouncer = createInboundDebouncer({
|
|
24436
24714
|
debounceMs,
|
|
24437
24715
|
buildKey: (payload) => {
|
|
@@ -24444,7 +24722,7 @@ function monitorInbound(conn) {
|
|
|
24444
24722
|
onInboundMessage(entries[0]);
|
|
24445
24723
|
return;
|
|
24446
24724
|
}
|
|
24447
|
-
console.error(`${
|
|
24725
|
+
console.error(`${TAG8} debounce: combining ${entries.length} messages account=${conn.accountId} from=${entries[0].senderPhone}`);
|
|
24448
24726
|
const last = entries[entries.length - 1];
|
|
24449
24727
|
const mediaEntry = entries.find((e) => e.mediaPath);
|
|
24450
24728
|
const combinedText = entries.map((e) => e.text).filter(Boolean).join("\n");
|
|
@@ -24457,7 +24735,7 @@ function monitorInbound(conn) {
|
|
|
24457
24735
|
});
|
|
24458
24736
|
},
|
|
24459
24737
|
onError: (err) => {
|
|
24460
|
-
console.error(`${
|
|
24738
|
+
console.error(`${TAG8} debounce flush error account=${conn.accountId}: ${String(err)}`);
|
|
24461
24739
|
}
|
|
24462
24740
|
});
|
|
24463
24741
|
sock.ev.on("messages.upsert", async (upsert) => {
|
|
@@ -24466,7 +24744,7 @@ function monitorInbound(conn) {
|
|
|
24466
24744
|
try {
|
|
24467
24745
|
await handleInboundMessage(conn, msg);
|
|
24468
24746
|
} catch (err) {
|
|
24469
|
-
console.error(`${
|
|
24747
|
+
console.error(`${TAG8} inbound handler error account=${conn.accountId}: ${String(err)}`);
|
|
24470
24748
|
}
|
|
24471
24749
|
}
|
|
24472
24750
|
});
|
|
@@ -24476,31 +24754,31 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24476
24754
|
const remoteJid = msg.key.remoteJid;
|
|
24477
24755
|
if (!remoteJid) return;
|
|
24478
24756
|
if (remoteJid === "status@broadcast") {
|
|
24479
|
-
console.error(`${
|
|
24757
|
+
console.error(`${TAG8} drop: status broadcast account=${conn.accountId}`);
|
|
24480
24758
|
return;
|
|
24481
24759
|
}
|
|
24482
24760
|
if (!msg.message) {
|
|
24483
|
-
console.error(`${
|
|
24761
|
+
console.error(`${TAG8} drop: empty message account=${conn.accountId} from=${remoteJid}`);
|
|
24484
24762
|
return;
|
|
24485
24763
|
}
|
|
24486
24764
|
const dedupKey = `${conn.accountId}:${remoteJid}:${msg.key.id}`;
|
|
24487
24765
|
if (isDuplicateInbound(dedupKey)) {
|
|
24488
|
-
console.error(`${
|
|
24766
|
+
console.error(`${TAG8} drop: duplicate account=${conn.accountId} key=${dedupKey}`);
|
|
24489
24767
|
return;
|
|
24490
24768
|
}
|
|
24491
24769
|
if (msg.key.fromMe) {
|
|
24492
24770
|
if (msg.key.id && isAgentSentMessage(msg.key.id)) {
|
|
24493
|
-
console.error(`${
|
|
24771
|
+
console.error(`${TAG8} drop: echo suppression account=${conn.accountId} msgId=${msg.key.id}`);
|
|
24494
24772
|
return;
|
|
24495
24773
|
}
|
|
24496
24774
|
const extracted2 = extractMessage(msg);
|
|
24497
24775
|
if (!extracted2.text) {
|
|
24498
|
-
console.error(`${
|
|
24776
|
+
console.error(`${TAG8} owner reply skipped \u2014 no text content account=${conn.accountId}`);
|
|
24499
24777
|
return;
|
|
24500
24778
|
}
|
|
24501
24779
|
const isGroup2 = isGroupJid(remoteJid);
|
|
24502
24780
|
const senderPhone2 = conn.selfPhone ?? "owner";
|
|
24503
|
-
console.error(`${
|
|
24781
|
+
console.error(`${TAG8} owner reply mirrored to session from=${senderPhone2} account=${conn.accountId}`);
|
|
24504
24782
|
const reply2 = async (text) => {
|
|
24505
24783
|
const currentSock = conn.sock;
|
|
24506
24784
|
if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
|
|
@@ -24521,7 +24799,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24521
24799
|
}
|
|
24522
24800
|
const extracted = extractMessage(msg);
|
|
24523
24801
|
if (!extracted.text && !extracted.mediaType) {
|
|
24524
|
-
console.error(`${
|
|
24802
|
+
console.error(`${TAG8} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
|
|
24525
24803
|
return;
|
|
24526
24804
|
}
|
|
24527
24805
|
let mediaResult;
|
|
@@ -24531,7 +24809,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24531
24809
|
maxBytes: maxMb * 1024 * 1024
|
|
24532
24810
|
});
|
|
24533
24811
|
if (!mediaResult) {
|
|
24534
|
-
console.error(`${
|
|
24812
|
+
console.error(`${TAG8} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
|
|
24535
24813
|
}
|
|
24536
24814
|
}
|
|
24537
24815
|
const isGroup = isGroupJid(remoteJid);
|
|
@@ -24559,7 +24837,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24559
24837
|
});
|
|
24560
24838
|
}
|
|
24561
24839
|
console.error(
|
|
24562
|
-
`${
|
|
24840
|
+
`${TAG8} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
|
|
24563
24841
|
);
|
|
24564
24842
|
if (!accessResult.allowed) return;
|
|
24565
24843
|
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
@@ -24594,273 +24872,6 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24594
24872
|
}
|
|
24595
24873
|
}
|
|
24596
24874
|
|
|
24597
|
-
// app/lib/whatsapp/config-persist.ts
|
|
24598
|
-
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "fs";
|
|
24599
|
-
import { resolve as resolve9, join as join6 } from "path";
|
|
24600
|
-
var TAG8 = "[whatsapp:config]";
|
|
24601
|
-
function configPath(accountDir) {
|
|
24602
|
-
return resolve9(accountDir, "account.json");
|
|
24603
|
-
}
|
|
24604
|
-
function readConfig(accountDir) {
|
|
24605
|
-
const path2 = configPath(accountDir);
|
|
24606
|
-
if (!existsSync10(path2)) throw new Error(`account.json not found at ${path2}`);
|
|
24607
|
-
return JSON.parse(readFileSync10(path2, "utf-8"));
|
|
24608
|
-
}
|
|
24609
|
-
function writeConfig(accountDir, config2) {
|
|
24610
|
-
const path2 = configPath(accountDir);
|
|
24611
|
-
writeFileSync6(path2, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
24612
|
-
}
|
|
24613
|
-
function reloadManagerConfig(accountDir) {
|
|
24614
|
-
try {
|
|
24615
|
-
const config2 = readConfig(accountDir);
|
|
24616
|
-
reloadConfig(config2);
|
|
24617
|
-
console.error(`${TAG8} reloaded manager config`);
|
|
24618
|
-
} catch (err) {
|
|
24619
|
-
console.error(`${TAG8} manager config reload failed: ${String(err)}`);
|
|
24620
|
-
}
|
|
24621
|
-
}
|
|
24622
|
-
var E164_PATTERN = /^\+\d{7,15}$/;
|
|
24623
|
-
function migrateAdminPhones(wa) {
|
|
24624
|
-
if (wa.adminPhones !== void 0) return 0;
|
|
24625
|
-
if (!Array.isArray(wa.allowFrom)) return 0;
|
|
24626
|
-
const allowFrom = wa.allowFrom;
|
|
24627
|
-
const phones = [];
|
|
24628
|
-
const remaining = [];
|
|
24629
|
-
for (const entry of allowFrom) {
|
|
24630
|
-
if (E164_PATTERN.test(entry)) {
|
|
24631
|
-
phones.push(entry);
|
|
24632
|
-
} else {
|
|
24633
|
-
remaining.push(entry);
|
|
24634
|
-
}
|
|
24635
|
-
}
|
|
24636
|
-
if (phones.length === 0) return 0;
|
|
24637
|
-
wa.adminPhones = phones;
|
|
24638
|
-
wa.allowFrom = remaining;
|
|
24639
|
-
console.error(`${TAG8} migrated ${phones.length} phone(s) from allowFrom to adminPhones`);
|
|
24640
|
-
return phones.length;
|
|
24641
|
-
}
|
|
24642
|
-
function persistAfterPairing(accountDir, accountId, selfPhone) {
|
|
24643
|
-
try {
|
|
24644
|
-
const config2 = readConfig(accountDir);
|
|
24645
|
-
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
24646
|
-
config2.whatsapp = {};
|
|
24647
|
-
}
|
|
24648
|
-
const wa = config2.whatsapp;
|
|
24649
|
-
if (!wa.accounts || typeof wa.accounts !== "object") {
|
|
24650
|
-
wa.accounts = {};
|
|
24651
|
-
}
|
|
24652
|
-
const accounts = wa.accounts;
|
|
24653
|
-
if (!accounts[accountId] || typeof accounts[accountId] !== "object") {
|
|
24654
|
-
accounts[accountId] = { name: "Main" };
|
|
24655
|
-
}
|
|
24656
|
-
if (selfPhone) {
|
|
24657
|
-
const normalized = selfPhone.startsWith("+") ? selfPhone : `+${selfPhone}`;
|
|
24658
|
-
if (!Array.isArray(wa.adminPhones)) {
|
|
24659
|
-
wa.adminPhones = [];
|
|
24660
|
-
}
|
|
24661
|
-
const adminPhones = wa.adminPhones;
|
|
24662
|
-
if (!adminPhones.includes(normalized)) {
|
|
24663
|
-
adminPhones.push(normalized);
|
|
24664
|
-
console.error(`${TAG8} added selfPhone=${normalized} to adminPhones`);
|
|
24665
|
-
}
|
|
24666
|
-
} else {
|
|
24667
|
-
console.error(`${TAG8} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
|
|
24668
|
-
}
|
|
24669
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24670
|
-
if (!parsed.success) {
|
|
24671
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
24672
|
-
console.error(`${TAG8} validation failed after pairing: ${msg}`);
|
|
24673
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
24674
|
-
}
|
|
24675
|
-
config2.whatsapp = parsed.data;
|
|
24676
|
-
writeConfig(accountDir, config2);
|
|
24677
|
-
console.error(`${TAG8} persisted after pairing account=${accountId} phone=${selfPhone ?? "null"}`);
|
|
24678
|
-
reloadManagerConfig(accountDir);
|
|
24679
|
-
return { ok: true };
|
|
24680
|
-
} catch (err) {
|
|
24681
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24682
|
-
console.error(`${TAG8} persist failed account=${accountId}: ${msg}`);
|
|
24683
|
-
return { ok: false, error: msg };
|
|
24684
|
-
}
|
|
24685
|
-
}
|
|
24686
|
-
function addAdminPhone(accountDir, phone) {
|
|
24687
|
-
const normalized = phone.trim();
|
|
24688
|
-
if (!E164_PATTERN.test(normalized)) {
|
|
24689
|
-
return { ok: false, error: `Invalid phone format "${normalized}". Expected E.164 (e.g. +441234567890).` };
|
|
24690
|
-
}
|
|
24691
|
-
try {
|
|
24692
|
-
const config2 = readConfig(accountDir);
|
|
24693
|
-
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
24694
|
-
config2.whatsapp = {};
|
|
24695
|
-
}
|
|
24696
|
-
const wa = config2.whatsapp;
|
|
24697
|
-
if (!Array.isArray(wa.adminPhones)) {
|
|
24698
|
-
wa.adminPhones = [];
|
|
24699
|
-
}
|
|
24700
|
-
const adminPhones = wa.adminPhones;
|
|
24701
|
-
if (adminPhones.includes(normalized)) {
|
|
24702
|
-
return { ok: true, message: `Phone ${normalized} is already in the admin list.` };
|
|
24703
|
-
}
|
|
24704
|
-
adminPhones.push(normalized);
|
|
24705
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24706
|
-
if (!parsed.success) {
|
|
24707
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
24708
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
24709
|
-
}
|
|
24710
|
-
config2.whatsapp = parsed.data;
|
|
24711
|
-
writeConfig(accountDir, config2);
|
|
24712
|
-
console.error(`${TAG8} added admin phone=${normalized}`);
|
|
24713
|
-
reloadManagerConfig(accountDir);
|
|
24714
|
-
return { ok: true, message: `Added ${normalized} as admin phone. Messages from this number will route to the admin agent.` };
|
|
24715
|
-
} catch (err) {
|
|
24716
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24717
|
-
console.error(`${TAG8} addAdminPhone failed: ${msg}`);
|
|
24718
|
-
return { ok: false, error: msg };
|
|
24719
|
-
}
|
|
24720
|
-
}
|
|
24721
|
-
function removeAdminPhone(accountDir, phone) {
|
|
24722
|
-
const normalized = phone.trim();
|
|
24723
|
-
try {
|
|
24724
|
-
const config2 = readConfig(accountDir);
|
|
24725
|
-
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
24726
|
-
return { ok: true, message: `No WhatsApp config exists \u2014 nothing to remove.` };
|
|
24727
|
-
}
|
|
24728
|
-
const wa = config2.whatsapp;
|
|
24729
|
-
if (!Array.isArray(wa.adminPhones)) {
|
|
24730
|
-
return { ok: true, message: `No admin phones configured \u2014 nothing to remove.` };
|
|
24731
|
-
}
|
|
24732
|
-
const adminPhones = wa.adminPhones;
|
|
24733
|
-
const idx = adminPhones.indexOf(normalized);
|
|
24734
|
-
if (idx === -1) {
|
|
24735
|
-
return { ok: true, message: `Phone ${normalized} is not in the admin list.` };
|
|
24736
|
-
}
|
|
24737
|
-
adminPhones.splice(idx, 1);
|
|
24738
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24739
|
-
if (!parsed.success) {
|
|
24740
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
24741
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
24742
|
-
}
|
|
24743
|
-
config2.whatsapp = parsed.data;
|
|
24744
|
-
writeConfig(accountDir, config2);
|
|
24745
|
-
console.error(`${TAG8} removed admin phone=${normalized}`);
|
|
24746
|
-
reloadManagerConfig(accountDir);
|
|
24747
|
-
return { ok: true, message: `Removed ${normalized} from admin phones. Messages from this number will now route to the public agent.` };
|
|
24748
|
-
} catch (err) {
|
|
24749
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24750
|
-
console.error(`${TAG8} removeAdminPhone failed: ${msg}`);
|
|
24751
|
-
return { ok: false, error: msg };
|
|
24752
|
-
}
|
|
24753
|
-
}
|
|
24754
|
-
function readAdminPhones(accountDir) {
|
|
24755
|
-
try {
|
|
24756
|
-
const config2 = readConfig(accountDir);
|
|
24757
|
-
const wa = config2.whatsapp;
|
|
24758
|
-
if (!wa) return [];
|
|
24759
|
-
migrateAdminPhones(wa);
|
|
24760
|
-
if (!Array.isArray(wa.adminPhones)) return [];
|
|
24761
|
-
return wa.adminPhones.filter((p) => typeof p === "string");
|
|
24762
|
-
} catch {
|
|
24763
|
-
return [];
|
|
24764
|
-
}
|
|
24765
|
-
}
|
|
24766
|
-
function setPublicAgent(accountDir, slug) {
|
|
24767
|
-
const trimmed = slug.trim();
|
|
24768
|
-
if (!trimmed) {
|
|
24769
|
-
return { ok: false, error: "Agent slug cannot be empty." };
|
|
24770
|
-
}
|
|
24771
|
-
const agentConfigPath = join6(accountDir, "agents", trimmed, "config.json");
|
|
24772
|
-
if (!existsSync10(agentConfigPath)) {
|
|
24773
|
-
return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
24774
|
-
}
|
|
24775
|
-
try {
|
|
24776
|
-
const config2 = readConfig(accountDir);
|
|
24777
|
-
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
24778
|
-
config2.whatsapp = {};
|
|
24779
|
-
}
|
|
24780
|
-
const wa = config2.whatsapp;
|
|
24781
|
-
wa.publicAgent = trimmed;
|
|
24782
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24783
|
-
if (!parsed.success) {
|
|
24784
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
24785
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
24786
|
-
}
|
|
24787
|
-
config2.whatsapp = parsed.data;
|
|
24788
|
-
writeConfig(accountDir, config2);
|
|
24789
|
-
console.error(`${TAG8} publicAgent set to ${trimmed}`);
|
|
24790
|
-
reloadManagerConfig(accountDir);
|
|
24791
|
-
return { ok: true, message: `Public agent set to "${trimmed}". WhatsApp messages from non-admin phones will be handled by this agent.` };
|
|
24792
|
-
} catch (err) {
|
|
24793
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24794
|
-
console.error(`${TAG8} setPublicAgent failed: ${msg}`);
|
|
24795
|
-
return { ok: false, error: msg };
|
|
24796
|
-
}
|
|
24797
|
-
}
|
|
24798
|
-
function getPublicAgent(accountDir) {
|
|
24799
|
-
try {
|
|
24800
|
-
const config2 = readConfig(accountDir);
|
|
24801
|
-
const wa = config2.whatsapp;
|
|
24802
|
-
if (!wa) return null;
|
|
24803
|
-
const slug = wa.publicAgent;
|
|
24804
|
-
if (typeof slug === "string" && slug.trim()) {
|
|
24805
|
-
return slug.trim();
|
|
24806
|
-
}
|
|
24807
|
-
return null;
|
|
24808
|
-
} catch {
|
|
24809
|
-
return null;
|
|
24810
|
-
}
|
|
24811
|
-
}
|
|
24812
|
-
function updateConfig(accountDir, fields) {
|
|
24813
|
-
const fieldNames = Object.keys(fields);
|
|
24814
|
-
if (fieldNames.length === 0) {
|
|
24815
|
-
return { ok: false, error: "No fields provided to update." };
|
|
24816
|
-
}
|
|
24817
|
-
try {
|
|
24818
|
-
const config2 = readConfig(accountDir);
|
|
24819
|
-
if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
|
|
24820
|
-
config2.whatsapp = {};
|
|
24821
|
-
}
|
|
24822
|
-
const wa = config2.whatsapp;
|
|
24823
|
-
migrateAdminPhones(wa);
|
|
24824
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
24825
|
-
wa[key] = value;
|
|
24826
|
-
}
|
|
24827
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24828
|
-
if (!parsed.success) {
|
|
24829
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
24830
|
-
console.error(`${TAG8} update validation failed: ${msg}`);
|
|
24831
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
24832
|
-
}
|
|
24833
|
-
config2.whatsapp = parsed.data;
|
|
24834
|
-
writeConfig(accountDir, config2);
|
|
24835
|
-
console.error(`${TAG8} updated fields=[${fieldNames.join(",")}]`);
|
|
24836
|
-
reloadManagerConfig(accountDir);
|
|
24837
|
-
return { ok: true, message: `Updated WhatsApp config: ${fieldNames.join(", ")}.` };
|
|
24838
|
-
} catch (err) {
|
|
24839
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24840
|
-
console.error(`${TAG8} updateConfig failed: ${msg}`);
|
|
24841
|
-
return { ok: false, error: msg };
|
|
24842
|
-
}
|
|
24843
|
-
}
|
|
24844
|
-
function getConfig(accountDir) {
|
|
24845
|
-
try {
|
|
24846
|
-
const config2 = readConfig(accountDir);
|
|
24847
|
-
const wa = config2.whatsapp;
|
|
24848
|
-
if (!wa) return null;
|
|
24849
|
-
const migrated = migrateAdminPhones(wa);
|
|
24850
|
-
if (migrated > 0) {
|
|
24851
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
24852
|
-
if (parsed.success) {
|
|
24853
|
-
config2.whatsapp = parsed.data;
|
|
24854
|
-
writeConfig(accountDir, config2);
|
|
24855
|
-
reloadManagerConfig(accountDir);
|
|
24856
|
-
}
|
|
24857
|
-
}
|
|
24858
|
-
return wa;
|
|
24859
|
-
} catch {
|
|
24860
|
-
return null;
|
|
24861
|
-
}
|
|
24862
|
-
}
|
|
24863
|
-
|
|
24864
24875
|
// app/api/whatsapp/login/wait/route.ts
|
|
24865
24876
|
async function POST10(req) {
|
|
24866
24877
|
try {
|
|
@@ -46,6 +46,8 @@ Ask what rules the agent should follow when using this skill:
|
|
|
46
46
|
|
|
47
47
|
Not every skill needs all of these. Only include what's relevant.
|
|
48
48
|
|
|
49
|
+
If the skill involves generating PDFs or print-ready documents, load `references/pdf-generation.md` for the constraints on HTML layout and the `browser_pdf_save` rendering path.
|
|
50
|
+
|
|
49
51
|
## Step 4: Decide on References
|
|
50
52
|
|
|
51
53
|
If the skill has detailed procedures, templates, or data formats, those belong in `references/` files — not inline in SKILL.md.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# PDF Generation in Skills
|
|
2
|
+
|
|
3
|
+
When a skill needs to produce a PDF, the output is always a two-stage process: the skill authors the HTML, and the browser-specialist renders it to PDF via the `browser_pdf_save` MCP tool.
|
|
4
|
+
|
|
5
|
+
## The rule
|
|
6
|
+
|
|
7
|
+
Never instruct the agent to write or execute raw Node.js scripts (e.g. `require('playwright')`, `page.pdf()`, `npx playwright`). Playwright is not in the Node.js module path — these scripts fail with MODULE_NOT_FOUND, waste tool calls searching the filesystem, and produce hardcoded paths that break on reinstall.
|
|
8
|
+
|
|
9
|
+
Instead, skills that produce PDFs should:
|
|
10
|
+
|
|
11
|
+
1. **Build the HTML** — the skill's behaviour section describes what the document contains and how it's structured. The agent writes the HTML file using standard file tools.
|
|
12
|
+
2. **Delegate rendering to browser-specialist** — the agent asks the browser-specialist to navigate to the HTML file and call `browser_pdf_save` to produce the PDF. The specialist has Playwright access through governed MCP tools; the admin agent does not.
|
|
13
|
+
|
|
14
|
+
## Print-ready HTML constraints
|
|
15
|
+
|
|
16
|
+
HTML intended for PDF output must follow print-ready layout patterns. The most common failures when agents design their own pagination are rigid fixed-height page divs (causing whitespace gaps) and `position: fixed` hacks for headers/footers (causing overlap and duplication).
|
|
17
|
+
|
|
18
|
+
**Continuous flow layout** — content flows naturally. Use `@page` margin rules for page margins, not wrapper divs with fixed heights. Use `page-break-inside: avoid` on logical units (cards, tables, stat groups) and `page-break-before: always` on major section dividers.
|
|
19
|
+
|
|
20
|
+
**Page numbers and running footers** — use `@page` margin boxes (`@bottom-center { content: counter(page); }`), not JavaScript or position-fixed elements.
|
|
21
|
+
|
|
22
|
+
**Dark backgrounds in print** — browsers strip background colours by default. Add `print-color-adjust: exact; -webkit-print-color-adjust: exact;` to dark elements in `@media print`.
|
|
23
|
+
|
|
24
|
+
**Cover and back pages** — full-bleed pages use `height: 100vh; page-break-after: always;` in `@media print` with zero `@page` margins (via `@page :first` for covers, named pages for back pages).
|
|
25
|
+
|
|
26
|
+
**Glassmorphism and backdrop-filter** — these do not survive browser print rendering. Any section using glassmorphism needs a pre-rendered PNG fallback image that is hidden on screen and shown only in `@media print`.
|
|
27
|
+
|
|
28
|
+
## What the skill file should contain
|
|
29
|
+
|
|
30
|
+
The skill's SKILL.md describes the document's purpose and structure. A `references/` file holds the HTML template or content specification. The skill should state that PDF rendering is delegated to browser-specialist — it should not contain Playwright code, npm commands, or file path assumptions about the Playwright installation.
|
|
@@ -42,7 +42,7 @@ Accounts are linked via QR in the Channels UI. Outbound routing (`src/web/outbou
|
|
|
42
42
|
|
|
43
43
|
| Target | Account | Reason |
|
|
44
44
|
|--------|---------|--------|
|
|
45
|
-
| Admin
|
|
45
|
+
| Admin phones | Account with admin binding | Admin's registered number |
|
|
46
46
|
| Self-chat | Account with selfChatMode | Admin talking to themselves |
|
|
47
47
|
| Group messages | Account that is a group member | Whichever is in the group |
|
|
48
48
|
| Customer DMs | Account with open DM policy | Dedicated agent number |
|
|
@@ -68,7 +68,7 @@ Admin phone registration and public agent selection persist across relinks — n
|
|
|
68
68
|
- **QR expired** — generate a new one with `whatsapp-login-start`
|
|
69
69
|
- **Phone not scanning** — ensure the phone has internet, WhatsApp is updated, and the user is in Linked Devices
|
|
70
70
|
- **Keeps disconnecting** — check `whatsapp-status` for error details. A 401 means re-link is needed. Repeated transient errors suggest network issues on the device.
|
|
71
|
-
- **Messages routing as public** — the user's personal phone is not in `
|
|
71
|
+
- **Messages routing as public** — the user's personal phone is not in `adminPhones`. Call `whatsapp-config` with `action: "add-admin-phone"` and the user's phone number to register it.
|
|
72
72
|
|
|
73
73
|
## Language
|
|
74
74
|
|
|
@@ -17,22 +17,42 @@ Call `whatsapp-status` first — if no WhatsApp account is connected, direct the
|
|
|
17
17
|
|
|
18
18
|
## Presenting settings
|
|
19
19
|
|
|
20
|
-
Call `whatsapp-config action: schema` to get field definitions with descriptions,
|
|
20
|
+
Call `whatsapp-config action: schema` to get field definitions with descriptions, and `whatsapp-config action: get-config` to get current values. Use the Description column from the schema output as the `description` field on each form field — do not improvise descriptions.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
### Admin phones
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
2. **Operational limits** — mediaMaxMb, textChunkLimit, debounceMs
|
|
26
|
-
3. **Behaviour** — sendReadReceipts, ackReaction
|
|
27
|
-
4. **Account settings** — name, enabled, selfChatMode
|
|
28
|
-
5. **Group configuration** — groups (per-group activation)
|
|
24
|
+
Before the form, call `whatsapp-config action: list-admin-phones` and display the result. Admin phones are managed via `add-admin-phone` / `remove-admin-phone` — they are not editable in this form.
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
- `description` on every field (from schema output)
|
|
32
|
-
- `defaultValue` showing the current value or the schema default
|
|
33
|
-
- Rich option labels with descriptions for enum fields (e.g. dmPolicy options should explain what "open", "allowlist", and "disabled" each mean)
|
|
26
|
+
### Form fields
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
Present settings as a `render-component` form. Each field must have a unique `name` matching the schema field name (used as the state key and the `update-config` field key), a `type`, a `label` (plain English), and a `description` (from schema output). For selects, specify `options`. For all fields, set `defaultValue` from `get-config` (current value) or the schema default.
|
|
29
|
+
|
|
30
|
+
**Messaging policies:**
|
|
31
|
+
|
|
32
|
+
- `dmPolicy` (`select`): label "Who can message your agent (DMs)", options:
|
|
33
|
+
- `open` — "Open — anyone can message"
|
|
34
|
+
- `allowlist` — "Allowlist — only approved numbers"
|
|
35
|
+
- `disabled` — "Disabled — no public DMs"
|
|
36
|
+
- `groupPolicy` (`select`): label "Group message handling", options:
|
|
37
|
+
- `open` — "Always — respond to all group messages"
|
|
38
|
+
- `allowlist` — "Allowlist — only in approved groups"
|
|
39
|
+
- `disabled` — "Disabled — ignore all group messages"
|
|
40
|
+
|
|
41
|
+
**Behaviour:**
|
|
42
|
+
|
|
43
|
+
- `sendReadReceipts` (`select`): label "Blue ticks (read receipts)", options:
|
|
44
|
+
- `true` — "On — contacts see read receipts"
|
|
45
|
+
- `false` — "Off — no read receipts"
|
|
46
|
+
|
|
47
|
+
**Operational limits:**
|
|
48
|
+
|
|
49
|
+
- `mediaMaxMb` (`number`): label "Media size limit (MB)"
|
|
50
|
+
- `textChunkLimit` (`number`): label "Message chunk limit (characters)"
|
|
51
|
+
- `debounceMs` (`number`): label "Message batching delay (ms)"
|
|
52
|
+
|
|
53
|
+
### Submit
|
|
54
|
+
|
|
55
|
+
The form's `submitMessage` must map the field values back to `update-config` field names: `Save these WhatsApp settings: {{json}}` — where `{{json}}` produces the JSON object with field names as keys (e.g. `{ "dmPolicy": "open", "mediaMaxMb": 50, ... }`).
|
|
36
56
|
|
|
37
57
|
## Group configuration
|
|
38
58
|
|
|
@@ -51,9 +71,11 @@ After the user submits, write changes via `whatsapp-config action: "update-confi
|
|
|
51
71
|
|
|
52
72
|
## Language
|
|
53
73
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- "
|
|
57
|
-
- "
|
|
58
|
-
- "
|
|
59
|
-
-
|
|
74
|
+
Plain English goes in `label` — schema field names go in `name`. The `name` property is the machine key (must match the schema exactly); the `label` is what the user reads. Examples:
|
|
75
|
+
|
|
76
|
+
- `name: "dmPolicy"`, `label: "Who can message your agent (DMs)"`
|
|
77
|
+
- `name: "debounceMs"`, `label: "Message batching delay (ms)"`
|
|
78
|
+
- `name: "mediaMaxMb"`, `label: "Media size limit (MB)"`
|
|
79
|
+
- `name: "sendReadReceipts"`, `label: "Blue ticks (read receipts)"`
|
|
80
|
+
|
|
81
|
+
Group names (not JIDs) in all user-facing text.
|
|
@@ -3,7 +3,7 @@ name: browser-specialist
|
|
|
3
3
|
description: "Web browser tasks — visiting websites, filling forms, clicking buttons, and reading page content. Delegate when a task requires controlling the browser."
|
|
4
4
|
summary: "Browses the web on your behalf — visiting websites, filling in forms, and extracting information. For example, when you need to sign up for a service, check a competitor's pricing page, or set up an online account."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_navigate_back, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_fill, mcp__plugin_playwright_playwright__browser_fill_form, mcp__plugin_playwright_playwright__browser_type, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_select_option, mcp__plugin_playwright_playwright__browser_wait_for, mcp__plugin_playwright_playwright__browser_handle_dialog, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_tabs, mcp__plugin_playwright_playwright__browser_close, mcp__admin__api-key-store
|
|
6
|
+
tools: mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_navigate_back, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_fill, mcp__plugin_playwright_playwright__browser_fill_form, mcp__plugin_playwright_playwright__browser_type, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_select_option, mcp__plugin_playwright_playwright__browser_wait_for, mcp__plugin_playwright_playwright__browser_handle_dialog, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_tabs, mcp__plugin_playwright_playwright__browser_close, mcp__plugin_playwright_playwright__browser_pdf_save, mcp__admin__api-key-store
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Browser Specialist
|