Skip to content

[Claimed #1830] feat(cli): add --connect flag for daemon mode with existing Chrome#1831

Closed
github-actions[bot] wants to merge 1 commit intomainfrom
external-contributor-pr-1830
Closed

[Claimed #1830] feat(cli): add --connect flag for daemon mode with existing Chrome#1831
github-actions[bot] wants to merge 1 commit intomainfrom
external-contributor-pr-1830

Conversation

@github-actions
Copy link
Contributor

@github-actions github-actions bot commented Mar 15, 2026

Mirrored from external contributor PR #1830 after approval by @miguelg719.

Original author: @peytoncasper
Original PR: #1830
Approved source head SHA: 09f21611c547a9da5cf1efe6b7f41e0170079587

@peytoncasper, please continue any follow-up discussion on this mirrored PR. When the external PR gets new commits, this same internal PR will be marked stale until the latest external commit is approved and refreshed here.

Original description

Summary

  • Adds --connect <url> CLI option that starts the daemon attached to an existing Chrome instance via CDP WebSocket URL
  • The daemon persists between commands, caching accessibility tree refs from snapshots
  • Unlike --ws (stateless, no ref caching), --connect gives you persistent daemon mode without launching Chrome

Use case

Remote node management where Chrome is launched externally with custom flags (anti-bot detection, persistent profiles, specific ports) and the browse CLI needs to interact with it while preserving refs between commands.

Example:

# External Chrome is running on port 9300 with custom flags
browse --connect ws://127.0.0.1:9300/devtools/browser/abc123 open https://example.com
browse --connect ws://127.0.0.1:9300/devtools/browser/abc123 snapshot  # refs cached
browse --connect ws://127.0.0.1:9300/devtools/browser/abc123 click @0-5  # refs work!

Changes

  • packages/cli/src/index.ts: Added --connect option to GlobalOpts, program options, runDaemon, ensureDaemon, sendCommand, and runCommand
  • When --connect is set, ensureBrowserInitialized uses localBrowserLaunchOptions: { cdpUrl: connectUrl } instead of launching Chrome
  • Retry logic in sendCommand skips killChromeProcesses when using --connect (Chrome is externally managed)

Test plan

  • browse --connect <ws_url> open <url> navigates successfully
  • browse --connect <ws_url> snapshot -c returns accessibility tree with refs
  • browse --connect <ws_url> click @0-1 uses cached refs from previous snapshot
  • Daemon persists between invocations (refs survive across CLI calls)
  • Existing --ws behavior unchanged
  • Local daemon mode (no flags) unchanged

Made with Cursor


Summary by cubic

Add a --connect flag to run the CLI daemon against an existing Chrome via CDP, enabling persistent ref caching without launching Chrome. Existing --ws behavior remains stateless and unchanged.

  • New Features
    • --connect <url> attaches the daemon to external Chrome; refs persist across commands.
    • Uses localBrowserLaunchOptions.cdpUrl when provided, passes the URL through daemon startup/retries, and skips killing Chrome during retry logic.

Written for commit 09f2161. Summary will update on new commits. Review in cubic

Adds a --connect <url> option that tells the browse daemon to attach to
an existing Chrome instance via CDP WebSocket URL instead of launching
its own Chrome. The daemon persists between commands (refs from snapshot
are cached), but Chrome lifecycle is managed externally.

Use case: remote node management where Chrome is launched with custom
flags (anti-detection, profiles, specific ports) and the browse CLI
needs to interact with it while preserving accessibility tree refs.

--ws (stateless, no ref cache) remains unchanged.
--connect (daemon mode, persistent refs, external Chrome) is new.

Made-with: Cursor
@changeset-bot
Copy link

changeset-bot bot commented Mar 15, 2026

⚠️ No Changeset found

Latest commit: 09f2161

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

Adds a --connect <url> CLI flag that starts the daemon attached to an existing Chrome instance via CDP WebSocket URL, enabling persistent ref caching (unlike the stateless --ws flag). The implementation threads connectUrl through runDaemon, ensureDaemon, and sendCommand, and correctly skips killChromeProcesses during retry logic when Chrome is externally managed.

  • The start command doesn't forward opts.connect to ensureDaemon, so browse --connect <url> start will ignore the connect URL
  • stop --force calls killChromeProcesses unconditionally, which could kill externally managed Chrome — the same guard from sendCommand should apply here
  • ensureDaemon doesn't track the connect URL, so switching between different --connect URLs on the same session silently reuses the first daemon

Confidence Score: 2/5

  • The core concept is sound but there are logic gaps that could lead to surprising behavior, including silently connecting to the wrong Chrome instance
  • Score of 2 reflects three logic issues: the start command ignoring --connect, stop --force potentially killing externally managed Chrome processes, and daemon reuse not differentiating between different connect URLs. The happy path works, but edge cases around daemon lifecycle management need attention before merging.
  • packages/cli/src/index.ts — the start command, stop --force handler, and ensureDaemon mode-check logic all need updates to fully support the --connect flag

Important Files Changed

Filename Overview
packages/cli/src/index.ts Adds --connect flag threading through daemon lifecycle functions. Core path works but start command, stop --force, and daemon reuse logic don't fully account for the new flag.

Sequence Diagram

sequenceDiagram
    participant User as CLI User
    participant CLI as browse CLI
    participant Daemon as Daemon Process
    participant Chrome as External Chrome

    Note over User, Chrome: --connect mode (persistent daemon, external Chrome)
    User->>CLI: browse --connect [cdp-url] open [url]
    CLI->>CLI: runCommand() checks opts.ws (not set)
    CLI->>Daemon: ensureDaemon(session, headless, connectUrl)
    Daemon->>Chrome: Connect via CDP WebSocket
    Chrome-->>Daemon: CDP connection established
    Daemon->>Daemon: Cache refs from accessibility tree
    Daemon-->>CLI: Command result
    CLI-->>User: Output

    Note over User, Chrome: Subsequent command reuses daemon + cached refs
    User->>CLI: browse --connect [cdp-url] click @0-5
    CLI->>Daemon: sendCommand() (daemon already running)
    Daemon->>Chrome: Execute click using cached ref
    Chrome-->>Daemon: Result
    Daemon-->>CLI: Result
    CLI-->>User: Output

    Note over User, Chrome: --ws mode (stateless, no daemon)
    User->>CLI: browse --ws [cdp-url] open [url]
    CLI->>Chrome: Direct Stagehand connection (no daemon)
    Chrome-->>CLI: Result (no ref caching)
    CLI-->>User: Output
Loading

Comments Outside Diff (3)

  1. packages/cli/src/index.ts, line 1528 (link)

    start command ignores --connect flag

    ensureDaemon is called here without opts.connect, so browse --connect <url> start will launch a new local Chrome instead of attaching to the provided CDP URL. This should be consistent with runCommand and daemon which both pass opts.connect through.

  2. packages/cli/src/index.ts, line 1547-1549 (link)

    stop --force may kill externally managed Chrome

    When the daemon was started with --connect (externally managed Chrome), stop --force still calls killChromeProcesses, which could kill the user's external Chrome process. This contradicts the design of --connect where Chrome lifecycle is externally managed. The same guard used in sendCommand (if (!connectUrl) await killChromeProcesses(session)) should be applied here — or persist whether the daemon is in "connect" mode so the stop command can check it.

  3. packages/cli/src/index.ts, line 1358-1363 (link)

    Daemon reuse doesn't account for different --connect URLs

    ensureDaemon only checks whether the running daemon's mode matches (e.g., both "local"), but it doesn't track the connectUrl. If a user runs:

    browse --connect ws://host1:9222 open https://a.com
    browse --connect ws://host2:9333 open https://b.com

    The second call will reuse the daemon connected to host1 since the mode ("local") matches. Consider persisting the connect URL alongside the mode file so the daemon can be restarted when the URL changes.

Last reviewed commit: 09f2161

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 1 file

Confidence score: 2/5

  • There is a high-confidence, user-facing regression in packages/cli/src/index.ts: ensureDaemon can return early based only on mode and ignore a mismatched connectUrl, which can silently attach to the wrong daemon target.
  • The start path in packages/cli/src/index.ts does not pass opts.connect into ensureDaemon, so browse --connect <url> start may launch a new Chrome instance instead of attaching as requested.
  • Given two concrete issues with severity 7-8/10 and clear behavioral impact in CLI attach/start flows, merge risk is elevated until these are fixed.
  • Pay close attention to packages/cli/src/index.ts - daemon reuse and --connect propagation can misroute connections and break expected startup behavior.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/index.ts">

<violation number="1" location="packages/cli/src/index.ts:1354">
P1: `ensureDaemon` returns early when the mode matches, but doesn't check whether the running daemon's `connectUrl` matches the requested one. A daemon started without `--connect` (or with a different URL) will be silently reused, sending commands to the wrong Chrome instance. The connect URL should be persisted and compared alongside mode.</violation>

<violation number="2" location="packages/cli/src/index.ts:1444">
P1: The `start` command doesn't forward `opts.connect` to `ensureDaemon`, so `browse --connect <url> start` will launch a new Chrome instead of attaching to the external one. Pass `opts.connect` like `runCommand` does.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant User as CLI User
    participant CLI as CLI Client
    participant Daemon as CLI Daemon Process
    participant Core as Stagehand Core
    participant Chrome as External Chrome (CDP)

    User->>CLI: browse --connect <ws_url> <command>
    
    CLI->>CLI: NEW: Parse --connect URL
    
    alt If --ws provided (Legacy/Stateless)
        CLI->>Core: Initialize Stagehand (Direct)
        Core->>Chrome: Connect via CDP
        Core-->>CLI: Return Result
    else If --connect or default (Persistent)
        CLI->>CLI: ensureDaemon(session, connectUrl)
        
        opt Daemon Not Running
            CLI->>Daemon: spawn(node index.js daemon --connect <url>)
            Daemon->>Core: NEW: init with { cdpUrl: connectUrl }
            Core->>Chrome: Attach to existing instance
            Note over Daemon,Chrome: No browser launch performed
        end

        CLI->>Daemon: sendCommand(command, args)
        Daemon->>Core: executeCommand()
        Core->>Chrome: Interaction (e.g., snapshot)
        Chrome-->>Core: Accessibility Tree / Refs
        Core->>Core: Cache Refs in Daemon Memory
        Core-->>Daemon: Command Result
        Daemon-->>CLI: Response
    end

    CLI-->>User: Output Result

    Note over CLI,Daemon: Error Handling / Retry Flow
    
    alt Command Fails (Retry Logic)
        CLI->>CLI: sendCommand (Attempt 2)
        CLI->>Daemon: ensureDaemon()
        
        opt If Max Retries Reached
            alt NEW: Using --connect
                CLI->>CLI: Skip killChromeProcesses()
            else Local Mode
                CLI->>CLI: killChromeProcesses()
            end
            CLI->>Daemon: Full Restart
        end
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -288,7 +288,7 @@ interface DaemonResponse {
// Default viewport matching Stagehand core
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 15, 2026

Choose a reason for hiding this comment

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

P1: The start command doesn't forward opts.connect to ensureDaemon, so browse --connect <url> start will launch a new Chrome instead of attaching to the external one. Pass opts.connect like runCommand does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/index.ts, line 1444:

<comment>The `start` command doesn't forward `opts.connect` to `ensureDaemon`, so `browse --connect <url> start` will launch a new Chrome instead of attaching to the external one. Pass `opts.connect` like `runCommand` does.</comment>

<file context>
@@ -1432,6 +1441,7 @@ async function ensureDaemon(session: string, headless: boolean): Promise<void> {
 
 interface GlobalOpts {
   ws?: string;
+  connect?: string;
   headless?: boolean;
   headed?: boolean;
</file context>
Fix with Cubic

}

async function ensureDaemon(session: string, headless: boolean): Promise<void> {
async function ensureDaemon(session: string, headless: boolean, connectUrl?: string): Promise<void> {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 15, 2026

Choose a reason for hiding this comment

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

P1: ensureDaemon returns early when the mode matches, but doesn't check whether the running daemon's connectUrl matches the requested one. A daemon started without --connect (or with a different URL) will be silently reused, sending commands to the wrong Chrome instance. The connect URL should be persisted and compared alongside mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/index.ts, line 1354:

<comment>`ensureDaemon` returns early when the mode matches, but doesn't check whether the running daemon's `connectUrl` matches the requested one. A daemon started without `--connect` (or with a different URL) will be silently reused, sending commands to the wrong Chrome instance. The connect URL should be persisted and compared alongside mode.</comment>

<file context>
@@ -1344,7 +1351,7 @@ async function stopDaemonAndCleanup(session: string): Promise<void> {
 }
 
-async function ensureDaemon(session: string, headless: boolean): Promise<void> {
+async function ensureDaemon(session: string, headless: boolean, connectUrl?: string): Promise<void> {
   const wantMode = await getDesiredMode(session);
   assertModeSupported(wantMode);
</file context>
Fix with Cubic

@github-actions github-actions bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:stale The mirrored PR is stale and waiting for a fresh approval to refresh. labels Mar 15, 2026
@github-actions
Copy link
Contributor Author

github-actions bot commented Mar 15, 2026

This mirrored PR could not be refreshed automatically after approval by @pirate.

Refresh reason: missing-previous-source
Resolve the branch manually, then keep using this same mirrored PR.

@miguelg719 miguelg719 closed this Mar 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:stale The mirrored PR is stale and waiting for a fresh approval to refresh. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants