Skip to content

About

Small CLI bridge for a browser-backed chatbot session.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

136 Commits

Folders and files

Repository files navigation

Chatbot CLI

Terminal bridge for an already-open target app browser tab exposed through Chromium DevTools Protocol.

CB uses Rebrowser's patched Playwright Core driver by default while still connecting to the already-open browser through CDP. Set CHATBOT_PLAYWRIGHT_PACKAGE=playwright-core to fall back to unpatched Playwright Core for compatibility checks. Rebrowser's patched Playwright packages can lag upstream Playwright releases, so driver upgrades should be validated with a live CDP status check.

Configuration

Copy .env.example into your shell or service environment and set:

  • CHATBOT_CDP_URL: Chromium DevTools endpoint. Defaults to http://127.0.0.1:9222.
  • CHATBOT_WEB_URL: target web app URL. Defaults to the built-in target app URL; set this in services to avoid hard-coded hosts.
  • CHATBOT_CDP_CONNECT_TIMEOUT_MS: Chromium DevTools attach timeout. Defaults to 60000.
  • CHATBOT_PLAYWRIGHT_PACKAGE: automation driver package. Defaults to rebrowser-playwright-core.
  • CHATBOT_TRANSCRIPT_DIR: private transcript output directory. Defaults to outputs/, which is gitignored.
  • CHATBOT_PLAYWRIGHT_CORE_PATH: optional absolute module path override; wins over CHATBOT_PLAYWRIGHT_PACKAGE.

Install dependencies:

npm install

Run:

node CB.js --message "hello"

On the RPC screen-97 test lane used during development, the browser CDP endpoint is:

node CB.js --cdp http://127.0.0.1:9241 --status

Use --new-tab when a test prompt should run in a separate target app tab without taking over an already-running conversation:

node CB.js --cdp http://127.0.0.1:9241 --new-tab --new-conversation --message "hello"

Commands

Inspect the current target app page:

node CB.js --status
node CB.js --status --state-jsonl
node CB.js --status --deep-status
node CB.js --models
node CB.js --stop
node CB.js --latest-assistant
node CB.js --sync-transcript
node CB.js --search "project name"
node CB.js --search "project name" --search-scrolls 3
node CB.js --search "project name" --search-all
node CB.js --search "project name" --search-open 1
node CB.js --dismiss-blocker
node CB.js --export-context-summary
node CB.js --retry-edit latest --recovery-incident <incident-id> --conversation <id>
node CB.js --retry-edit latest --recovery-incident <incident-id> --edit-suffix "." --conversation <id>
node CB.js --recovery-resend --recovery-incident <incident-id> --conversation <id> --message "..."
node CB.js --recover-interrupted

--status is a passive current-page DOM read for composer, generation, turn, artifact, and voice/dictation state. Use it while another target app response is in progress; it does not open model UI or run the startup settle/reconcile path. --deep-status additionally opens the model picker/configurator to inspect the live model/reasoning options; reserve that for model-picker debugging, not ordinary readiness checks.

--models opens the live model picker, then reports the current model/effort, selected composer button, available models (e.g., Latest representing GPT-6 / Astra, GPT-5.6 Sol, GPT-5.5), and reasoning effort slider levels (Instant, Medium, High, Extra High, Pro). These options are inspected dynamically from the live UI.

--search <query> opens the target app's left-navigation search UI, enters the query, prints visible results, and closes the search dialog. Add --search-open <index-or-title> to open a result from that search by 1-based index or title/session text match. Search does not dismiss unrelated blocking modals; use --dismiss-blocker for that explicit action.

Search is dynamic: CB waits while the history UI is still searching, distinguishes no-result state from results, and reports phase, resultCount, complete, hasMore, scrolls, and bottomConfirmations in JSONL output. Use --search-scrolls <n> to scroll to the bottom repeatedly and load more results, or --search-all to keep scrolling until the loaded result list stops growing or the safety cap is hit. Completion after scrolling requires repeated stable bottom checks, because the target app can load more rows after it first reaches the bottom. A bounded search should not end with both hasMore:false and truncated:true: once the UI reaches bottom, CB runs extra confirmation probes outside the load-scroll budget. If that bottom state cannot be confirmed after the safety probes, CB reports bottomConfirmationTimedOut instead of truncated. Search emits search_opening before interacting with the UI and does not pre-reconcile conversation transcripts before the search panel opens.

--dismiss-blocker attempts to dismiss one known safe blocking modal without sending a prompt or running another UI feature. It only uses safe close/Escape behavior and reports if the blocker remains or is not safe to dismiss automatically.

--recover-interrupted inspects connection and generation state fail-closed. If generation is actively in progress, it refuses destructive action and leaves the active answer stream intact. If generation has finished and the UI has settled, it hydrates the target conversation and verifies the composer is editable without performing destructive reloads or clicking Stop.

--retry-edit [latest] executes Stage 1 of the in-session recovery hierarchy. It performs a mandatory exact-thread clean reload (reloadExactConversation), locates the latest rendered user turn, verifies that appending --edit-suffix (default: .) produces a distinct turn hash (EDIT_MUTATION_NOT_DISTINCT check), captures pre-edit prompt version baseline ($K$) via the "See versions" frontend UI (button[data-testid="variants-turn-action-button"]) while safely preserving active version branch state, opens the turn-scoped inline editor, asserts initial content matches the source prompt (EDIT_EDITOR_MISMATCH), populates the editor with the edited text, registers a pre-send WAL round (operationKind: 'edit_retry') recording the version baseline, clicks the scoped Send button, attests the revised user turn, performs dual-vector version attestation verifying monotonic increment ($K \to K+1$) and rendered content hash match on Version $K+1$, and passively monitors the regenerated assistant descendant until completion. Stage 1 is a one-shot operation requiring --recovery-incident <id>.

--recovery-resend executes Stage 2 of the in-session recovery hierarchy. It performs a mandatory exact-thread clean reload (reloadExactConversation) and asserts thread identity before submitting the prompt in the same conversation thread, guaranteeing a fresh generation attempt without thread drift. Stage 2 is a one-shot existing-thread operation requiring --message and a mandatory --recovery-incident <id>.

To prevent ambiguous WAL transcript reconciliation if a recovery turn is interrupted, CB CLI automatically injects a unique recovery discriminator prefix into the user prompt: [Recovery Stage 2: <incident-id>] <original prompt>. Stage 2 is gated at the CLI entry point and strictly prohibits combination with --new-conversation, scheduling, or queue operations.

--branch-turn [latest|prior-assistant|<turnId>] executes Stage 3 of the in-session recovery hierarchy (native backend branching). It performs an exact-thread reload of the parent conversation, proves generation is idle, locates the target assistant turn (latest or prior-assistant), records a pre-branch WAL entry (lineage.jsonl), opens the turn's scoped More actions menu, navigates to Open new branch -> Branch in new Chat, executes the single commit click, identifies the destination page (navigated tab or newly opened tab), handles ephemeral /c/WEB:* routes until a stable child UUID is observed, and attests parent lineage via the live DOM divider (a[href*="/c/<parentSessionId>"] starting with "Branched from"). It briefly holds overlapping conversation leases on both parent and child during ledger persistence, establishes an empty child transcript without fabricating ancestry (leaving ancestral turns cleanly linked in backend lineage and beginning child transcript with post-branch turns), and guarantees that no user prompt is automatically sent as part of the branch transaction. Stage 3 is a one-shot operation requiring --conversation <parent-uuid> and --recovery-incident <id>.

--auto-recover: Unified 3-stage automated recovery coordinator. Orchestrates the full in-session recovery escalation hierarchy across Stages 1, 2, and 3 under proof-driven escalation:

  1. Incident Initialization: Acquires incident coordinator lease, freezes exact raw source prompt text into outputs/recovery/<id>/source-prompt.txt (persisting SHA-256), freezes branchAnchorTurnRef (nearest clean assistant turn preceding the failed prompt), and writes entry to recovery-incidents.json / jsonl.
  2. Stage 1 (In-Place Edit): Executes --retry-edit with visible discriminator (.). If model succeeds, marks incident completed_stage1. If proven terminal model failure occurs, escalates to Stage 2. If automation uncertainty occurs (stage1_needs_reconciliation), stops safely without escalating.
  3. Stage 2 (Same-Session Resend): Executes --recovery-resend using the frozen raw prompt text and automatic [Recovery Stage 2: <id>] discriminator. If model succeeds, marks incident completed_stage2. If proven terminal model failure occurs, escalates to Stage 3.
  4. Stage 3 (Native Backend Branching): Executes --branch-turn targeting the frozen branchAnchorTurnRef. Attests live DOM divider and marks incident completed_stage3_bound. Requires --conversation <parent-uuid> and --recovery-incident <id>. Mutually exclusive with other primary commands.

--recover-branch <branch-id>: Inspects or safely completes a stranded Stage 3 branch transaction from the lineage ledger. If the record is in stable_candidate or destination_unverified with an identified candidate child UUID, and --cdp is provided, performs read-only browser-backed attestation verifying the child tab's live DOM parent divider against the parent session before promoting the record to lineage_attested and bound, strictly without ever clicking branch again.

--export-context-summary (alias: --compact-conversation) parses the active or specified conversation transcript, extracts high-signal architecture details (overarching mission, verified code references, commit SHAs, empirical findings, standing recommendations, and external GitHub/web URLs), and writes diagnostic JSON and Markdown artifacts under outputs/compactions/. Note: this is an offline diagnostic export utility only, completely excluded from the operational recovery path.

Legacy --compact-handoff is quarantined and disabled by default (returning LEGACY_COMPACTION_HANDOFF_DISABLED), as fresh continuation chats are excluded from the project's 3-stage recovery hierarchy (Stage 1 in-place edit/regenerate -> Stage 2 same-thread resend -> Stage 3 native branch). Set CB_ENABLE_LEGACY_COMPACTION=1 only for controlled offline testing.

Settings and personalization dialogs are treated as blocking UI, not as safe auto-dismiss targets. Model-picker commands must not use account/profile or settings controls as model switchers; if a settings dialog is open, close it explicitly before inspecting or selecting models.

The current Intelligence picker combines a 5-point reasoning effort slider (Instant, Medium, High, Extra High, Pro) in simple view with a model selection list (Latest representing GPT-6 / Astra, GPT-5.6 Sol, GPT-5.5) in advanced view. In the composer, the pill button displays the selected effort or model prefix when closed and a placeholder while open.

Model and reasoning availability is account/subscription-dependent. CB reads the live picker before selecting. If a requested tier or model is targeted, CB toggles between the slider view and the model list view, updates the selection, and returns to the composer.

Watch target app state for orchestration:

node CB.js --watch-state --state-jsonl
node CB.js --watch-state --wait-ready --state-jsonl --timeout 120000

--watch-state polls the live page and emits state changes. --wait-ready exits when a new assistant turn has completed after the watcher baseline; the final JSONL event has "phase":"ready" and "ready":true, which can be used as a trigger for another agent to consume the transcript or latest assistant output. Progress-only assistant text such as Finalizing answer, Thinking, or Thought for ... is treated as in-flight state, not as a completed answer or transcript export. When target app renders multiple assistant message nodes inside one turn, CB stitches the substantive nodes and ignores progress-only placeholders, so short prefaces do not hide the final answer. Every CB invocation reconciles the active target app DOM into the session transcript and updates the conversation index before doing new work. If a prior --message process timed out or was killed after the user prompt was accepted, run --sync-transcript once the browser shows the completed answer. This appends any missing completed DOM turns to the session transcript, marks matching pending rounds recovered, and updates the index. Use --latest-assistant when another agent needs the full latest response text instead of the short /status preview.

Schedule sequential work:

node CB.js --schedule --new-conversation --alias research-a --message "Start the analysis"
node CB.js --schedule --conversation research-a --message "Follow up after the first answer"
node CB.js --run-queue --cdp http://127.0.0.1:9241
node CB.js --recover-queue --cdp http://127.0.0.1:9241
node CB.js --queue-status

--run-queue processes scheduled prompts in strict queue order. It only considers the first non-done job: running, waiting, needs_recovery, or failed jobs block later pending work until they are recovered, reset, or intentionally skipped. It sends one prompt, waits for target app to finish, records the transcript/session id, then starts the next queued prompt. If target app is still generating when a timeout fires, the job is held as waiting and the runner stops instead of marking it failed and continuing. Pre-send UI blockers such as subscription modals or modal-conversation-history-rate-limit are held as needs_recovery; no prompt is considered submitted and the next queued job must not start. Known safe blockers such as #modal-subscription-failure and identified artifact/lightbox close overlays are auto-dismissed by clicking only visible Close controls and then rechecking the blocker state. Payment, upgrade, login, captcha, destructive, or unknown dialogs are not clicked. A follow-up can target a future conversation alias before target app has assigned the real /c/<session-id>; the first --new-conversation --alias ... job resolves that alias after the answer lands, and later jobs open the resolved session id.

If a new-conversation job displays the submitted user turn but the backend never assigns a stable /c/<session-id> UUID, CB deliberately does not reload (which would discard the in-flight stream). Instead, it throws NEW_SESSION_ID_UNCERTAIN fail-closed, marks the job as needs_recovery, and halts the queue.

--recover-queue never sends a new prompt. It syncs the active target app conversation, skips any in-flight assistant preface while the stop control is visible, updates matching rounds/jobs after the final answer is complete, audits already-done jobs for stale response counts, and reports the first queue blocker or next pending job. Use --conversation <session-id-or-alias> with --recover-queue, --sync-transcript, --latest-assistant, or --status to inspect or recover a specific existing conversation without sending a message. Scheduler completion is driven by target app state, not by a timer: queued jobs have no response timeout by default, and older jobs that only stored the default 180000 timeout do not inherit it. Use --timeout <ms> only as an explicit watchdog, or --timeout 0 to force no timeout.

Scheduler and conversation state is private output data under outputs/scheduler/: queue.json, conversation-index.json, and rounds.json are the readable current state, while their .jsonl companions are append-only event journals.

Send a prompt and stream the answer as target app produces it:

node CB.js --message "Summarize this in three bullets"

Before submitting, CB verifies that the composer draft contains the intended prompt text or a recognized long-form pasted-text attachment. After submitting, it waits for a matching user turn to appear after the baseline turn before it records the prompt as accepted or appends it to the transcript. For brand-new conversations, the matching user turn is not enough: CB also requires the URL to move to a stable /c/<session-id> UUID. If no stable session id appears within the acceptance timeout, CB fails closed with NEW_SESSION_ID_UNCERTAIN without reloading. The transaction is marked uncertain and preserved for manual inspection. Before typing or clicking send, CB also checks the center point of the composer and send button with document.elementFromPoint(). If a visible modal, dialog, or open overlay covers that point and cannot be safely dismissed, CB reports a UI blocker and does not insert or submit the prompt.

Use --no-stream to wait silently and print the final answer only. Use --state-jsonl with a prompt to emit structured state events while still returning the answer.

Select visible UI options before sending:

node CB.js --model "5.5 Pro Extended" --message "hello"
node CB.js --model "Thinking Heavy" --message "hello"
node CB.js --conversation 6a3338bc-81a0-83ec-a28f-28a763a2bc1b --message "Follow up in this session"
node CB.js --new-conversation --alias scratch-a --message "Start a fresh session and index it"
printf "/model 5.5 Pro Extended\n/status\n/exit\n" | node CB.js

For current target app picker shapes, model labels can include a model version, mode, and effort, for example 5.5 Pro Extended or 5.5 Thinking Heavy. --status --state-jsonl includes a lightweight modelSelection object from the visible composer button. Add --deep-status only when you need a compact modelConfig object with the live model list and effort options. If a requested effort is unavailable, selection reports the fallback, for example 5.5 Thinking Heavy can select Thinking Extra High when Heavy is not present. Compact Pro rows can display only Pro in the composer while the checked flyout effort is Standard; /models and deep status open the picker to read that hidden checked effort.

Attach files before sending:

node CB.js --attach ./notes.txt --message "Use the attached notes"
node CB.js --attach ./clip.mp3 --message "Describe the audio"
node CB.js --attach ./clip.mp4 --message "Describe the video"

If the target app page accepts the file chip but keeps the send button disabled, the CLI exits with a clear error instead of waiting for the full response timeout.

Save artifacts from the latest assistant turn:

node CB.js --download-artifacts
node CB.js --message "Give me a small JS example" --download-artifacts --show-artifacts
node CB.js --message "Generate a square image with the text CB IMAGE TEST" --download-artifacts

Artifacts are written under outputs/artifacts/<session>/<timestamp>/ and can include metadata, links, visible images, code blocks, and files exposed through download controls. Generated target app images are saved from the latest assistant turn as local image files. Use --show-artifacts to print saved text/code artifacts in the terminal. If an assistant labels a code block with a preceding FILE: name.ext line, the CLI uses that filename instead of code-block-N.txt.

Interactive Mode

Run node CB.js and use:

  • /status: print model, composer, generation, and artifact state.
  • /status deep: inspect model picker/configurator details too.
  • /models: list visible model picker options.
  • /reasoning: list visible reasoning controls.
  • /model <text> and /reasoning <text>: click matching UI options.
  • /search <text>: search target app conversations/history and print results.
  • /search-all <text>: search and scroll/load until the result list stops growing.
  • /search-open <text>[ | index-or-title]: search and open the first or matching result.
  • /dismiss-blocker: dismiss one known safe blocker without sending a prompt.
  • /attach <path>: queue a file for the next prompt.
  • /artifacts: save artifact metadata from the latest assistant turn.
  • /download: save latest-turn artifacts to disk.
  • /stop: click a visible stop/interrupt generation control.
  • /stream on|off: toggle live answer streaming.

CB is text/file-only. It recognizes target app voice/dictation controls in /status, but deliberately does not click Start dictation, Start Voice, or related microphone controls because the CLI cannot provide voice input.

Multiline paste works directly at CB>. Long pasted text that target app converts into a composer attachment is surfaced through /status.

Long Markdown prompts are matched back to target app turns with normalized text anchors, so code fences, inline backticks, ProseMirror spacing, and collapsed "show more" rendering do not block response capture.

Continuity, Leases & Concurrency Architecture

CB implements a fail-closed continuity architecture designed to prevent cross-thread prompt bleed, off-thread root dispatches, and false-success classifications:

Operational Concurrency Invariant

  • Single-Writer-Per-CDP-Endpoint: Exactly one mutating CB process may control a given physical Chromium browser instance / CDP endpoint at a time.
  • All mutating commands (sending prompts, navigating conversations, selecting models/reasoning, uploading attachments, and dismissing blockers) acquire a browser-lane lease keyed by the normalized CDP endpoint URL (e.g. http://127.0.0.1:9241).
  • Concurrent writers must target distinct CDP ports/browser instances or serialize sequentially through the queue runner.

Pre-Send Write-Ahead Log (WAL)

  • Every prompt dispatch journals through outputs/scheduler/rounds.jsonl.
  • State transitions are monotonic: prepared -> dispatching -> accepted (or uncertain / aborted_precommit).
  • Dead prepared processes cleanly abort without contaminating queues; dead dispatching processes isolate as uncertain.

Three-Layer Writer Leases

  • Browser-Lane Lease: Scoped per CDP endpoint (outputs/scheduler/leases/lane-<key>.lock), acquired at the start of any mutating operation (including navigation, model selection, attachments, and prompt dispatch) and held until completion, ensuring no second process can clobber the physical tab.
  • Bootstrap Writer Lease: Scoped per CDP endpoint (bootstrap-<key>.lock), held from root composer setup until post-send stable-ID attestation, preventing concurrent processes from colliding on /.
  • Conversation Lease: File-backed lock (<sessionId>.lock) with atomic token validation, preventing concurrent processes from appending into the same conversation.

3-Tier Stable-ID Turn Attestation

  • Candidate conversation IDs discovered after a new-chat dispatch must attest the accepted user prompt inside the target DOM before binding:
    1. message_id: Exact match on data-message-id.
    2. testid_hash: Exact match on turn testid and raw prompt content hash.
    3. unique_text_hash: Unique prompt text match across mounted turns.
  • Mismatches or timeouts throw NEW_SESSION_ATTRIBUTION_MISMATCH / NEW_SESSION_ATTRIBUTION_UNVERIFIED and fail closed without reloading or blind resending.

Immutable Session Identity & Authoritative Recovery

  • Once a round is bound, round.sessionId and expectedSessionId are strictly immutable.
  • Response-error paths record observed page drift purely as diagnostic metadata (observedSessionId, observedUrl) without rewriting the authoritative round identity.
  • Completion persistence re-asserts thread identity before writing transcripts.
  • roundAllowsTranscriptRecovery() strictly requires authoritative binding (not_applicable or attested) and a durable stable UUID.

Known recovery procedures

Two queue-runner failure modes were observed and hardened in CB.js. The behaviors below are what the runner now does automatically; the manual steps are documented in case you ever need to inspect or undo state by hand.

1. Stale transcript tail crashes the queue runner

--run-queue reconciles the on-disk transcript against the live target app DOM before running jobs. If a transcript file (typically a leftover outputs/new-chat.txt from a prior clean test) has a tail that no longer matches the live page, sync used to throw and abort the whole runner before any job could run.

Now: on a transcript-tail mismatch the runner renames the stale file aside (outputs/new-chat.txt.stale-<ISO timestamp>) and retries the sync against a fresh transcript, instead of throwing. The quarantine is logged.

Manual recovery (if ever needed):

mv outputs/new-chat.txt outputs/new-chat.txt.stale-backup
# the runner recreates outputs/new-chat.txt on next use

2. A blocking failed job pins the queue

By design a job in failed status blocks later pending jobs until it is recovered, reset, or skipped (so genuine failures are not silently dropped). There was no CLI path to skip a blocking failed job, leaving manual edits to outputs/scheduler/queue.json as the only way out.

Now: --skip-failed makes --run-queue auto-skip a blocking failed job, journal it as job_skipped_auto, and continue to the next pending job.

CB --run-queue --cdp http://127.0.0.1:9241 --skip-failed

The default remains that a failed job blocks until manually reset. To skip a failed job without running the queue, edit outputs/scheduler/queue.json directly:

python3 -c "
import json, datetime
p='outputs/scheduler/queue.json'
d=json.load(open(p))
for j in d['jobs']:
    if j.get('status')=='failed':
        j['status']='skipped'
        j['updatedAt']=datetime.datetime.utcnow().isoformat(timespec='milliseconds')+'Z'
json.dump(d, open(p,'w'), indent=2)
print('skipped failed jobs')
"

About

Small CLI bridge for a browser-backed chatbot session.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages