Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 37 additions & 25 deletions src/clis/antigravity/model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { SelectorError, ArgumentError } from '../../errors.js';

export const modelCommand = cli({
site: 'antigravity',
Expand All @@ -13,34 +14,45 @@ export const modelCommand = cli({
columns: ['Status'],
func: async (page, kwargs) => {
const targetName = kwargs.name.toLowerCase();

await page.evaluate(`
async () => {
const targetModelName = ${JSON.stringify(targetName)};

// 1. Locate the model selector dropdown trigger
const trigger = document.querySelector('div[aria-haspopup="dialog"] > div[tabindex="0"]');
if (!trigger) throw new Error('Could not find the model selector trigger in the UI');
trigger.click();

// 2. Wait a brief moment for React to mount the Portal/Dialog
await new Promise(r => setTimeout(r, 200));

// 3. Find the option spanning target text
const spans = Array.from(document.querySelectorAll('[role="dialog"] span'));
const target = spans.find(s => s.innerText.toLowerCase().includes(targetModelName));
if (!target) {
// If not found, click the trigger again to close it safely

try {
const result = await page.evaluate(`
async () => {
const targetModelName = ${JSON.stringify(targetName)};

// 1. Locate the model selector dropdown trigger
const trigger = document.querySelector('div[aria-haspopup="dialog"] > div[tabindex="0"]');
if (!trigger) throw new Error('Could not find the model selector trigger in the UI');
trigger.click();
throw new Error('Model matching "' + targetModelName + '" was not found in the dropdown list.');

// 2. Wait a brief moment for React to mount the Portal/Dialog
await new Promise(r => setTimeout(r, 200));

// 3. Find the option spanning target text
const spans = Array.from(document.querySelectorAll('[role="dialog"] span'));
const target = spans.find(s => s.innerText.toLowerCase().includes(targetModelName));
if (!target) {
// If not found, click the trigger again to close it safely
trigger.click();
throw new Error('Model matching "' + targetModelName + '" was not found in the dropdown list.');
}

// 4. Click the closest parent that handles the row action
const optionNode = target.closest('.cursor-pointer') || target;
optionNode.click();
return { ok: true };
}

// 4. Click the closest parent that handles the row action
const optionNode = target.closest('.cursor-pointer') || target;
optionNode.click();
`);
if (!result?.ok) throw new SelectorError('model selector', 'Could not find or select model in UI');
} catch (e: any) {
if (e.message?.includes('Could not find the model selector trigger')) {
throw new SelectorError('model selector trigger', 'Could not find the model selector trigger in Antigravity UI');
}
if (e.message?.includes('was not found in the dropdown list')) {
throw new ArgumentError(`Model matching "${targetName}" was not found in the dropdown list.`);
}
`);

throw e;
}
await page.wait(0.5);
return [{ Status: `Model switched to: ${kwargs.name}` }];
},
Expand Down
28 changes: 18 additions & 10 deletions src/clis/antigravity/new.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { SelectorError } from '../../errors.js';

export const newCommand = cli({
site: 'antigravity',
Expand All @@ -10,19 +11,26 @@ export const newCommand = cli({
args: [],
columns: ['status'],
func: async (page) => {
await page.evaluate(`
async () => {
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
if (!btn) throw new Error('Could not find New Conversation button');

// In case it's disabled, we must check, but we'll try to click it anyway
btn.click();
try {
await page.evaluate(`
async () => {
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
if (!btn) throw new Error('Could not find New Conversation button');

// In case it's disabled, we must check, but we'll try to click it anyway
btn.click();
}
`);
} catch (e: any) {
if (e.message?.includes('Could not find New Conversation button')) {
throw new SelectorError('New Conversation button', 'Could not find New Conversation button in Antigravity UI');
}
`);

throw e;
}

// Give it a moment to reset the UI
await page.wait(0.5);

return [{ status: 'Successfully started a new conversation' }];
},
});
37 changes: 23 additions & 14 deletions src/clis/antigravity/read.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { SelectorError } from '../../errors.js';

export const readCommand = cli({
site: 'antigravity',
Expand All @@ -12,25 +13,33 @@ export const readCommand = cli({
],
columns: ['role', 'content'],
func: async (page, kwargs) => {
// We execute a script inside Antigravity's Chromium environment to extract the text
// We execute a script inside Antigravity's Chromium environment to extract the text
// of the entire conversation pane.
const rawText = await page.evaluate(`
async () => {
const container = document.getElementById('conversation');
if (!container) throw new Error('Could not find conversation container');

// Extract the full visible text of the conversation
// In Electron/Chromium, innerText preserves basic visual line breaks nicely
return container.innerText;
let rawText: string;
try {
rawText = await page.evaluate(`
async () => {
const container = document.getElementById('conversation');
if (!container) throw new Error('Could not find conversation container');

// Extract the full visible text of the conversation
// In Electron/Chromium, innerText preserves basic visual line breaks nicely
return container.innerText;
}
`);
} catch (e: any) {
if (e.message?.includes('Could not find conversation container')) {
throw new SelectorError('#conversation element', 'Could not find conversation container in Antigravity UI');
}
`);

throw e;
}

// We can do simple heuristic parsing based on typical visual markers if needed.
// For now, we return the entire text blob, or just the last 2000 characters if it's too long.
const cleanText = String(rawText).trim();
return [{
role: 'history',
content: cleanText
return [{
role: 'history',
content: cleanText
}];
},
});
29 changes: 18 additions & 11 deletions src/clis/antigravity/send.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { SelectorError } from '../../errors.js';

export const sendCommand = cli({
site: 'antigravity',
Expand All @@ -17,18 +18,24 @@ export const sendCommand = cli({
// We use evaluate to focus and insert text because Lexical editors maintain
// absolute control over their DOM and don't respond to raw node.textContent.
// document.execCommand simulates a native paste/typing action perfectly.
await page.evaluate(`
async () => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
const editor = container.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find Antigravity input box');

editor.focus();
document.execCommand('insertText', false, ${JSON.stringify(text)});
try {
await page.evaluate(`
async () => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
const editor = container.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find Antigravity input box');

editor.focus();
document.execCommand('insertText', false, ${JSON.stringify(text)});
}
`);
} catch (e: any) {
if (e.message?.includes('Could not find')) {
throw new SelectorError('Antigravity input box', 'Could not find or focus input element');
}
`);

throw e;
}
// Wait for the React/Lexical state to flush the new input
await page.wait(0.5);

Expand Down
98 changes: 58 additions & 40 deletions src/clis/antigravity/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { CDPBridge } from '../../browser/cdp.js';
import type { IPage } from '../../types.js';
import { SelectorError, ArgumentError, TimeoutError, ConfigError, BrowserConnectError } from '../../errors.js';

// ─── Types ───────────────────────────────────────────────────────────

Expand Down Expand Up @@ -255,47 +256,64 @@ async function getLastAssistantReply(page: IPage, userText?: string): Promise<st
async function sendMessage(page: IPage, message: string, bridge?: CDPBridge): Promise<void> {
if (!bridge) {
// Fallback: use JS-based approach
await page.evaluate(`
(() => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
const editor = container?.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find input box');
editor.focus();
document.execCommand('insertText', false, ${JSON.stringify(message)});
})()
`);
try {
await page.evaluate(`
(() => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
const editor = container?.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find input box');
editor.focus();
document.execCommand('insertText', false, ${JSON.stringify(message)});
})()
`);
} catch (e: any) {
if (e.message?.includes('Could not find input box')) {
throw new SelectorError('Antigravity input box', 'Could not find Antigravity input box (Lexical editor) in UI');
}
throw e;
}
await sleep(500);
await page.pressKey('Enter');
return;
}

// Get the bounding box of the Lexical editor for a physical mouse click
const rect = await page.evaluate(`
(() => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
const editor = container.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find Antigravity input box');
const r = editor.getBoundingClientRect();
return JSON.stringify({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
})()
`);
const { x, y } = JSON.parse(String(rect));

// Physical mouse click to give the element real browser focus
await bridge.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
await sleep(50);
await bridge.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
await sleep(200);

// Inject text at the CDP level (no deprecated execCommand)
await bridge.send('Input.insertText', { text: message });
await sleep(300);

// Send Enter via native CDP key event
await bridge.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
await sleep(50);
await bridge.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
try {
const rect = await page.evaluate(`
(() => {
const container = document.getElementById('antigravity.agentSidePanelInputBox');
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
const editor = container.querySelector('[data-lexical-editor="true"]');
if (!editor) throw new Error('Could not find Antigravity input box');
const r = editor.getBoundingClientRect();
return JSON.stringify({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
})()
`);
const { x, y } = JSON.parse(String(rect));

// Physical mouse click to give the element real browser focus
await bridge.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
await sleep(50);
await bridge.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
await sleep(200);

// Inject text at the CDP level (no deprecated execCommand)
await bridge.send('Input.insertText', { text: message });
await sleep(300);

// Send Enter via native CDP key event
await bridge.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
await sleep(50);
await bridge.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
} catch (e: any) {
if (e.message?.includes('Could not find antigravity.agentSidePanelInputBox')) {
throw new SelectorError('#antigravity.agentSidePanelInputBox', 'Could not find Antigravity input container in UI');
}
if (e.message?.includes('Could not find Antigravity input box')) {
throw new SelectorError('Antigravity input box', 'Could not find Antigravity input box (Lexical editor) in UI');
}
throw e;
}
}

async function waitForReply(
Expand Down Expand Up @@ -349,7 +367,7 @@ async function waitForReply(
await sleep(pollInterval);
}

throw new Error('Timeout waiting for Antigravity reply');
throw new TimeoutError('Antigravity reply', timeout / 1000);
}

// ─── Request Handlers ────────────────────────────────────────────────
Expand All @@ -362,13 +380,13 @@ async function handleMessages(
// Extract the last user message
const userMessages = body.messages.filter(m => m.role === 'user');
if (userMessages.length === 0) {
throw new Error('No user message found in request');
throw new ArgumentError('No user message found in request');
}
const lastUserMsg = userMessages[userMessages.length - 1];
const userText = extractTextContent(lastUserMsg.content);

if (!userText.trim()) {
throw new Error('Empty user message');
throw new ArgumentError('Empty user message');
}

// Optimization 1: New conversation if this is the first message in the session
Expand Down Expand Up @@ -437,7 +455,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {

const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
if (!endpoint) {
throw new Error(
throw new ConfigError(
'OPENCLI_CDP_ENDPOINT is not set.\n' +
'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve'
);
Expand All @@ -464,7 +482,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
} catch (err: any) {
cdp = null;
const isRefused = err?.cause?.code === 'ECONNREFUSED' || err?.message?.includes('ECONNREFUSED');
throw new Error(
throw new BrowserConnectError(
isRefused
? `Cannot connect to Antigravity at ${endpoint}.\n` +
' 1. Make sure Antigravity is running\n' +
Expand Down
3 changes: 2 additions & 1 deletion src/clis/barchart/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
*/
import { cli, Strategy } from '../../registry.js';
import { AuthRequiredError } from '../../errors.js';

cli({
site: 'barchart',
Expand Down Expand Up @@ -100,7 +101,7 @@ cli({
if (!data) return [];

if (data.error === 'no-csrf') {
throw new Error('Could not extract CSRF token from barchart.com. Make sure you are logged in.');
throw new AuthRequiredError('barchart.com', 'Could not extract CSRF token. Make sure you are logged in.');
}

if (!Array.isArray(data)) return [];
Expand Down
3 changes: 2 additions & 1 deletion src/clis/boss/chatmsg.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { EmptyResultError } from '../../errors.js';
import { requirePage, navigateToChat, bossFetch, findFriendByUid } from './utils.js';

cli({
Expand All @@ -19,7 +20,7 @@ cli({
await navigateToChat(page);

const friend = await findFriendByUid(page, kwargs.uid);
if (!friend) throw new Error('未找到该候选人');
if (!friend) throw new EmptyResultError('未找到该候选人');

const gid = friend.uid;
const securityId = encodeURIComponent(friend.securityId);
Expand Down
Loading
Loading