Skip to content

Commit 9b425c7

Browse files
authored
feat: Electron auto-launcher — zero-config CDP connection (#653)
* docs: add dingtalk and wecom CLI to external CLI hub Add dingtalk-workspace-cli and wecom-cli as external CLI integrations alongside lark-cli, gh, docker, etc. * feat: add confirmPrompt() to TUI module * feat: add Electron app registry with builtin + user-defined apps * feat: add Electron app launcher with auto-detect and restart * fix: launcher uses processName for path discovery, platform-guard tests * feat: integrate Electron auto-launcher into execution pipeline - CDPBridge.connect() accepts cdpEndpoint parameter instead of requiring env var - getBrowserFactory() selects CDPBridge for registered Electron apps by site name - executeCommand() calls resolveElectronEndpoint() for Electron apps, skips daemon check - Remove requiredEnv/OPENCLI_CDP_ENDPOINT from all chatwise commands - Remove chatwise-opencli.ps1 wrapper script and chatwise/shared.ts - Update antigravity/serve.ts to use launcher instead of manual env var - Replace hardcoded app names in scoreCDPTarget with registry lookup - Fix Discord bundleId typo (com.iscord.app → com.discord.app) * fix: resolve review issues — port collision and registry completeness - Change ChatGPT CDP port from 9224 to 9236 (was colliding with Antigravity) - scoreCDPTarget now uses full registry (builtin + user-defined) via getAllElectronApps() - Use displayName (falling back to processName) for target score boosting * fix: assign unique CDP ports — antigravity 9234, chatgpt 9236 Both were sharing port 9224, which could cause silent mis-connection.
1 parent 12443f0 commit 9b425c7

23 files changed

Lines changed: 551 additions & 180 deletions

chatwise-opencli.ps1

Lines changed: 0 additions & 82 deletions
This file was deleted.

src/browser/cdp.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
waitForSelectorJs,
3030
} from './dom-helpers.js';
3131
import { isRecord, saveBase64ToFile } from '../utils.js';
32+
import { getAllElectronApps } from '../electron-apps.js';
3233

3334
export interface CDPTarget {
3435
type?: string;
@@ -56,11 +57,11 @@ export class CDPBridge implements IBrowserFactory {
5657
private _pending = new Map<number, { resolve: (val: unknown) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>();
5758
private _eventListeners = new Map<string, Set<(params: unknown) => void>>();
5859

59-
async connect(opts?: { timeout?: number; workspace?: string }): Promise<IPage> {
60+
async connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise<IPage> {
6061
if (this._ws) throw new Error('CDPBridge is already connected. Call close() before reconnecting.');
6162

62-
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
63-
if (!endpoint) throw new Error('OPENCLI_CDP_ENDPOINT is not set');
63+
const endpoint = opts?.cdpEndpoint ?? process.env.OPENCLI_CDP_ENDPOINT;
64+
if (!endpoint) throw new Error('CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)');
6465

6566
let wsUrl = endpoint;
6667
if (endpoint.startsWith('http')) {
@@ -414,19 +415,15 @@ function scoreCDPTarget(target: CDPTarget, preferredPattern?: RegExp): number {
414415
if (url === '' || url === 'about:blank') score -= 40;
415416

416417
if (title && title !== 'devtools') score += 25;
417-
if (title.includes('antigravity')) score += 120;
418-
if (title.includes('codex')) score += 120;
419-
if (title.includes('cursor')) score += 120;
420-
if (title.includes('chatwise')) score += 120;
421-
if (title.includes('notion')) score += 120;
422-
if (title.includes('discord')) score += 120;
423-
424-
if (url.includes('antigravity')) score += 100;
425-
if (url.includes('codex')) score += 100;
426-
if (url.includes('cursor')) score += 100;
427-
if (url.includes('chatwise')) score += 100;
428-
if (url.includes('notion')) score += 100;
429-
if (url.includes('discord')) score += 100;
418+
419+
// Boost score for known Electron app names from the registry (builtin + user-defined)
420+
const appNames = Object.values(getAllElectronApps()).map(a => (a.displayName ?? a.processName).toLowerCase());
421+
for (const name of appNames) {
422+
if (title.includes(name)) { score += 120; break; }
423+
}
424+
for (const name of appNames) {
425+
if (url.includes(name)) { score += 100; break; }
426+
}
430427

431428
return score;
432429
}

src/clis/antigravity/SKILL.md

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,10 @@ description: How to automate Antigravity using OpenCLI
77
This skill allows AI agents to control the [Antigravity](https://github.com/chengazhen/Antigravity) desktop app (and any Electron app with CDP enabled) programmatically via OpenCLI.
88

99
## Requirements
10-
The target Electron application MUST be launched with the remote-debugging-port flag:
11-
\`\`\`bash
12-
/Applications/Antigravity.app/Contents/MacOS/Electron --remote-debugging-port=9224
13-
\`\`\`
14-
15-
The agent must configure the endpoint environment variable locally before invoking standard commands:
16-
\`\`\`bash
17-
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
18-
\`\`\`
10+
opencli automatically detects, launches (with `--remote-debugging-port=9234`), and connects to Antigravity.
11+
If Antigravity is already running without CDP, opencli will prompt to restart it.
1912

20-
If the endpoint exposes multiple inspectable targets, also set:
13+
If the endpoint exposes multiple inspectable targets, set:
2114
\`\`\`bash
2215
export OPENCLI_CDP_TARGET="antigravity"
2316
\`\`\`
@@ -33,7 +26,6 @@ export OPENCLI_CDP_TARGET="antigravity"
3326

3427
### Generating and Saving Code
3528
\`\`\`bash
36-
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
3729
opencli antigravity send "Write a python script to fetch HN top stories"
3830
# wait ~10-15 seconds for output to render
3931
opencli antigravity extract-code > hn_fetcher.py
@@ -42,6 +34,5 @@ opencli antigravity extract-code > hn_fetcher.py
4234
### Reading Real-time Logs
4335
Agents can run long-running streaming watch instances:
4436
\`\`\`bash
45-
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9224"
4637
opencli antigravity watch
4738
\`\`\`

src/clis/antigravity/serve.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
* and returns it in Anthropic format.
77
*
88
* Usage:
9-
* OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve --port 8082
9+
* opencli antigravity serve --port 8082
1010
* ANTHROPIC_BASE_URL=http://localhost:8082 claude
1111
*/
1212

1313
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
1414
import { CDPBridge } from '../../browser/cdp.js';
1515
import type { IPage } from '../../types.js';
16+
import { resolveElectronEndpoint } from '../../launcher.js';
1617
import { EXIT_CODES, getErrorMessage } from '../../errors.js';
1718

1819
// ─── Types ───────────────────────────────────────────────────────────
@@ -436,13 +437,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
436437
}
437438
}
438439

439-
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
440-
if (!endpoint) {
441-
throw new Error(
442-
'OPENCLI_CDP_ENDPOINT is not set.\n' +
443-
'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve'
444-
);
445-
}
440+
const endpoint = await resolveElectronEndpoint('antigravity');
446441

447442
// Note: Antigravity chat panel lives inside editor windows, not in Launchpad.
448443
// If multiple editor windows are open, set OPENCLI_CDP_TARGET to the window title.
@@ -461,7 +456,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
461456
console.error(`[serve] Connecting via CDP (target pattern: "${process.env.OPENCLI_CDP_TARGET}")...`);
462457
cdp = new CDPBridge();
463458
try {
464-
page = await cdp.connect({ timeout: 15_000 });
459+
page = await cdp.connect({ timeout: 15_000, cdpEndpoint: endpoint });
465460
} catch (err: unknown) {
466461
cdp = null;
467462
const errMsg = getErrorMessage(err);
@@ -471,7 +466,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
471466
isRefused
472467
? `Cannot connect to Antigravity at ${endpoint}.\n` +
473468
' 1. Make sure Antigravity is running\n' +
474-
' 2. Launch with: --remote-debugging-port=9224'
469+
' 2. Launch with: --remote-debugging-port=9234'
475470
: `CDP connection failed: ${errMsg}`
476471
);
477472
}

src/clis/chatwise/ask.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { cli, Strategy } from '../../registry.js';
22
import { SelectorError } from '../../errors.js';
33
import type { IPage } from '../../types.js';
4-
import { chatwiseRequiredEnv } from './shared.js';
54

65
export const askCommand = cli({
76
site: 'chatwise',
@@ -10,7 +9,6 @@ export const askCommand = cli({
109
domain: 'localhost',
1110
strategy: Strategy.UI,
1211
browser: true,
13-
requiredEnv: chatwiseRequiredEnv,
1412
args: [
1513
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
1614
{ name: 'timeout', required: false, help: 'Max seconds to wait (default: 30)', default: '30' },

src/clis/chatwise/export.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as fs from 'node:fs';
22
import { cli, Strategy } from '../../registry.js';
33
import type { IPage } from '../../types.js';
4-
import { chatwiseRequiredEnv } from './shared.js';
54

65
export const exportCommand = cli({
76
site: 'chatwise',
@@ -10,7 +9,6 @@ export const exportCommand = cli({
109
domain: 'localhost',
1110
strategy: Strategy.UI,
1211
browser: true,
13-
requiredEnv: chatwiseRequiredEnv,
1412
args: [
1513
{ name: 'output', required: false, help: 'Output file (default: /tmp/chatwise-export.md)' },
1614
],

src/clis/chatwise/history.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { cli, Strategy } from '../../registry.js';
22
import type { IPage } from '../../types.js';
3-
import { chatwiseRequiredEnv } from './shared.js';
43

54
export const historyCommand = cli({
65
site: 'chatwise',
@@ -9,7 +8,6 @@ export const historyCommand = cli({
98
domain: 'localhost',
109
strategy: Strategy.UI,
1110
browser: true,
12-
requiredEnv: chatwiseRequiredEnv,
1311
args: [],
1412
columns: ['Index', 'Title'],
1513
func: async (page: IPage) => {

src/clis/chatwise/model.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { cli, Strategy } from '../../registry.js';
22
import { SelectorError } from '../../errors.js';
33
import type { IPage } from '../../types.js';
4-
import { chatwiseRequiredEnv } from './shared.js';
54

65
export const modelCommand = cli({
76
site: 'chatwise',
@@ -10,7 +9,6 @@ export const modelCommand = cli({
109
domain: 'localhost',
1110
strategy: Strategy.UI,
1211
browser: true,
13-
requiredEnv: chatwiseRequiredEnv,
1412
args: [
1513
{ name: 'model-name', required: false, positional: true, help: 'Model to switch to (e.g. gpt-4, claude-3)' },
1614
],

src/clis/chatwise/new.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
import { makeNewCommand } from '../_shared/desktop-commands.js';
2-
import { chatwiseRequiredEnv } from './shared.js';
32

4-
export const newCommand = makeNewCommand('chatwise', 'ChatWise conversation', { requiredEnv: chatwiseRequiredEnv });
3+
export const newCommand = makeNewCommand('chatwise', 'ChatWise conversation');

src/clis/chatwise/read.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { cli, Strategy } from '../../registry.js';
22
import type { IPage } from '../../types.js';
3-
import { chatwiseRequiredEnv } from './shared.js';
43

54
export const readCommand = cli({
65
site: 'chatwise',
@@ -9,7 +8,6 @@ export const readCommand = cli({
98
domain: 'localhost',
109
strategy: Strategy.UI,
1110
browser: true,
12-
requiredEnv: chatwiseRequiredEnv,
1311
args: [],
1412
columns: ['Content'],
1513
func: async (page: IPage) => {

0 commit comments

Comments
 (0)