A local, read-only Playwright application that exports conversations from Intertel Conversa to Excel, CSV, JSON and JSONL - preserving each chat exactly as displayed.
How it works · Safety · Getting started · Commands · Speed · Output · Troubleshooting
This project is not affiliated with or endorsed by Intertel.
Intertel's application DOM is private and not known in advance, so nothing is hardcoded. You point the exporter at each region of the interface once, it saves stable selectors locally, and every later run reads the interface through those selectors.
flowchart TB
subgraph setup["One-time setup"]
direction LR
LOGIN["Manual login"] --> PROFILE[("Browser profile<br/>data/browser-profile")]
CALIBRATE["Selector calibration"] --> SELECTORS[("Selectors<br/>config/selectors.json")]
end
subgraph runtime["Export runtime"]
direction LR
DISCOVER["Discover conversations"] --> QUEUE["Build work queue"]
QUEUE --> WORKERS["Scrape in worker tabs"]
CHECK["Check completeness"]
end
subgraph storage["Local persistence and output"]
direction LR
RAW[("Raw records<br/>data/raw")]
CHECKPOINT[("Checkpoint<br/>data/checkpoints")]
WRITERS["Output writers"]
FILES[["Excel · CSV · JSON · JSONL<br/>output"]]
WRITERS --> FILES
end
PROFILE --> DISCOVER
SELECTORS --> DISCOVER
WORKERS --> RAW
WORKERS --> CHECKPOINT
RAW --> CHECK
CHECKPOINT --> CHECK
CHECK --> WRITERS
RAW --> VALIDATE["Validate later"]
CHECKPOINT --> VALIDATE
VALIDATE --> WRITERS
All persistent data stays local. Raw records and the checkpoint are the source of truth; generated
files can be rebuilt with npm run validate.
| Path | Responsibility |
|---|---|
src/cli.ts |
Commands, the export run loop and the parallel worker pool |
src/browser/ |
Persistent Chromium profile, session, and the in-page panel |
src/calibration/ |
Guided selector capture, channel detection, selector validation |
src/scraper/ |
Discovery, history walking, DOM extraction, parsing, completeness |
src/storage/ |
Atomic raw records and the resumable checkpoint |
src/export/ |
Excel, CSV, JSON, JSONL writers and the live run progress model |
src/utils/ |
Atomic file writes, hashing, retries, time and error classification |
Each queued conversation follows the same path. With --concurrency, multiple worker tabs run this
path at the same time and share the serialized checkpoint.
flowchart TD
START(["npm run export"]) --> PRE["Validate session and selectors"]
PRE --> DISCOVER["Discover the complete conversation list"]
DISCOVER --> PLAN["Apply resume, retry and limit options"]
subgraph worker["Worker pool · one conversation per tab"]
direction TB
OPEN["Relocate and open conversation"] --> CHANNEL["Detect its channel"]
CHANNEL --> WALK["Walk message history to the oldest edge"]
WALK --> RESULT{"Outcome"}
RESULT -- "complete" --> COMPLETE["Save complete record"]
RESULT -- "partial" --> PARTIAL["Save partial record and diagnostic"]
RESULT -- "failed" --> FAILED["Save failure diagnostic"]
end
PLAN --> OPEN
COMPLETE --> PERSIST["Persist result and update checkpoint"]
PARTIAL --> PERSIST
FAILED --> PERSIST
PERSIST --> DRAIN["Continue until the queue is empty and all workers finish"]
DRAIN --> WRITE["Write Excel, CSV, JSON and JSONL"]
WRITE --> VERDICT{"Completeness proven?"}
VERDICT -- "yes" --> DONE(["Complete"])
VERDICT -- "no" --> INCOMPLETE(["Incomplete · review diagnostics"])
Reading a conversation is where the time goes, so a whole cycle - scroll, wait for the panel to settle, read every row - happens in a single call into the page. Messages already captured come back as bare ids instead of their text, so a long chat stops re-sending what the scraper holds.
sequenceDiagram
autonumber
participant N as Node
participant P as Page
N->>P: advanceHistoryCycle(knownIds, quietMs)
activate P
P->>P: scroll up (jump to top, or one screen)
P->>P: MutationObserver waits until the panel is quiet
P->>P: read every visible row
P-->>N: new rows + ids of known rows + scroll metrics + alerts
deactivate P
N->>N: build records for new rows only
N->>N: merge into the ordered transcript
Note over N,P: repeat until the top is reached or the guard trips
The exporter uses your normal authenticated browser session and only reads the conversation UI. It does not send messages, change statuses, assign conversations, alter labels, archive data, or bypass authentication.
- It exports whatever the calibrated conversation list contains, whichever channel that is. Each
conversation's channel is detected and recorded on the record, but it is not a filter, so
calibrate against the section you actually want to export. Facebook, Messenger, WhatsApp,
Instagram, Telegram, SMS, email and web chat are recognised by name; anything else is exported
just the same and recorded as
unknown. - The only thing written into the page is the exporter's own guidance panel and temporary outlines around the element you are calibrating or the conversation being exported. Those styles are restored as soon as the step or the run moves on.
- Conversation data stays on your machine and is never sent to a language or text-processing provider. Cookies and tokens are never printed or exported.
Prerequisites - Node.js 20+, npm, and permission to access and export the client data.
npm install
npx playwright install chromium
Copy-Item .env.example .envReview .env before running, then work through the four steps below.
flowchart LR
A["1 · Log in manually"] --> B["2 · Calibrate selectors"]
B --> C["3 · Test three conversations"]
C --> D["4 · Run the full export"]
npm run loginA dedicated headed Chromium profile opens at your INTERTEL_URL. Sign in manually in the browser -
never type the password into the terminal. A Sign in to Intertel card appears in the corner and
follows you across page reloads; when the authenticated application is visible, click Save session
and continue. The reusable profile stays in data/browser-profile/, but Chromium password and
autofill databases are removed whenever the exporter opens or closes.
SESSION_MAX_AGE_HOURS controls how long the authenticated browser session may be reused. Once that
age is reached, the dedicated profile is cleared before Chromium opens and manual login is required.
Leave the value empty to keep the session until Intertel expires it. This checkout uses six hours.
npm run calibrateCalibration happens entirely inside Chromium. Each guided step draws a small diagram of the Intertel screen with the region you need outlined in orange and marked CLICK, plus written click/avoid instructions.
| Control | What it does |
|---|---|
| Keep selection | Accepts the highlighted element and moves on |
| Discard and retry | Picks the same step again |
| ← Previous step | Goes back one step |
| Skip this optional step | Leaves an optional field uncalibrated |
| Unselect highlight | Clears the dashed outlines so you can see the interface underneath |
| Clear / unselect | Drops a saved selection entirely |
The panel never blocks your work: drag it by its header, or collapse it to a single bar with − (double-clicking the header does the same). Its position and collapsed state carry to the next step, and element picking keeps working while it is collapsed. It renders in an isolated shadow root, so Intertel's own styles cannot distort it.
One optional step asks for the Load more conversations control at the bottom of the list. Calibrating it lets discovery continue through every additional batch instead of stopping when that control appears. Older calibration files that predate this step are rejected deliberately, so an export cannot silently stop at the first batch.
Calibration saves local selectors to config/selectors.json and element screenshots to
data/screenshots/calibration/. Both are Git-ignored: they can be account-specific or contain
customer information. Stable attributes are preferred; generated classes are only a fallback.
npm run export:testRuns headed, processes at most three conversations, and generates every output format. Review message order, chat text, timestamps, sender roles, attachments and the run summary before committing to a full run.
npm run exportDiscovery and every conversation are checkpointed continuously, and chat is atomically persisted while the history is being walked, so an interrupted run resumes with the same command.
| Command | Purpose |
|---|---|
npm run login |
Open Chromium for a manual sign-in and store the local profile |
npm run calibrate |
Capture and validate the Intertel selectors interactively |
npm run export:test |
Headed run limited to three conversations |
npm run export |
Full run, resuming from the checkpoint |
npm run retry-failed |
Re-attempt only failed and partial conversations |
npm run validate |
Re-check checkpoints, records and completeness; rewrite the outputs |
| Option | Default | Notes |
|---|---|---|
--headed / --headless |
from .env |
Headed shows the live panel |
--concurrency <n> |
EXPORT_CONCURRENCY |
Conversations scraped at once, one tab each; max 6 |
--limit <n> |
all | Stop after n conversations |
--resume |
on in npm run export |
Continue from the checkpoint |
--force |
off | Rescrape conversations already completed |
--retry-failed |
off | Only previously failed or partial conversations |
--download-attachments |
off | Save files under output/attachments/{conversation_id}/ |
--max-list-scrolls <n> |
MAX_LIST_SCROLLS |
Discovery guard |
--max-message-scrolls <n> |
MAX_MESSAGE_SCROLLS |
Per-conversation history guard |
--delay-between-conversations <ms> |
.env |
Pause between conversations |
--output-directory <path> |
output/ |
Where the artefacts are written |
| Variable | Default | Meaning |
|---|---|---|
INTERTEL_URL |
https://app.intertelconversa.com/app |
Where the browser opens |
BROWSER_HEADLESS |
false |
Default mode when no flag is passed |
SESSION_MAX_AGE_HOURS |
6 |
Hours before local login expires; empty never does |
EXPORT_CONCURRENCY |
1 |
Default tabs; 3 is a sensible everyday value |
DELAY_AFTER_NAVIGATION_MS |
400 |
Ceiling for a conversation to render after opening |
DELAY_AFTER_SCROLL_MS |
120 |
Quiet window the panel must hold before it is read |
DELAY_BETWEEN_CONVERSATIONS_MS |
50 |
Pause between conversations |
MAX_LIST_SCROLLS |
10000 |
Discovery scroll guard |
MAX_MESSAGE_SCROLLS |
300 |
History scroll guard per conversation |
LOG_LEVEL |
info |
debug adds scroll-cycle detail |
TIMEZONE |
America/Mexico_City |
Used to resolve zone-less timestamps |
The DELAY_* values are settle ceilings, not fixed sleeps: the scraper waits for the page to go
quiet and only falls back to these. Raise them if Intertel renders slowly and conversations come
back partial.
Headed exports show a live panel in the Chromium window:
- a segmented meter and conversation count, readable at a glance
- completed / partial / failed / skipped counters
- Capturing - the messages being read out of the open conversation, newest last, colour-coded by speaker
- the workbook line: which file was written, how many conversations and message rows it holds, and when it was last saved
- a feed of the same redacted log lines that reach the terminal
On the page itself, the conversation being exported is outlined in the list and each message row washes blue as it is read, so you can watch the scrape move through the chat. Both marks are removed when the run finishes. With several tabs the panel narrates the tab you are looking at while the counters aggregate every worker. Headless runs skip the panel and log to the terminal only.
Nothing resolves a browser locator per field; a whole scroll cycle is one page call, the settle window starts the instant the scroll happens, and already-captured messages come back as bare ids. Opening a conversation matches its row by identity inside the page rather than reading the whole list. Panels that only append rows are walked by jumping straight to the end; panels that unmount rows as they scroll are detected on the next cycle and walked conservatively so nothing is skipped.
What remains is Intertel's own rendering time, and the only way to shorten that is to overlap it:
npm run export -- --resume --concurrency 3Each worker takes the next conversation in its own tab of the same signed-in session. Measured on a 24-conversation fixture, with every message captured in each case:
| Tabs | Per conversation | 285 conversations |
|---|---|---|
| 1 | 0.92s | ~4.4 min |
| 3 | 0.34s | ~1.6 min |
| 4 | 0.24s | ~1.1 min |
Concurrency is off by default because more tabs means more load on Intertel and a higher chance of tripping its rate limiting, which pauses the run and costs the time back. Three is a sensible ceiling for everyday use; six is the maximum accepted.
Every extra tab must prove it is on the calibrated inbox before it is used. A tab that cannot is closed and the run continues with fewer - fewer workers is merely slower, whereas a tab on the wrong section would produce wrong data. Records are written per conversation and the checkpoint serialises its writes, so a parallel run resumes exactly like a serial one.
The default destination is output/.
| File | Contents |
|---|---|
intertel-export.xlsx |
The workbook - see below |
intertel-conversations.csv |
One row per conversation |
intertel-messages.csv |
One row per message |
intertel-raw.json |
Structured chat backup |
intertel-rag.jsonl |
One retrieval-friendly record per conversation |
intertel-errors.csv |
Failed, partial and warning records |
intertel-run-summary.json |
Machine-readable counts and completeness result |
The workbook opens on a Run Summary cover sheet - the completeness verdict, the figures, warnings and a guide to the other sheets - followed by:
- Conversation View - a readable transcript where each conversation is a labelled band you can collapse, with the speaker colour-coded per message
- Conversations, Messages, Errors - grids with frozen headers, filters, real date cells and colour-coded status columns
It stays on Calibri so emoji and keycap digits render, and colour marks state only: a header, a status cell, a failed row. Nothing is tinted for decoration.
Source records, checkpoints, screenshots and structured diagnostics stay under data/. Every
customer-data directory is Git-ignored and nothing is uploaded automatically.
File generation is not proof that every conversation was exported. The run reports complete only
when all of these hold:
- the conversation list reached a stable end rather than a scroll guard
- every discovered eligible conversation was processed
- none failed or remained partial
- no message-history scroll guard was reached
Check intertel-run-summary.json, the Run Summary worksheet and
intertel-errors.csv. A status of incomplete is intentional whenever the evidence is
insufficient.
| Symptom | What to do |
|---|---|
| Expired session | Run npm run login, sign in, retry |
| Missing or stale selectors | Open a representative conversation and rerun npm run calibrate; UI changes commonly require recalibration |
| Run stops on "Open the conversation inbox" | The calibrated regions are not on screen - open the desired section and one conversation, then click Re-check and start export |
| Empty output | Confirm you started in the correct section, a conversation was open during calibration, and the list panel actually holds visible cards |
| Conversation list never finishes | Raise MAX_LIST_SCROLLS, but inspect the summary - hitting the guard is always reported as incomplete |
| Long history comes back partial | Raise MAX_MESSAGE_SCROLLS, then npm run retry-failed |
| Rate limits or an unstable UI | Drop to --concurrency 1, use headed mode, and raise the DELAY_* ceilings |
Excel shows ##### |
The column is narrower than its date - widen it; the exporter sizes columns from content |
npm run typecheck
npm run lint
npm testThe suite is deliberately small and aimed at silent data loss, the failure an exporter cannot afford. It covers the virtualized-panel guards, message-history completeness, the complete or incomplete verdict, message deduplication, every output format, and checkpoint resume under parallel writes. Some of it drives a local Playwright Chromium - no Intertel account and no customer data are involved.