run_group_audit
Analyze WhatsApp group memberships to identify overlapping members, members with no direct chat, and members not in your contacts.
Instructions
Run a combined group-membership audit (overlaps, no direct chat, not in contacts).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| group_limit | No | How many groups to scan | |
| refresh_group_info | No | Fetch live group metadata before analysis | |
| overlap_min_shared_groups | No | Threshold for overlap list | |
| min_shared_groups | No | Threshold for no-direct-chat and not-in-contacts lists |
Implementation Reference
- src/services/whatsapp.ts:2979-3054 (handler)The runGroupAudit method on WhatsAppService: builds a group membership audit matrix, then filters for overlaps, members without direct chat, and members not in contacts, returning summary counts and detailed lists.
async runGroupAudit( groupLimit = 200, refreshGroupInfo = false, overlapMinSharedGroups = 2, minSharedGroups = 1, ): Promise<{ summary: { groupsProcessed: number; totalMembers: number; overlapCount: number; withoutDirectChatCount: number; notInContactsCount: number; }; overlaps: Array<{ canonicalId: string; ids: string[]; name: string | null; pushname: string | null; number: string | null; groupCount: number; groups: Array<{ id: string; name: string }>; hasDirectChat: boolean; inContacts: boolean; }>; withoutDirectChat: Array<{ canonicalId: string; ids: string[]; name: string | null; pushname: string | null; number: string | null; groupCount: number; groups: Array<{ id: string; name: string }>; hasDirectChat: boolean; inContacts: boolean; }>; notInContacts: Array<{ canonicalId: string; ids: string[]; name: string | null; pushname: string | null; number: string | null; groupCount: number; groups: Array<{ id: string; name: string }>; hasDirectChat: boolean; inContacts: boolean; }>; }> { const matrix = await this.buildGroupAuditMatrix( groupLimit, refreshGroupInfo, ); const overlapThreshold = Math.max(2, overlapMinSharedGroups); const baseThreshold = Math.max(1, minSharedGroups); const overlaps = matrix.members.filter( (m) => m.groupCount >= overlapThreshold, ); const withoutDirectChat = matrix.members.filter( (m) => !m.hasDirectChat && m.groupCount >= baseThreshold, ); const notInContacts = matrix.members.filter( (m) => !m.inContacts && m.groupCount >= baseThreshold, ); return { summary: { groupsProcessed: matrix.groupsProcessed, totalMembers: matrix.members.length, overlapCount: overlaps.length, withoutDirectChatCount: withoutDirectChat.length, notInContactsCount: notInContacts.length, }, overlaps, withoutDirectChat, notInContacts, }; } - src/tools/chats.ts:466-526 (registration)Registration of the 'run_group_audit' tool via server.tool() with Zod schema params and handler calling whatsappService.runGroupAudit().
server.tool( "run_group_audit", "Run a combined group-membership audit (overlaps, no direct chat, not in contacts).", { group_limit: z .number() .int() .positive() .optional() .default(200) .describe("How many groups to scan"), refresh_group_info: z .boolean() .optional() .default(false) .describe("Fetch live group metadata before analysis"), overlap_min_shared_groups: z .number() .int() .positive() .optional() .default(2) .describe("Threshold for overlap list"), min_shared_groups: z .number() .int() .positive() .optional() .default(1) .describe("Threshold for no-direct-chat and not-in-contacts lists"), }, async ({ group_limit, refresh_group_info, overlap_min_shared_groups, min_shared_groups, }): Promise<CallToolResult> => { try { const result = await whatsappService.runGroupAudit( group_limit, refresh_group_info, overlap_min_shared_groups, min_shared_groups, ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (error: any) { log.error("Error in run_group_audit tool:", error); return { content: [ { type: "text", text: `Error running group audit: ${error?.message || String(error)}`, }, ], isError: true, }; } }, ); - src/tools/chats.ts:469-496 (schema)Input schema for run_group_audit: group_limit (default 200), refresh_group_info (default false), overlap_min_shared_groups (default 2), min_shared_groups (default 1).
{ group_limit: z .number() .int() .positive() .optional() .default(200) .describe("How many groups to scan"), refresh_group_info: z .boolean() .optional() .default(false) .describe("Fetch live group metadata before analysis"), overlap_min_shared_groups: z .number() .int() .positive() .optional() .default(2) .describe("Threshold for overlap list"), min_shared_groups: z .number() .int() .positive() .optional() .default(1) .describe("Threshold for no-direct-chat and not-in-contacts lists"), }, - src/services/whatsapp.ts:2777-2877 (helper)buildGroupAuditMatrix helper: lists groups, fetches participants, builds a member->groups map, and returns the matrix used by runGroupAudit (and other audit methods).
private async buildGroupAuditMatrix( groupLimit = 200, refreshGroupInfo = false, ): Promise<{ groupsProcessed: number; members: Array<{ canonicalId: string; ids: string[]; name: string | null; pushname: string | null; number: string | null; groupCount: number; groups: Array<{ id: string; name: string }>; hasDirectChat: boolean; inContacts: boolean; }>; }> { let groups: SimpleChat[] = []; try { groups = await this.listGroups(groupLimit, false); } catch (error) { log.warn( { err: error, groupLimit, refreshGroupInfo }, "Group audit: failed to list groups", ); return { groupsProcessed: 0, members: [], }; } const memberMap = new Map< string, { canonicalId: string; ids: Set<string>; groups: Array<{ id: string; name: string }>; } >(); for (const group of groups) { const groupId = String(group.id || "").trim(); if (!groupId.endsWith("@g.us")) continue; let participants: string[] = []; try { participants = await this.getGroupParticipantJids( groupId, refreshGroupInfo, ); } catch (error) { log.warn( { err: error, groupId, refreshGroupInfo }, "Group audit: failed to load participants", ); continue; } for (const participantRaw of participants) { const participant = this.normalizeJid(participantRaw); if (!participant) continue; const canonicalId = this.resolveCanonicalChatId(participant); const existing = memberMap.get(canonicalId) || { canonicalId, ids: new Set<string>(), groups: [], }; existing.ids.add(participant); if (!existing.groups.some((g) => g.id === groupId)) { existing.groups.push({ id: groupId, name: group.name || groupId, }); } memberMap.set(canonicalId, existing); } } const members = Array.from(memberMap.values()) .map((entry) => { const profile = this.buildMemberDisplay(entry.canonicalId); return { canonicalId: entry.canonicalId, ids: Array.from(entry.ids.values()).sort(), name: profile.name, pushname: profile.pushname, number: profile.number, groupCount: entry.groups.length, groups: entry.groups.sort((a, b) => a.name.localeCompare(b.name)), hasDirectChat: this.hasDirectChatForParticipant(entry.canonicalId), inContacts: this.isParticipantInContacts(entry.canonicalId), }; }) .sort( (a, b) => b.groupCount - a.groupCount || String(a.name || "").localeCompare(String(b.name || "")), ); return { groupsProcessed: groups.length, members, }; } - src/server.ts:165-169 (registration)Tool metadata registration in server.ts marking run_group_audit as readOnly, idempotent, and openWorld.
run_group_audit: { readOnlyHint: true, idempotentHint: true, openWorldHint: true, },