feat: add daily WhatsApp triage script with email delivery - #18
feat: add daily WhatsApp triage script with email delivery#18barbararoque98 wants to merge 1 commit into
Conversation
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>
📝 WalkthroughWalkthroughThis 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. ChangesEmail Triage Workflow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
.gitignorepackage.jsonrodar-triagem.ps1src/whatsapp.tstriagem-config.example.jsontriagem.js
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.8.0", | ||
| "@whiskeysockets/baileys": "^6.7.16", | ||
| "nodemailer": "^8.0.7", |
There was a problem hiding this comment.
🧩 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
fiRepository: 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:
- 1: https://github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md
- 2: https://github.com/nodemailer/nodemailer/releases
- 3: https://security.snyk.io/package/npm/nodemailer
🏁 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)
PYRepository: 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__)
PYRepository: 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 || trueRepository: 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:
- 1: https://www.npmjs.com/search?page=2&perPage=20&q=Prometheus+Investment+Alliance+B1+amenities+review&UEWhK=EnnRB2b
- 2: https://www.npmjs.com/search?q=Zraox+im+Kontext+von+Scam-Diskussionen+richtig+bewerten
- 3: https://www.npmjs.com/package/nodemailer
🌐 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:
- 1: https://registry.npmjs.org/nodemailer
- 2: https://www.npmjs.com/package/nodemailer
- 3: https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md
- 4: https://github.com/nodemailer/nodemailer/releases
- 5: https://security.snyk.io/package/npm/nodemailer
- 6: nodemailer/nodemailer@v8.0.5...v8.0.7
🌐 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:
- 1: [BUG] npm ci check fails to notice package.json dependencies update npm/cli#7000
- 2: [BUG]
npm cifails becausenpm installproduces an out-of-sync lockfile (regression since v7.0.9) npm/cli#8726 - 3: [BUG]
npm installcreates unsyncpackage-lock.jsonnpm/cli#6787 - 4: https://dev.to/armorbreak/npm-scripts-and-packagejson-the-complete-guide-2026-4m9j
- 5: https://stackoverflow.com/questions/45022048/why-does-npm-install-rewrite-package-lock-json
- 6: https://stackoverflow.com/questions/73506013/how-to-check-whether-package-lock-json-out-of-date
- 7: [BUG]
npm cifails with "package.json and package-lock.json are out of sync" when new patch versions are published npm/cli#8693 - 8: [BUG] npm ci fails with newer versions of npm npm/cli#8767
- 9: package-lock.json file not updated after package.json file is changed npm/npm#16866
- 10: https://dev.to/sohanaakbar7/docker-caching-strategies-that-actually-work-with-npm-ci-4ja4
Sync package-lock.json with nodemailer dependency
nodemailerversion8.0.7is published on npm, so the dependency version change isn’t necessary.package-lock.jsoncontains nonodemailerentries (it isn’t listed underpackages[""].dependencies), sonpm ciwill fail with a package.json/package-lock.json mismatch. Regenerate and commitpackage-lock.jsonvianpm install(ornpm install --package-lock-only) sonodemaileris 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.
| $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 |
There was a problem hiding this comment.
🧹 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 utf8Task 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.
| 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 |
There was a problem hiding this comment.
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.
| @@ -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 | |||
There was a problem hiding this comment.
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.
| & "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.
| 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, | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // 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 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| 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() || ""; |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.”
| 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}`; | ||
| }; |
There was a problem hiding this comment.
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("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
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/Gmailrodar-triagem.ps1: PowerShell wrapper for Windows Task Schedulersrc/whatsapp.ts: two contact-resolution improvements — handlescontacts.upsertevents to persist contacts on sync, and capturespushNamefrom every incoming message so LID-format contacts resolve to display names over timetriagem-config.example.json: credential-free config template; actualtriagem-config.jsonis excluded via.gitignorepackage.json: addsnodemailerdependency.gitignore: excludes credentials file, lock file and debug scriptsScheduling
Two Windows Task Scheduler triggers guarantee the email is sent at least once per day when the machine is on:
A lock file (
triagem-ultimo-envio.txt) prevents duplicate sends within the same day.Test plan
triagem-config.example.jsontotriagem-config.jsonand fill in credentialsnode triagem.jsmanually and confirm email arrives🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores