Skip to content

feat: add daily WhatsApp triage script with email delivery - #18

Open
barbararoque98 wants to merge 1 commit into
jlucaso1:mainfrom
barbararoque98:feature/triagem-diaria
Open

feat: add daily WhatsApp triage script with email delivery#18
barbararoque98 wants to merge 1 commit into
jlucaso1:mainfrom
barbararoque98:feature/triagem-diaria

Conversation

@barbararoque98

@barbararoque98 barbararoque98 commented Jun 1, 2026

Copy link
Copy Markdown

Summary

  • triagem.js: standalone Node.js script that reads the local WhatsApp SQLite DB, classifies conversations by urgency (VIP list, keyword patterns, group/junk filters) and sends a formatted HTML email via nodemailer/Gmail
  • rodar-triagem.ps1: PowerShell wrapper for Windows Task Scheduler
  • src/whatsapp.ts: two contact-resolution improvements — handles contacts.upsert events to persist contacts on sync, and captures pushName from every incoming message so LID-format contacts resolve to display names over time
  • triagem-config.example.json: credential-free config template; actual triagem-config.json is excluded via .gitignore
  • package.json: adds nodemailer dependency
  • .gitignore: excludes credentials file, lock file and debug scripts

Scheduling

Two Windows Task Scheduler triggers guarantee the email is sent at least once per day when the machine is on:

  1. Daily at 08:00 — runs if the machine is already on
  2. At logon — fallback if machine was off at 8am

A lock file (triagem-ultimo-envio.txt) prevents duplicate sends within the same day.

Test plan

  • Copy triagem-config.example.json to triagem-config.json and fill in credentials
  • Run node triagem.js manually and confirm email arrives
  • Verify lock file is created after successful run
  • Run again same day and confirm skip message, no duplicate email
  • Check that new incoming messages update contact names in DB

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added email notification feature to send daily WhatsApp message digests with automatic classification (urgent, important, can wait, spam).
    • Enhanced contact storage to capture and persist sender information from incoming messages.
  • Chores

    • Added project-specific configuration files and temporary debug artifacts to ignore list.
    • Added email service dependency.

Adds triagem.js: reads the local WhatsApp SQLite DB, classifies
conversations by urgency (VIP rules first, then keyword patterns,
then group/junk filters) and sends a formatted HTML email via
nodemailer/Gmail. Runs daily via Windows Task Scheduler and Startup
folder fallback to guarantee at least one send per day.

Also improves contact name resolution in whatsapp.ts:
handles contacts.upsert events and captures pushName from every
incoming message so LID-format contacts resolve to display names.

Files changed:
- triagem.js: triage logic, email sending, daily dedup via lock file
- rodar-triagem.ps1: PowerShell wrapper for Task Scheduler
- triagem-config.example.json: credential-free config template
- .gitignore: exclude triagem-config.json, lock file, debug scripts
- package.json: add nodemailer dependency
- src/whatsapp.ts: persist contacts from upsert events and pushName

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an email triage system that reads unread WhatsApp messages from the past 24 hours, classifies conversations by priority using keyword/pattern matching and VIP detection, formats results into an HTML email summary, and sends it daily via Gmail with deduplication.

Changes

Email Triage Workflow

Layer / File(s) Summary
WhatsApp contact and message persistence
src/whatsapp.ts
Adds contacts.upsert event handler to persist incoming contacts via storeContact, and enhances messages.upsert to store the sender's pushName as the contact notify field using normalized JID.
Triagem configuration and dependencies
package.json, triagem-config.example.json
Adds Nodemailer ^8.0.7 dependency for email delivery and provides example configuration with email credentials, VIP contacts, and automatic-sender whitelist.
Triagem message classification and database reading
triagem.js
Implements config loading, 24-hour SQLite WhatsApp database queries for unread chats, and classification logic using ordered rule evaluation: VIP detection, group/junk heuristics, automatic-sender patterns, urgency keywords, and content patterns.
Triagem email formatting and sending
triagem.js
Formats classified conversations into sectioned HTML email (urgente, importante, pode_esperar) with preview truncation, sends via Gmail Nodemailer, enforces daily deduplication via America/Sao_Paulo date lock file, and exits on errors.
Deployment and project infrastructure
rodar-triagem.ps1, .gitignore
Adds PowerShell runner script that sets PATH, changes to project directory, executes triagem.js, and logs output; updates gitignore to exclude triagem config/state/log files and debug scripts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • jlucaso1/whatsapp-mcp-ts#7: Adds contacts table and storeContact() function that this PR builds upon for persisting contact data via WhatsApp event handlers.

Suggested reviewers

  • jlucaso1

Poem

🐰 A triage script hops through chats so quick,
Sorting messages by their urgent tick,
Classifies VIPs with whisker-twitch care,
Sends HTML summaries through the air,
So dear humans wake to organized mail—
A helpful rabbit's productivity tale! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a daily WhatsApp triage script with email delivery, which is the core feature across the new triagem.js script, supporting configuration, wrapper script, and dependency additions.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
coderabbitai Bot requested a review from jlucaso1 June 1, 2026 18:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 13: package.json adds "nodemailer": "^8.0.7" but package-lock.json lacks
any nodemailer entry causing npm ci to fail; run npm install (or npm install
--package-lock-only) to regenerate package-lock.json so nodemailer is recorded,
verify packages[""].dependencies includes nodemailer, then commit the updated
package-lock.json alongside the package.json change to keep
package.json/package-lock.json in sync.

In `@rodar-triagem.ps1`:
- Line 3: The current invocation line uses Out-File which overwrites
triagem-log.txt on each run; change the command that runs node (the line calling
"node.exe" triagem.js piped to Out-File "triagem-log.txt") to use Out-File with
-Append (or an equivalent append operator) so new output is appended to
C:\Servicos\whatsapp-mcp\triagem-log.txt instead of replacing it, preserving
historical logs for triagem.js runs.
- Around line 1-3: The script currently hardcodes three absolute paths (the Node
binary path "C:\Program Files\nodejs\node.exe", the project directory used by
Set-Location "C:\Servicos\whatsapp-mcp", and the log file path
"C:\Servicos\whatsapp-mcp\triagem-log.txt"); change triagem.ps1 to accept these
as parameters (e.g., $NodePath, $ProjectDir, $LogFile) with sensible defaults
and then replace direct usages of Set-Location, the node executable invocation,
and Out-File with those parameters while keeping the existing behavior of
appending stderr/stdout to the log and preserving the $env:Path setup; reference
symbols to update: $env:Path, Set-Location, triagem.js invocation (&
"...node.exe" triagem.js ...), and Out-File.
- Around line 2-3: The script currently lacks error handling around Set-Location
and the Node invocation (the & "C:\Program Files\nodejs\node.exe" triagem.js ...
pipeline), so failures aren't propagated to Task Scheduler; modify the script to
validate Set-Location (wrap in try/catch or test with Test-Path and throw/exit
on failure), run Node with a method that exposes its exit code (use
Start-Process -FilePath "C:\Program Files\nodejs\node.exe" -ArgumentList
"triagem.js" -Wait -NoNewWindow or capture $LASTEXITCODE after the call), write
stderr/stdout to triagem-log.txt, and if Node returns a non-zero exit code then
write an error entry to the log and call exit $LASTEXITCODE (or throw) so the
PowerShell process exits non-zero and Task Scheduler can detect the failure.

In `@src/whatsapp.ts`:
- Line 217: Replace the Portuguese comment "// Salva o pushName do remetente
sempre que disponível" with English to match the codebase; specifically change
it to "// Store sender's pushName whenever available" near the code that handles
or stores the pushName (the pushName-related comment in the message handling
block).
- Around line 217-223: The code that saves pushName uses senderJid derived from
msg.key.participant || (!msg.key.fromMe ? msg.key.remoteJid : null) without
guarding against group JIDs; update the logic used before calling
storeContact/jidNormalizedUser to mirror parseMessageForDb by checking
isJidGroup(msg.key.remoteJid) and only use msg.key.remoteJid as the sender when
it is not a group (i.e., !isJidGroup(msg.key.remoteJid)); if remoteJid is a
group prefer msg.key.participant (and still skip when null) so you never store a
group JID as a contact.
- Around line 197-206: The loop processing events["contacts.upsert"] calls
storeContact for each contact without error handling; wrap the per-contact call
to storeContact inside a try/catch so a single failing store doesn't abort the
whole loop, log the error (including contact.id or contact data) and continue to
the next contact; update the block handling events["contacts.upsert"] and ensure
the catch uses the existing logger or error reporting mechanism to record the
exception and context.

In `@triagem.js`:
- Around line 24-27: classificar currently only inspects messages[0] so urgent
content earlier in the batch is ignored; update classificar to examine the whole
recent messages array (produced by lerMensagens24h) by computing a combined
normalized string or scanning each message's content (e.g., map messages ->
content?.toLowerCase()) and use that aggregate when deriving lastMsg, urgency
checks, and downstream logic referenced in the same function (affecting the
checks around lines 42-72). Replace the single lastMsg usage with a combined
variable (e.g., recentMessagesText or any-urgent boolean) and ensure all
conditional checks (group detection, urgency keyword matching, fallback
heuristics) consult this combined representation so any earlier urgent message
in the batch will mark the chat as urgent.
- Around line 97-121: The current triage selects recent incoming messages but
doesn't restrict to unread conversations; update the SQL in chatsComMensagens
and the per-chat messages query to filter on an unread flag (e.g., add WHERE
c.unread_count > 0 in the outer query or WHERE EXISTS (SELECT 1 FROM messages m
WHERE m.chat_jid = c.jid AND m.unread = 1) and add AND m.unread = 1 to the
messages query), or if the schema lacks an unread column, add a stable unread
state to the schema (e.g., messages.unread BOOLEAN or chats.unread_count INT)
and run a migration, then use that column in the queries (refer to
chatsComMensagens and the messages SELECT block) — if you cannot add unread
state now, change the PR description/tests to “recent incoming messages” instead
of “unread-only triage.”
- Around line 136-145: formatMsg currently injects unescaped WhatsApp data
(nome, preview and elsewhere junkNomes) directly into an HTML string, enabling
HTML injection; update the code to HTML-escape these values before interpolation
(create or reuse an escapeHtml function and call it on nome, preview, and
junkNomes), then use the escaped values in the template returned by formatMsg
and at the other occurrences referenced (lines around 181-182) so no raw
user-controlled text is placed into the HTML.
- Around line 194-200: Replace the non-atomic exists/read check for LOCK_PATH
with an atomic lock acquisition: try to create an exclusive lock (e.g. use
fs.openSync(lockAcquirePath, "wx") or fs.mkdirSync(lockDir, { mode: 0o700 }))
before sendMail; if that open/mkdir fails with EEXIST then read LOCK_PATH and if
it already contains hoje exit, otherwise another process holds the lock so exit.
On successful exclusive creation, write hoje into LOCK_PATH (fs.writeFileSync)
and close the lock handle, then call sendMail; if sendMail fails remove the
LOCK_PATH (fs.unlinkSync) so retries can proceed; also apply the same atomic
lock logic to the other check block around lines 242-250. Use functions
fs.openSync/fs.closeSync/fs.writeFileSync/fs.unlinkSync (or async equivalents)
and handle EEXIST errors explicitly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3fbc6055-bb76-40e5-a2c5-33f6558d072f

📥 Commits

Reviewing files that changed from the base of the PR and between ce9b144 and 5c7cb54.

📒 Files selected for processing (6)
  • .gitignore
  • package.json
  • rodar-triagem.ps1
  • src/whatsapp.ts
  • triagem-config.example.json
  • triagem.js

Comment thread package.json
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"@whiskeysockets/baileys": "^6.7.16",
"nodemailer": "^8.0.7",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package.json dependency section
ls -la
rg -n "\"nodemailer\"" package.json || true

# Check if lockfiles exist (helps understand resolution expectations)
ls -ლა package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null || true

# If a lockfile exists, show the resolved nodemailer version
if [ -f package-lock.json ]; then
  rg -n "\"nodemailer\"" package-lock.json | head -n 50 || true
fi
if [ -f yarn.lock ]; then
  rg -n "nodemailer@|nodemailer " yarn.lock | head -n 50 || true
fi
if [ -f pnpm-lock.yaml ]; then
  rg -n "nodemailer@" pnpm-lock.yaml | head -n 50 || true
fi

Repository: jlucaso1/whatsapp-mcp-ts

Length of output: 1115


🌐 Web query:

Nodemailer npm versions 8.0.7 published?

💡 Result:

Yes, Nodemailer version 8.0.7 was officially published to the npm registry on April 27, 2026 [1][2][3]. This version included a bug fix to maintain domains as UTF-8 when the local part of an email address contains non-ASCII characters [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show how npm resolved nodemailer in the lockfile (authoritative for this repo)
echo "== package-lock nodemailer entries =="
rg -n "\"nodemailer\"|nodemailer@" package-lock.json | head -n 80 || true

echo
echo "== package-lock resolved nodemailer version (if present) =="
# Extract the first matching dependency object with its version
python3 - <<'PY'
import json
from pathlib import Path

p=Path("package-lock.json")
data=json.loads(p.read_text())

def walk(obj):
    if isinstance(obj, dict):
        for k,v in obj.items():
            if k=="nodemailer":
                if isinstance(v, dict) and "version" in v:
                    print("nodemailer version:", v["version"])
                elif isinstance(v, str):
                    print("nodemailer version:", v)
            walk(v)
    elif isinstance(obj, list):
        for it in obj:
            walk(it)

walk(data)
PY

Repository: jlucaso1/whatsapp-mcp-ts

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== occurrences of 'nodemailer' in repo =="
rg -n "nodemailer" . || true

echo
echo "== occurrences of 'nodemailer' in package-lock.json (raw substring) =="
rg -n "nodemailer" package-lock.json || true

echo
echo "== show top of package-lock.json to inspect lockfile format =="
head -n 60 package-lock.json

echo
echo "== try extracting dependency keys at the top level (small JSON scan) =="
python3 - <<'PY'
import json
from pathlib import Path
data=json.loads(Path("package-lock.json").read_text())
print("lockfileVersion:", data.get("lockfileVersion"))
print("top keys:", list(data.keys())[:30])
for k in ["packages","dependencies"]:
    if k in data:
        print(f"has '{k}':", type(data[k]).__name__)
PY

Repository: jlucaso1/whatsapp-mcp-ts

Length of output: 2733


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json =="
cat -n package.json

echo
echo "== package-lock.json: dependencies under packages[''] =="
python3 - <<'PY'
import json
from pathlib import Path
d=json.loads(Path("package-lock.json").read_text())
root=d.get("packages",{}).get("",{})
print("root dependencies:", root.get("dependencies",{}))
print("root devDependencies:", list(root.get("devDependencies",{}).keys())[:20])
PY

echo
echo "== quick check: does package-lock mention nodemailer anywhere (case-sensitive substring)? =="
rg -n "nodemailer" package-lock.json || true

Repository: jlucaso1/whatsapp-mcp-ts

Length of output: 1185


🌐 Web query:

site:npmjs.com nodemailer 8.0.7

💡 Result:

As of June 1, 2026, version 8.0.7 of the nodemailer package is a valid and published version [1][2]. It is part of the 8.x series, which began in February 2026 [3]. The official nodemailer package on the npm registry indicates that version 8.0.10 was the most recent release as of May 29, 2026 [3]. Version 8.0.7 was released shortly before this, approximately mid-May 2026 [1][2]. You can verify the availability and details of this version directly on the official npm registry page for nodemailer [3].

Citations:


🌐 Web query:

https://registry.npmjs.org/nodemailer/8.0.7

💡 Result:

Nodemailer version 8.0.7 is a valid, released version of the popular Node.js email library [1][2]. It was published to the npm registry on April 27, 2026 [3][4][5]. The 8.0.7 release primarily included a bug fix to ensure that the domain remains in UTF-8 format when the local part of an email address is non-ASCII [4][6]. As of today, June 1, 2026, version 8.0.7 has been superseded by newer releases (up to version 8.0.10, which was published on May 29, 2026) [1][2][5]. You can view the full release history and changelogs on the official Nodemailer GitHub repository [4][6].

Citations:


🌐 Web query:

npm ci package-lock.json not up to date with package.json error message

💡 Result:

The error message "npm ci can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync" occurs when npm determines that the dependency definitions in your package.json do not match the resolved tree stored in your package-lock.json [1][2][3]. Because npm ci is designed for automated environments to perform strict, reproducible installs, it refuses to proceed if it detects any discrepancy, effectively forcing the developer to resolve the conflict [4][5]. Common Causes: - Manual Changes: You edited package.json (e.g., changing a version range or adding a dependency) but did not run npm install to update the lockfile [1][6]. - Out-of-Sync Lockfile: A previous npm install process created a lockfile that does not perfectly satisfy the requirements defined in package.json [2][3]. - Registry Updates: New versions of dependencies have been published to the registry that, while satisfying the semver range in package.json, do not match the specific versions pinned in your existing package-lock.json [7]. - Environmental Issues: In some cases, bugs in specific versions of npm have caused legitimate lockfiles to be flagged as out of sync [2][8]. Recommended Solutions: 1. Regenerate the Lockfile: The most reliable fix is to ensure your environment is clean. Run the following sequence in your project root: rm -rf node_modules package-lock.json npm install This forces npm to resolve dependencies from scratch based on your current package.json and generate a fresh, synchronized lockfile [2][8][4]. 2. Update Your Project: Ensure you are using a modern version of npm, as older versions had known bugs regarding lockfile synchronization [9]. 3. Debugging: If the error persists, check the specific details provided in the error output (e.g., "Invalid: lock file's pkg@x.y.z does not satisfy pkg@a.b.c"). This will tell you exactly which package is causing the mismatch [2][3]. Best Practices for CI/CD: - Never manually edit package-lock.json [4]. - Always commit your package-lock.json to version control [4]. - Use npm ci exclusively in your CI/CD pipelines to ensure the build environment uses the exact dependencies specified in the lockfile [10][4].

Citations:


Sync package-lock.json with nodemailer dependency

  • nodemailer version 8.0.7 is published on npm, so the dependency version change isn’t necessary.
  • package-lock.json contains no nodemailer entries (it isn’t listed under packages[""].dependencies), so npm ci will fail with a package.json/package-lock.json mismatch. Regenerate and commit package-lock.json via npm install (or npm install --package-lock-only) so nodemailer is included.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 13, package.json adds "nodemailer": "^8.0.7" but
package-lock.json lacks any nodemailer entry causing npm ci to fail; run npm
install (or npm install --package-lock-only) to regenerate package-lock.json so
nodemailer is recorded, verify packages[""].dependencies includes nodemailer,
then commit the updated package-lock.json alongside the package.json change to
keep package.json/package-lock.json in sync.

Comment thread rodar-triagem.ps1
Comment on lines +1 to +3
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Set-Location "C:\Servicos\whatsapp-mcp"
& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Consider parameterizing hardcoded paths for maintainability.

The script contains three hardcoded absolute paths that make it fragile to directory structure changes or Node.js reinstallation. While acceptable for initial deployment, parameterizing these would improve portability and make it easier to deploy on different machines or update Node.js versions.

♻️ Optional refactor to use script parameters
+param(
+  [string]$ProjectDir = "C:\Servicos\whatsapp-mcp",
+  [string]$NodePath = "C:\Program Files\nodejs\node.exe"
+)
+
 $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
-Set-Location "C:\Servicos\whatsapp-mcp"
-& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8
+Set-Location $ProjectDir
+& $NodePath triagem.js 2>&1 | Out-File "$ProjectDir\triagem-log.txt" -Encoding utf8

Task Scheduler can pass parameters via "Add arguments" field, or the script can rely on defaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rodar-triagem.ps1` around lines 1 - 3, The script currently hardcodes three
absolute paths (the Node binary path "C:\Program Files\nodejs\node.exe", the
project directory used by Set-Location "C:\Servicos\whatsapp-mcp", and the log
file path "C:\Servicos\whatsapp-mcp\triagem-log.txt"); change triagem.ps1 to
accept these as parameters (e.g., $NodePath, $ProjectDir, $LogFile) with
sensible defaults and then replace direct usages of Set-Location, the node
executable invocation, and Out-File with those parameters while keeping the
existing behavior of appending stderr/stdout to the log and preserving the
$env:Path setup; reference symbols to update: $env:Path, Set-Location,
triagem.js invocation (& "...node.exe" triagem.js ...), and Out-File.

Comment thread rodar-triagem.ps1
Comment on lines +2 to +3
Set-Location "C:\Servicos\whatsapp-mcp"
& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add error handling to ensure Task Scheduler detects failures.

The script has no error handling for directory changes or Node.js execution. If Set-Location fails or node.exe exits with a non-zero code, the PowerShell script still exits successfully and Task Scheduler records the run as successful. This prevents automated failure detection and alerting.

🛡️ Proposed fix to add error handling and propagate exit codes
+$ErrorActionPreference = "Stop"
+
 $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
-Set-Location "C:\Servicos\whatsapp-mcp"
-& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8
+
+try {
+  Set-Location "C:\Servicos\whatsapp-mcp"
+  & "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8 -Append
+  exit $LASTEXITCODE
+} catch {
+  Write-Error "Triagem script failed: $_"
+  exit 1
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rodar-triagem.ps1` around lines 2 - 3, The script currently lacks error
handling around Set-Location and the Node invocation (the & "C:\Program
Files\nodejs\node.exe" triagem.js ... pipeline), so failures aren't propagated
to Task Scheduler; modify the script to validate Set-Location (wrap in try/catch
or test with Test-Path and throw/exit on failure), run Node with a method that
exposes its exit code (use Start-Process -FilePath "C:\Program
Files\nodejs\node.exe" -ArgumentList "triagem.js" -Wait -NoNewWindow or capture
$LASTEXITCODE after the call), write stderr/stdout to triagem-log.txt, and if
Node returns a non-zero exit code then write an error entry to the log and call
exit $LASTEXITCODE (or throw) so the PowerShell process exits non-zero and Task
Scheduler can detect the failure.

Comment thread rodar-triagem.ps1
@@ -0,0 +1,3 @@
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Set-Location "C:\Servicos\whatsapp-mcp"
& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Log file is overwritten on each run, losing execution history.

Using Out-File without -Append overwrites triagem-log.txt every time the script runs. This eliminates historical logs that are valuable for debugging intermittent Task Scheduler issues, tracking when sends were skipped due to the daily lock, or diagnosing email delivery failures.

📝 Proposed fix to append logs instead of overwriting
-& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8
+& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8 -Append
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8
& "C:\Program Files\nodejs\node.exe" triagem.js 2>&1 | Out-File "C:\Servicos\whatsapp-mcp\triagem-log.txt" -Encoding utf8 -Append
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rodar-triagem.ps1` at line 3, The current invocation line uses Out-File which
overwrites triagem-log.txt on each run; change the command that runs node (the
line calling "node.exe" triagem.js piped to Out-File "triagem-log.txt") to use
Out-File with -Append (or an equivalent append operator) so new output is
appended to C:\Servicos\whatsapp-mcp\triagem-log.txt instead of replacing it,
preserving historical logs for triagem.js runs.

Comment thread src/whatsapp.ts
Comment on lines +197 to +206
if (events["contacts.upsert"]) {
for (const contact of events["contacts.upsert"]) {
storeContact({
jid: contact.id,
name: contact.name ?? null,
notify: contact.notify ?? null,
phoneNumber: (contact as any).phoneNumber ?? null,
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add error handling around storeContact calls.

If storeContact throws an exception (e.g., database constraint violation, connection issue), the entire event-processing loop could fail and prevent subsequent events from being handled.

🛡️ Proposed fix with error handling and logging
 if (events["contacts.upsert"]) {
+  logger.info(`Storing ${events["contacts.upsert"].length} upserted contacts.`);
+  let stored = 0;
   for (const contact of events["contacts.upsert"]) {
-    storeContact({
-      jid: contact.id,
-      name: contact.name ?? null,
-      notify: contact.notify ?? null,
-      phoneNumber: (contact as any).phoneNumber ?? null,
-    });
+    try {
+      storeContact({
+        jid: contact.id,
+        name: contact.name ?? null,
+        notify: contact.notify ?? null,
+        phoneNumber: (contact as any).phoneNumber ?? null,
+      });
+      stored++;
+    } catch (error) {
+      logger.error({ err: error, contactId: contact.id }, "Failed to store contact");
+    }
   }
+  logger.info(`Stored ${stored} upserted contacts.`);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (events["contacts.upsert"]) {
for (const contact of events["contacts.upsert"]) {
storeContact({
jid: contact.id,
name: contact.name ?? null,
notify: contact.notify ?? null,
phoneNumber: (contact as any).phoneNumber ?? null,
});
}
}
if (events["contacts.upsert"]) {
logger.info(`Storing ${events["contacts.upsert"].length} upserted contacts.`);
let stored = 0;
for (const contact of events["contacts.upsert"]) {
try {
storeContact({
jid: contact.id,
name: contact.name ?? null,
notify: contact.notify ?? null,
phoneNumber: (contact as any).phoneNumber ?? null,
});
stored++;
} catch (error) {
logger.error({ err: error, contactId: contact.id }, "Failed to store contact");
}
}
logger.info(`Stored ${stored} upserted contacts.`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/whatsapp.ts` around lines 197 - 206, The loop processing
events["contacts.upsert"] calls storeContact for each contact without error
handling; wrap the per-contact call to storeContact inside a try/catch so a
single failing store doesn't abort the whole loop, log the error (including
contact.id or contact data) and continue to the next contact; update the block
handling events["contacts.upsert"] and ensure the catch uses the existing logger
or error reporting mechanism to record the exception and context.

Comment thread src/whatsapp.ts
Comment on lines +217 to +223
// Salva o pushName do remetente sempre que disponível
if (msg.pushName && msg.key?.remoteJid) {
const senderJid = msg.key.participant || (!msg.key.fromMe ? msg.key.remoteJid : null);
if (senderJid) {
storeContact({ jid: jidNormalizedUser(senderJid), notify: msg.pushName });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent storing group JIDs as contacts.

Line 219 derives the sender JID without checking whether remoteJid is a group. This differs from the logic in parseMessageForDb (lines 79-85), which validates !isJidGroup(msg.key.remoteJid) before using remoteJid as the sender.

When processing group messages, this could incorrectly store the group's JID as a contact instead of the participant's JID, potentially corrupting the contact database or causing issues in downstream consumers like the triage script.

🔧 Proposed fix aligning with parseMessageForDb logic
 // Salva o pushName do remetente sempre que disponível
 if (msg.pushName && msg.key?.remoteJid) {
-  const senderJid = msg.key.participant || (!msg.key.fromMe ? msg.key.remoteJid : null);
+  let senderJid = msg.key.participant;
+  if (!msg.key.fromMe && !senderJid && !isJidGroup(msg.key.remoteJid)) {
+    senderJid = msg.key.remoteJid;
+  }
   if (senderJid) {
-    storeContact({ jid: jidNormalizedUser(senderJid), notify: msg.pushName });
+    try {
+      storeContact({ jid: jidNormalizedUser(senderJid), notify: msg.pushName });
+    } catch (error) {
+      logger.error({ err: error, senderJid }, "Failed to store contact pushName");
+    }
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Salva o pushName do remetente sempre que disponível
if (msg.pushName && msg.key?.remoteJid) {
const senderJid = msg.key.participant || (!msg.key.fromMe ? msg.key.remoteJid : null);
if (senderJid) {
storeContact({ jid: jidNormalizedUser(senderJid), notify: msg.pushName });
}
}
// Salva o pushName do remetente sempre que disponível
if (msg.pushName && msg.key?.remoteJid) {
let senderJid = msg.key.participant;
if (!msg.key.fromMe && !senderJid && !isJidGroup(msg.key.remoteJid)) {
senderJid = msg.key.remoteJid;
}
if (senderJid) {
try {
storeContact({ jid: jidNormalizedUser(senderJid), notify: msg.pushName });
} catch (error) {
logger.error({ err: error, senderJid }, "Failed to store contact pushName");
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/whatsapp.ts` around lines 217 - 223, The code that saves pushName uses
senderJid derived from msg.key.participant || (!msg.key.fromMe ?
msg.key.remoteJid : null) without guarding against group JIDs; update the logic
used before calling storeContact/jidNormalizedUser to mirror parseMessageForDb
by checking isJidGroup(msg.key.remoteJid) and only use msg.key.remoteJid as the
sender when it is not a group (i.e., !isJidGroup(msg.key.remoteJid)); if
remoteJid is a group prefer msg.key.participant (and still skip when null) so
you never store a group JID as a contact.

Comment thread triagem.js
Comment on lines +24 to +27
function classificar(chat, messages, config) {
const chatName = (chat.name || chat.jid || "").toLowerCase();
const isGroup = chat.jid.endsWith("@g.us");
const lastMsg = messages[0]?.content?.toLowerCase() || "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Classify against the whole recent message batch, not only messages[0].

lerMensagens24h() fetches up to 5 incoming messages per chat, but classificar() only inspects the latest one. A chat with an urgent earlier message followed by a short acknowledgment gets downgraded incorrectly.

🧭 Proposed fix
 function classificar(chat, messages, config) {
   const chatName = (chat.name || chat.jid || "").toLowerCase();
   const isGroup = chat.jid.endsWith("`@g.us`");
-  const lastMsg = messages[0]?.content?.toLowerCase() || "";
+  const recentContent = messages
+    .map((message) => message.content?.toLowerCase() || "")
+    .join("\n");
@@
-  if (junkPatterns.some((p) => p.test(lastMsg))) return { nivel: "junk", tag: "" };
+  if (junkPatterns.some((p) => p.test(recentContent))) return { nivel: "junk", tag: "" };
@@
-  if (urgentPatterns.some((p) => p.test(lastMsg))) return { nivel: "urgente", tag: "conteúdo" };
+  if (urgentPatterns.some((p) => p.test(recentContent))) return { nivel: "urgente", tag: "conteúdo" };
@@
-  if (importantPatterns.some((p) => p.test(lastMsg))) return { nivel: "importante", tag: "" };
+  if (importantPatterns.some((p) => p.test(recentContent))) return { nivel: "importante", tag: "" };

Also applies to: 42-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@triagem.js` around lines 24 - 27, classificar currently only inspects
messages[0] so urgent content earlier in the batch is ignored; update
classificar to examine the whole recent messages array (produced by
lerMensagens24h) by computing a combined normalized string or scanning each
message's content (e.g., map messages -> content?.toLowerCase()) and use that
aggregate when deriving lastMsg, urgency checks, and downstream logic referenced
in the same function (affecting the checks around lines 42-72). Replace the
single lastMsg usage with a combined variable (e.g., recentMessagesText or
any-urgent boolean) and ensure all conditional checks (group detection, urgency
keyword matching, fallback heuristics) consult this combined representation so
any earlier urgent message in the batch will mark the chat as urgent.

Comment thread triagem.js
Comment on lines +97 to +121
const chatsComMensagens = db.prepare(`
SELECT DISTINCT
c.jid,
COALESCE(c.name, ct.name, ct.notify, ct.phone_number, c.jid) as name,
c.last_message_time,
(SELECT COUNT(*) FROM messages m WHERE m.chat_jid = c.jid AND m.timestamp >= ? AND m.is_from_me = 0) as msg_count
FROM chats c
LEFT JOIN contacts ct ON c.jid = ct.jid
WHERE c.jid IN (
SELECT DISTINCT chat_jid FROM messages
WHERE timestamp >= ? AND is_from_me = 0
)
ORDER BY c.last_message_time DESC
`).all(since, since);

const resultado = [];

for (const chat of chatsComMensagens) {
const messages = db.prepare(`
SELECT content, timestamp, sender, is_from_me
FROM messages
WHERE chat_jid = ? AND timestamp >= ? AND is_from_me = 0
ORDER BY timestamp DESC
LIMIT 5
`).all(chat.jid, since);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The query does not implement unread-only triage.

Both queries filter on timestamp and is_from_me, but nothing distinguishes already-read chats/messages. With the current schema shown in src/database.ts:44-63, this script will include every incoming conversation from the last 24 hours, even if the user already handled it.

If unread-only is still the requirement, this needs an upstream unread-state contract before merge; otherwise the PR description and test plan should be narrowed to “recent incoming messages” instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@triagem.js` around lines 97 - 121, The current triage selects recent incoming
messages but doesn't restrict to unread conversations; update the SQL in
chatsComMensagens and the per-chat messages query to filter on an unread flag
(e.g., add WHERE c.unread_count > 0 in the outer query or WHERE EXISTS (SELECT 1
FROM messages m WHERE m.chat_jid = c.jid AND m.unread = 1) and add AND m.unread
= 1 to the messages query), or if the schema lacks an unread column, add a
stable unread state to the schema (e.g., messages.unread BOOLEAN or
chats.unread_count INT) and run a migration, then use that column in the queries
(refer to chatsComMensagens and the messages SELECT block) — if you cannot add
unread state now, change the PR description/tests to “recent incoming messages”
instead of “unread-only triage.”

Comment thread triagem.js
Comment on lines +136 to +145
const formatMsg = (item) => {
const nome = item.chat.name || formatarJid(item.chat.jid);
const ultima = item.messages[0]?.content || "";
const preview = ultima.length > 70 ? ultima.substring(0, 70) + "..." : ultima;
const emoji = item.chat.jid.endsWith("@g.us") ? "👥" : "👤";
const tagStr = item.tag ? ` <span style="font-size:11px;color:#888;font-weight:normal;">(${item.tag})</span>` : "";
const count = item.chat.msg_count || 0;
const countStr = count > 5 ? ` <span style="font-size:11px;color:#aaa;">(${count} mensagens)</span>` : "";
return `${emoji} <b>${nome}</b>${tagStr} — "${preview}"${countStr}`;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape contact names and message previews before interpolating them into HTML.

nome, preview, and junkNomes come from WhatsApp data and are inserted raw into the email body. A crafted contact name or message like <a href="...">...</a> can alter the rendered email and spoof links/content.

🛡️ Proposed fix
 function formatarEmail(grupos, data) {
   const { urgente, importante, pode_esperar, junk } = grupos;
+
+  const escapeHtml = (value = "") =>
+    value
+      .replaceAll("&", "&amp;")
+      .replaceAll("<", "&lt;")
+      .replaceAll(">", "&gt;")
+      .replaceAll('"', "&quot;")
+      .replaceAll("'", "&`#39`;");
 
   const formatMsg = (item) => {
-    const nome = item.chat.name || formatarJid(item.chat.jid);
+    const nome = escapeHtml(item.chat.name || formatarJid(item.chat.jid));
     const ultima = item.messages[0]?.content || "";
-    const preview = ultima.length > 70 ? ultima.substring(0, 70) + "..." : ultima;
+    const previewRaw = ultima.length > 70 ? ultima.substring(0, 70) + "..." : ultima;
+    const preview = escapeHtml(previewRaw);
     const emoji = item.chat.jid.endsWith("`@g.us`") ? "👥" : "👤";
@@
   if (junk.length > 0) {
-    const junkNomes = junk.map(item => item.chat.name || formatarJid(item.chat.jid)).join(", ");
+    const junkNomes = junk
+      .map((item) => escapeHtml(item.chat.name || formatarJid(item.chat.jid)))
+      .join(", ");
     html += `<p style="color:`#999`;font-size:12px;margin-top:20px;">⚪ <b>Junk ignorado (${junk.length}):</b> ${junkNomes}</p>`;
   }

Also applies to: 181-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@triagem.js` around lines 136 - 145, formatMsg currently injects unescaped
WhatsApp data (nome, preview and elsewhere junkNomes) directly into an HTML
string, enabling HTML injection; update the code to HTML-escape these values
before interpolation (create or reuse an escapeHtml function and call it on
nome, preview, and junkNomes), then use the escaped values in the template
returned by formatMsg and at the other occurrences referenced (lines around
181-182) so no raw user-controlled text is placed into the HTML.

Comment thread triagem.js
Comment on lines +194 to +200
const agora = new Date().toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo" });
const hoje = new Date().toLocaleDateString("pt-BR", { timeZone: "America/Sao_Paulo" });

if (fs.existsSync(LOCK_PATH) && fs.readFileSync(LOCK_PATH, "utf-8").trim() === hoje) {
console.log(`⏭️ Triagem já enviada hoje (${hoje}). Pulando.`);
process.exit(0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Acquire the daily lock atomically before sending.

The recommended scheduling uses both an 08:00 trigger and a logon fallback. Two runs can pass the existsSync/readFileSync check before either writes triagem-ultimo-envio.txt, so both send the email. That breaks the “one send per day” guarantee.

Use an atomic lock acquisition strategy here, e.g. an exclusive per-day lock file or open(..., "wx")/mkdir before sendMail, and release it on failure so retries still work.

Also applies to: 242-250

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@triagem.js` around lines 194 - 200, Replace the non-atomic exists/read check
for LOCK_PATH with an atomic lock acquisition: try to create an exclusive lock
(e.g. use fs.openSync(lockAcquirePath, "wx") or fs.mkdirSync(lockDir, { mode:
0o700 })) before sendMail; if that open/mkdir fails with EEXIST then read
LOCK_PATH and if it already contains hoje exit, otherwise another process holds
the lock so exit. On successful exclusive creation, write hoje into LOCK_PATH
(fs.writeFileSync) and close the lock handle, then call sendMail; if sendMail
fails remove the LOCK_PATH (fs.unlinkSync) so retries can proceed; also apply
the same atomic lock logic to the other check block around lines 242-250. Use
functions fs.openSync/fs.closeSync/fs.writeFileSync/fs.unlinkSync (or async
equivalents) and handle EEXIST errors explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant