@jackwener/opencli 1.7.3 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Dynamic adapter commands are registered via commanderAdapter.ts.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
8
9
|
import * as path from 'node:path';
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { Command } from 'commander';
|
|
@@ -13,20 +14,132 @@ import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
|
13
14
|
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
14
15
|
import { serializeCommand, formatArgSummary } from './serialization.js';
|
|
15
16
|
import { render as renderOutput } from './output.js';
|
|
16
|
-
import { getBrowserFactory, browserSession } from './runtime.js';
|
|
17
17
|
import { PKG_VERSION } from './version.js';
|
|
18
18
|
import { printCompletionScript } from './completion.js';
|
|
19
19
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
|
|
20
20
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
21
|
-
import { EXIT_CODES, getErrorMessage } from './errors.js';
|
|
22
|
-
import {
|
|
21
|
+
import { EXIT_CODES, getErrorMessage, BrowserConnectError } from './errors.js';
|
|
22
|
+
import { TargetError } from './browser/target-errors.js';
|
|
23
|
+
import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
|
|
24
|
+
import { daemonStatus, daemonStop } from './commands/daemon.js';
|
|
23
25
|
import { log } from './logger.js';
|
|
24
26
|
const CLI_FILE = fileURLToPath(import.meta.url);
|
|
27
|
+
const DEFAULT_BROWSER_WORKSPACE = 'browser:default';
|
|
28
|
+
const BROWSER_TAB_OPTION_DESCRIPTION = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
|
|
29
|
+
function getBrowserCacheDir() {
|
|
30
|
+
return process.env.OPENCLI_CACHE_DIR || path.join(os.homedir(), '.opencli', 'cache');
|
|
31
|
+
}
|
|
32
|
+
function getBrowserTargetStatePath(scope = DEFAULT_BROWSER_WORKSPACE) {
|
|
33
|
+
const safeWorkspace = scope.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
34
|
+
return path.join(getBrowserCacheDir(), 'browser-state', `${safeWorkspace}.json`);
|
|
35
|
+
}
|
|
36
|
+
function loadBrowserTargetState(scope = DEFAULT_BROWSER_WORKSPACE) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(getBrowserTargetStatePath(scope), 'utf-8');
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function saveBrowserTargetState(defaultPage, scope = DEFAULT_BROWSER_WORKSPACE) {
|
|
47
|
+
const target = getBrowserTargetStatePath(scope);
|
|
48
|
+
if (!defaultPage) {
|
|
49
|
+
fs.rmSync(target, { force: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
53
|
+
fs.writeFileSync(target, JSON.stringify({ defaultPage, updatedAt: new Date().toISOString() }), 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
function hasBrowserTabTarget(tabs, targetPage) {
|
|
56
|
+
return tabs.some((tab) => {
|
|
57
|
+
return typeof tab === 'object'
|
|
58
|
+
&& tab !== null
|
|
59
|
+
&& 'page' in tab
|
|
60
|
+
&& typeof tab.page === 'string'
|
|
61
|
+
&& tab.page === targetPage;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function resolveBrowserTargetInSession(page, targetPage, opts) {
|
|
65
|
+
const candidate = targetPage.trim();
|
|
66
|
+
if (!candidate)
|
|
67
|
+
return undefined;
|
|
68
|
+
let tabs;
|
|
69
|
+
try {
|
|
70
|
+
tabs = await page.tabs();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (opts.source === 'saved') {
|
|
74
|
+
saveBrowserTargetState(undefined, opts.scope);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Target tab ${candidate} could not be validated in the current browser session. ` +
|
|
78
|
+
'The Browser Bridge workspace may have restarted; re-run "opencli browser tab list" and choose a current target.', { cause: err });
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(tabs) && hasBrowserTabTarget(tabs, candidate)) {
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
if (opts.source === 'saved') {
|
|
84
|
+
saveBrowserTargetState(undefined, opts.scope);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Target tab ${candidate} is not part of the current browser session. ` +
|
|
88
|
+
'The Browser Bridge workspace may have restarted; re-run "opencli browser tab list" and choose a current target.');
|
|
89
|
+
}
|
|
90
|
+
async function resolveStoredBrowserTarget(page, scope = DEFAULT_BROWSER_WORKSPACE) {
|
|
91
|
+
const defaultPage = loadBrowserTargetState(scope)?.defaultPage?.trim();
|
|
92
|
+
if (!defaultPage)
|
|
93
|
+
return undefined;
|
|
94
|
+
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
95
|
+
}
|
|
25
96
|
/** Create a browser page for browser commands. Uses a dedicated browser workspace for session persistence. */
|
|
26
|
-
async function getBrowserPage() {
|
|
97
|
+
async function getBrowserPage(targetPage) {
|
|
27
98
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
28
99
|
const bridge = new BrowserBridge();
|
|
29
|
-
|
|
100
|
+
const envTimeout = process.env.OPENCLI_BROWSER_TIMEOUT;
|
|
101
|
+
const idleTimeout = envTimeout ? parseInt(envTimeout, 10) : undefined;
|
|
102
|
+
const page = await bridge.connect({
|
|
103
|
+
timeout: 30,
|
|
104
|
+
workspace: DEFAULT_BROWSER_WORKSPACE,
|
|
105
|
+
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
106
|
+
});
|
|
107
|
+
const resolvedTargetPage = targetPage
|
|
108
|
+
? await resolveBrowserTargetInSession(page, targetPage, { scope: DEFAULT_BROWSER_WORKSPACE, source: 'explicit' })
|
|
109
|
+
: await resolveStoredBrowserTarget(page, DEFAULT_BROWSER_WORKSPACE);
|
|
110
|
+
if (resolvedTargetPage) {
|
|
111
|
+
if (!page.setActivePage) {
|
|
112
|
+
throw new Error('This browser session does not support explicit tab targeting');
|
|
113
|
+
}
|
|
114
|
+
page.setActivePage(resolvedTargetPage);
|
|
115
|
+
}
|
|
116
|
+
return page;
|
|
117
|
+
}
|
|
118
|
+
function addBrowserTabOption(command) {
|
|
119
|
+
return command.option('--tab <targetId>', BROWSER_TAB_OPTION_DESCRIPTION);
|
|
120
|
+
}
|
|
121
|
+
function getBrowserTargetId(command) {
|
|
122
|
+
if (!command)
|
|
123
|
+
return undefined;
|
|
124
|
+
const opts = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
|
|
125
|
+
return typeof opts.tab === 'string' && opts.tab.trim() ? opts.tab.trim() : undefined;
|
|
126
|
+
}
|
|
127
|
+
function resolveBrowserTabTarget(targetId, opts) {
|
|
128
|
+
if (typeof targetId === 'string' && targetId.trim())
|
|
129
|
+
return targetId.trim();
|
|
130
|
+
if (typeof opts?.tab === 'string' && opts.tab.trim())
|
|
131
|
+
return opts.tab.trim();
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function parsePositiveIntOption(val, label, fallback) {
|
|
135
|
+
if (val === undefined)
|
|
136
|
+
return fallback;
|
|
137
|
+
const parsed = parseInt(val, 10);
|
|
138
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
139
|
+
console.error(`[cli] Invalid ${label}="${val}", using default ${fallback}`);
|
|
140
|
+
return fallback;
|
|
141
|
+
}
|
|
142
|
+
return parsed;
|
|
30
143
|
}
|
|
31
144
|
function applyVerbose(opts) {
|
|
32
145
|
if (opts.verbose)
|
|
@@ -129,119 +242,6 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
129
242
|
console.log(renderVerifyReport(r));
|
|
130
243
|
process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
131
244
|
});
|
|
132
|
-
// ── Built-in: explore / synthesize / generate / cascade ───────────────────
|
|
133
|
-
program
|
|
134
|
-
.command('explore')
|
|
135
|
-
.alias('probe')
|
|
136
|
-
.description('Explore a website: discover APIs, stores, and recommend strategies')
|
|
137
|
-
.argument('<url>')
|
|
138
|
-
.option('--site <name>')
|
|
139
|
-
.option('--goal <text>')
|
|
140
|
-
.option('--wait <s>', '', '3')
|
|
141
|
-
.option('--auto', 'Enable interactive fuzzing')
|
|
142
|
-
.option('--click <labels>', 'Comma-separated labels to click before fuzzing')
|
|
143
|
-
.option('-v, --verbose', 'Debug output')
|
|
144
|
-
.action(async (url, opts) => {
|
|
145
|
-
applyVerbose(opts);
|
|
146
|
-
const { exploreUrl, renderExploreSummary } = await import('./explore.js');
|
|
147
|
-
const clickLabels = opts.click
|
|
148
|
-
? opts.click.split(',').map((s) => s.trim())
|
|
149
|
-
: undefined;
|
|
150
|
-
const workspace = `explore:${inferHost(url, opts.site)}`;
|
|
151
|
-
const result = await exploreUrl(url, {
|
|
152
|
-
BrowserFactory: getBrowserFactory(),
|
|
153
|
-
site: opts.site,
|
|
154
|
-
goal: opts.goal,
|
|
155
|
-
waitSeconds: parseFloat(opts.wait),
|
|
156
|
-
auto: opts.auto,
|
|
157
|
-
clickLabels,
|
|
158
|
-
workspace,
|
|
159
|
-
});
|
|
160
|
-
console.log(renderExploreSummary(result));
|
|
161
|
-
});
|
|
162
|
-
program
|
|
163
|
-
.command('synthesize')
|
|
164
|
-
.description('Synthesize CLIs from explore')
|
|
165
|
-
.argument('<target>')
|
|
166
|
-
.option('--top <n>', '', '3')
|
|
167
|
-
.option('-v, --verbose', 'Debug output')
|
|
168
|
-
.action(async (target, opts) => {
|
|
169
|
-
applyVerbose(opts);
|
|
170
|
-
const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
|
|
171
|
-
console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
|
|
172
|
-
});
|
|
173
|
-
program
|
|
174
|
-
.command('generate')
|
|
175
|
-
.description('One-shot: explore → synthesize → verify → register')
|
|
176
|
-
.argument('<url>')
|
|
177
|
-
.option('--goal <text>')
|
|
178
|
-
.option('--site <name>')
|
|
179
|
-
.option('--format <fmt>', 'Output format: table, json', 'table')
|
|
180
|
-
.option('--no-register', 'Verify the generated adapter without registering it')
|
|
181
|
-
.option('-v, --verbose', 'Debug output')
|
|
182
|
-
.action(async (url, opts) => {
|
|
183
|
-
applyVerbose(opts);
|
|
184
|
-
const { generateVerifiedFromUrl, renderGenerateVerifiedSummary } = await import('./generate-verified.js');
|
|
185
|
-
const workspace = `generate:${inferHost(url, opts.site)}`;
|
|
186
|
-
const r = await generateVerifiedFromUrl({
|
|
187
|
-
url,
|
|
188
|
-
BrowserFactory: getBrowserFactory(),
|
|
189
|
-
goal: opts.goal,
|
|
190
|
-
site: opts.site,
|
|
191
|
-
workspace,
|
|
192
|
-
noRegister: opts.register === false,
|
|
193
|
-
});
|
|
194
|
-
if (opts.format === 'json')
|
|
195
|
-
console.log(JSON.stringify(r, null, 2));
|
|
196
|
-
else
|
|
197
|
-
console.log(renderGenerateVerifiedSummary(r));
|
|
198
|
-
process.exitCode = r.status === 'success' ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
199
|
-
});
|
|
200
|
-
// ── Built-in: record ─────────────────────────────────────────────────────
|
|
201
|
-
program
|
|
202
|
-
.command('record')
|
|
203
|
-
.description('Record API calls from a live browser session → generate YAML candidates')
|
|
204
|
-
.argument('<url>', 'URL to open and record')
|
|
205
|
-
.option('--site <name>', 'Site name (inferred from URL if omitted)')
|
|
206
|
-
.option('--out <dir>', 'Output directory for candidates')
|
|
207
|
-
.option('--poll <ms>', 'Poll interval in milliseconds', '2000')
|
|
208
|
-
.option('--timeout <ms>', 'Auto-stop after N milliseconds (default: 60000)', '60000')
|
|
209
|
-
.option('-v, --verbose', 'Debug output')
|
|
210
|
-
.action(async (url, opts) => {
|
|
211
|
-
applyVerbose(opts);
|
|
212
|
-
const { recordSession, renderRecordSummary } = await import('./record.js');
|
|
213
|
-
const result = await recordSession({
|
|
214
|
-
BrowserFactory: getBrowserFactory(),
|
|
215
|
-
url,
|
|
216
|
-
site: opts.site,
|
|
217
|
-
outDir: opts.out,
|
|
218
|
-
pollMs: parseInt(opts.poll, 10),
|
|
219
|
-
timeoutMs: parseInt(opts.timeout, 10),
|
|
220
|
-
});
|
|
221
|
-
console.log(renderRecordSummary(result));
|
|
222
|
-
process.exitCode = result.candidateCount > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.EMPTY_RESULT;
|
|
223
|
-
});
|
|
224
|
-
program
|
|
225
|
-
.command('cascade')
|
|
226
|
-
.description('Strategy cascade: find simplest working strategy')
|
|
227
|
-
.argument('<url>')
|
|
228
|
-
.option('--site <name>')
|
|
229
|
-
.option('-v, --verbose', 'Debug output')
|
|
230
|
-
.action(async (url, opts) => {
|
|
231
|
-
applyVerbose(opts);
|
|
232
|
-
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
|
233
|
-
const workspace = `cascade:${inferHost(url, opts.site)}`;
|
|
234
|
-
const result = await browserSession(getBrowserFactory(), async (page) => {
|
|
235
|
-
try {
|
|
236
|
-
const siteUrl = new URL(url);
|
|
237
|
-
await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
|
|
238
|
-
await page.wait(2);
|
|
239
|
-
}
|
|
240
|
-
catch { }
|
|
241
|
-
return cascadeProbe(page, url);
|
|
242
|
-
}, { workspace });
|
|
243
|
-
console.log(renderCascadeResult(result));
|
|
244
|
-
});
|
|
245
245
|
// ── Built-in: browser (browser control for Claude Code skill) ───────────────
|
|
246
246
|
//
|
|
247
247
|
// Make websites accessible for AI agents.
|
|
@@ -249,35 +249,115 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
249
249
|
const browser = program
|
|
250
250
|
.command('browser')
|
|
251
251
|
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)');
|
|
252
|
+
/** Resolve a ref/CSS target via the unified resolver, throwing TargetError on failure. */
|
|
253
|
+
async function resolveRef(page, ref) {
|
|
254
|
+
const resolution = await page.evaluate(resolveTargetJs(ref));
|
|
255
|
+
if (!resolution.ok) {
|
|
256
|
+
throw new TargetError(resolution);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
252
259
|
/** Wrap browser actions with error handling and optional --json output */
|
|
253
260
|
function browserAction(fn) {
|
|
254
261
|
return async (...args) => {
|
|
255
262
|
try {
|
|
256
|
-
const
|
|
263
|
+
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
264
|
+
const targetPage = getBrowserTargetId(command);
|
|
265
|
+
const page = await getBrowserPage(targetPage);
|
|
257
266
|
await fn(page, ...args);
|
|
258
267
|
}
|
|
259
268
|
catch (err) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
if (err instanceof BrowserConnectError) {
|
|
270
|
+
log.error(err.message);
|
|
271
|
+
if (err.hint)
|
|
272
|
+
log.error(`Hint: ${err.hint}`);
|
|
263
273
|
}
|
|
264
|
-
else if (
|
|
265
|
-
log.error(`
|
|
274
|
+
else if (err instanceof TargetError) {
|
|
275
|
+
log.error(`[${err.code}] ${err.message}`);
|
|
276
|
+
if (err.hint)
|
|
277
|
+
log.error(`Hint: ${err.hint}`);
|
|
278
|
+
if (err.candidates?.length) {
|
|
279
|
+
log.error('Candidates:');
|
|
280
|
+
err.candidates.forEach((c, i) => log.error(` ${i + 1}. ${c}`));
|
|
281
|
+
}
|
|
266
282
|
}
|
|
267
283
|
else {
|
|
268
|
-
|
|
284
|
+
const msg = getErrorMessage(err);
|
|
285
|
+
if (msg.includes('attach failed') || msg.includes('chrome-extension://')) {
|
|
286
|
+
log.error(`Browser attach failed — another extension may be interfering. Try disabling 1Password.`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
log.error(msg);
|
|
290
|
+
}
|
|
269
291
|
}
|
|
270
292
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
271
293
|
}
|
|
272
294
|
};
|
|
273
295
|
}
|
|
296
|
+
const browserTab = browser
|
|
297
|
+
.command('tab')
|
|
298
|
+
.description('Tab management — list, create, and close tabs in the automation window');
|
|
299
|
+
browserTab.command('list')
|
|
300
|
+
.description('List tabs in the automation window with target IDs')
|
|
301
|
+
.action(browserAction(async (page) => {
|
|
302
|
+
const tabs = await page.tabs();
|
|
303
|
+
console.log(JSON.stringify(tabs, null, 2));
|
|
304
|
+
}));
|
|
305
|
+
browserTab.command('new')
|
|
306
|
+
.argument('[url]', 'Optional URL to open in the new tab')
|
|
307
|
+
.description('Create a new tab and print its target ID')
|
|
308
|
+
.action(browserAction(async (page, url) => {
|
|
309
|
+
if (!page.newTab) {
|
|
310
|
+
throw new Error('This browser session does not support creating tabs');
|
|
311
|
+
}
|
|
312
|
+
const createdPage = await page.newTab(url);
|
|
313
|
+
console.log(JSON.stringify({
|
|
314
|
+
page: createdPage,
|
|
315
|
+
url: url ?? null,
|
|
316
|
+
}, null, 2));
|
|
317
|
+
}));
|
|
318
|
+
addBrowserTabOption(browserTab.command('select')
|
|
319
|
+
.argument('[targetId]', 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"')
|
|
320
|
+
.description('Select a tab by target ID and make it the default browser tab'))
|
|
321
|
+
.action(browserAction(async (page, targetId, opts) => {
|
|
322
|
+
const resolvedTarget = resolveBrowserTabTarget(targetId, opts);
|
|
323
|
+
if (!resolvedTarget) {
|
|
324
|
+
throw new Error('Target tab required. Pass it as an argument or --tab <targetId>.');
|
|
325
|
+
}
|
|
326
|
+
await page.selectTab(resolvedTarget);
|
|
327
|
+
saveBrowserTargetState(resolvedTarget, DEFAULT_BROWSER_WORKSPACE);
|
|
328
|
+
console.log(JSON.stringify({ selected: resolvedTarget }, null, 2));
|
|
329
|
+
}));
|
|
330
|
+
addBrowserTabOption(browserTab.command('close')
|
|
331
|
+
.argument('[targetId]', 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"')
|
|
332
|
+
.description('Close a tab by target ID'))
|
|
333
|
+
.action(browserAction(async (page, targetId, opts) => {
|
|
334
|
+
const resolvedTarget = resolveBrowserTabTarget(targetId, opts);
|
|
335
|
+
if (!page.closeTab) {
|
|
336
|
+
throw new Error('This browser session does not support closing tabs');
|
|
337
|
+
}
|
|
338
|
+
if (!resolvedTarget) {
|
|
339
|
+
throw new Error('Target tab required. Pass it as an argument or --tab <targetId>.');
|
|
340
|
+
}
|
|
341
|
+
const validatedTarget = await resolveBrowserTargetInSession(page, resolvedTarget, {
|
|
342
|
+
scope: DEFAULT_BROWSER_WORKSPACE,
|
|
343
|
+
source: 'explicit',
|
|
344
|
+
});
|
|
345
|
+
if (!validatedTarget) {
|
|
346
|
+
throw new Error(`Target tab ${resolvedTarget} is not part of the current browser session.`);
|
|
347
|
+
}
|
|
348
|
+
await page.closeTab(validatedTarget);
|
|
349
|
+
if (loadBrowserTargetState(DEFAULT_BROWSER_WORKSPACE)?.defaultPage === validatedTarget) {
|
|
350
|
+
saveBrowserTargetState(undefined, DEFAULT_BROWSER_WORKSPACE);
|
|
351
|
+
}
|
|
352
|
+
console.log(JSON.stringify({ closed: validatedTarget }, null, 2));
|
|
353
|
+
}));
|
|
274
354
|
// ── Navigation ──
|
|
275
355
|
/** Network interceptor JS — injected on every open/navigate to capture fetch/XHR */
|
|
276
356
|
const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=50000,F=window.fetch;window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();if(window.__opencli_net.length<M){var b=null;if(t.length<=B)try{b=JSON.parse(t)}catch(e){b=t}window.__opencli_net.push({url:r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),method:(arguments[1]&&arguments[1].method)||'GET',status:r.status,size:t.length,ct:ct,body:b})}}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if((ct.includes('json')||ct.includes('text'))&&window.__opencli_net.length<M){var t=x.responseText,b=null;if(t&&t.length<=B)try{b=JSON.parse(t)}catch(e){b=t}window.__opencli_net.push({url:x._ou,method:x._om||'GET',status:x.status,size:t?t.length:0,ct:ct,body:b})}}catch(e){}});return S.apply(this,arguments)}})()`;
|
|
277
|
-
browser.command('open').argument('<url>').description('Open URL in automation window')
|
|
357
|
+
addBrowserTabOption(browser.command('open').argument('<url>').description('Open URL in automation window'))
|
|
278
358
|
.action(browserAction(async (page, url) => {
|
|
279
359
|
// Start session-level capture before navigation (catches initial requests)
|
|
280
|
-
const hasSessionCapture = await page.startNetworkCapture?.()
|
|
360
|
+
const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
|
|
281
361
|
await page.goto(url);
|
|
282
362
|
await page.wait(2);
|
|
283
363
|
// Fallback: inject JS interceptor when session capture is unavailable
|
|
@@ -287,15 +367,18 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
287
367
|
}
|
|
288
368
|
catch { /* non-fatal */ }
|
|
289
369
|
}
|
|
290
|
-
console.log(
|
|
370
|
+
console.log(JSON.stringify({
|
|
371
|
+
url: await page.getCurrentUrl?.() ?? url,
|
|
372
|
+
...(page.getActivePage?.() ? { page: page.getActivePage?.() } : {}),
|
|
373
|
+
}, null, 2));
|
|
291
374
|
}));
|
|
292
|
-
browser.command('back').description('Go back in browser history')
|
|
375
|
+
addBrowserTabOption(browser.command('back').description('Go back in browser history'))
|
|
293
376
|
.action(browserAction(async (page) => {
|
|
294
377
|
await page.evaluate('history.back()');
|
|
295
378
|
await page.wait(2);
|
|
296
379
|
console.log('Navigated back');
|
|
297
380
|
}));
|
|
298
|
-
browser.command('scroll').argument('<direction>', 'up or down').option('--amount <pixels>', 'Pixels to scroll', '500')
|
|
381
|
+
addBrowserTabOption(browser.command('scroll').argument('<direction>', 'up or down').option('--amount <pixels>', 'Pixels to scroll', '500'))
|
|
299
382
|
.description('Scroll page')
|
|
300
383
|
.action(browserAction(async (page, direction, opts) => {
|
|
301
384
|
if (direction !== 'up' && direction !== 'down') {
|
|
@@ -307,14 +390,19 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
307
390
|
console.log(`Scrolled ${direction}`);
|
|
308
391
|
}));
|
|
309
392
|
// ── Inspect ──
|
|
310
|
-
browser.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
393
|
+
addBrowserTabOption(browser.command('state').description('Page state: URL, title, interactive elements with [N] indices'))
|
|
311
394
|
.action(browserAction(async (page) => {
|
|
312
395
|
const snapshot = await page.snapshot({ viewportExpand: 2000 });
|
|
313
396
|
const url = await page.getCurrentUrl?.() ?? '';
|
|
314
397
|
console.log(`URL: ${url}\n`);
|
|
315
398
|
console.log(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2));
|
|
316
399
|
}));
|
|
317
|
-
browser.command('
|
|
400
|
+
addBrowserTabOption(browser.command('frames').description('List cross-origin iframe targets in snapshot order'))
|
|
401
|
+
.action(browserAction(async (page) => {
|
|
402
|
+
const frames = await page.frames?.() ?? [];
|
|
403
|
+
console.log(JSON.stringify(frames, null, 2));
|
|
404
|
+
}));
|
|
405
|
+
addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
|
|
318
406
|
.description('Take screenshot')
|
|
319
407
|
.action(browserAction(async (page, path) => {
|
|
320
408
|
if (path) {
|
|
@@ -327,59 +415,53 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
327
415
|
}));
|
|
328
416
|
// ── Get commands (structured data extraction) ──
|
|
329
417
|
const get = browser.command('get').description('Get page properties');
|
|
330
|
-
get.command('title').description('Page title')
|
|
418
|
+
addBrowserTabOption(get.command('title').description('Page title'))
|
|
331
419
|
.action(browserAction(async (page) => {
|
|
332
420
|
console.log(await page.evaluate('document.title'));
|
|
333
421
|
}));
|
|
334
|
-
get.command('url').description('Current page URL')
|
|
422
|
+
addBrowserTabOption(get.command('url').description('Current page URL'))
|
|
335
423
|
.action(browserAction(async (page) => {
|
|
336
424
|
console.log(await page.getCurrentUrl?.() ?? await page.evaluate('location.href'));
|
|
337
425
|
}));
|
|
338
|
-
get.command('text').argument('<index>', 'Element index').description('Element text content')
|
|
426
|
+
addBrowserTabOption(get.command('text').argument('<index>', 'Element index').description('Element text content'))
|
|
339
427
|
.action(browserAction(async (page, index) => {
|
|
340
|
-
|
|
428
|
+
await resolveRef(page, String(index));
|
|
429
|
+
const text = await page.evaluate(getTextResolvedJs());
|
|
341
430
|
console.log(text ?? '(empty)');
|
|
342
431
|
}));
|
|
343
|
-
get.command('value').argument('<index>', 'Element index').description('Input/textarea value')
|
|
432
|
+
addBrowserTabOption(get.command('value').argument('<index>', 'Element index').description('Input/textarea value'))
|
|
344
433
|
.action(browserAction(async (page, index) => {
|
|
345
|
-
|
|
434
|
+
await resolveRef(page, String(index));
|
|
435
|
+
const val = await page.evaluate(getValueResolvedJs());
|
|
346
436
|
console.log(val ?? '(empty)');
|
|
347
437
|
}));
|
|
348
|
-
get.command('html').option('--selector <css>', 'CSS selector scope').description('Page HTML (or scoped)')
|
|
438
|
+
addBrowserTabOption(get.command('html').option('--selector <css>', 'CSS selector scope').description('Page HTML (or scoped)'))
|
|
349
439
|
.action(browserAction(async (page, opts) => {
|
|
350
440
|
const sel = opts.selector ? JSON.stringify(opts.selector) : 'null';
|
|
351
441
|
const html = await page.evaluate(`(${sel} ? document.querySelector(${sel})?.outerHTML : document.documentElement.outerHTML)?.slice(0, 50000)`);
|
|
352
442
|
console.log(html ?? '(empty)');
|
|
353
443
|
}));
|
|
354
|
-
get.command('attributes').argument('<index>', 'Element index').description('Element attributes')
|
|
444
|
+
addBrowserTabOption(get.command('attributes').argument('<index>', 'Element index').description('Element attributes'))
|
|
355
445
|
.action(browserAction(async (page, index) => {
|
|
356
|
-
|
|
446
|
+
await resolveRef(page, String(index));
|
|
447
|
+
const attrs = await page.evaluate(getAttributesResolvedJs());
|
|
357
448
|
console.log(attrs ?? '{}');
|
|
358
449
|
}));
|
|
359
450
|
// ── Interact ──
|
|
360
|
-
browser.command('click').argument('<index>', 'Element index from state').description('Click element by index')
|
|
451
|
+
addBrowserTabOption(browser.command('click').argument('<index>', 'Element index from state').description('Click element by index'))
|
|
361
452
|
.action(browserAction(async (page, index) => {
|
|
362
453
|
await page.click(index);
|
|
363
454
|
console.log(`Clicked element [${index}]`);
|
|
364
455
|
}));
|
|
365
|
-
browser.command('type').argument('<index>', 'Element index').argument('<text>', 'Text to type')
|
|
456
|
+
addBrowserTabOption(browser.command('type').argument('<index>', 'Element index').argument('<text>', 'Text to type'))
|
|
366
457
|
.description('Click element, then type text')
|
|
367
458
|
.action(browserAction(async (page, index, text) => {
|
|
368
459
|
await page.click(index);
|
|
369
460
|
await page.wait(0.3);
|
|
370
461
|
await page.typeText(index, text);
|
|
371
462
|
// Detect autocomplete/combobox fields and wait for dropdown suggestions
|
|
372
|
-
|
|
373
|
-
const isAutocomplete = await page.evaluate(
|
|
374
|
-
(() => {
|
|
375
|
-
const el = document.querySelector('[data-opencli-ref="' + ${safeIndex} + '"]');
|
|
376
|
-
if (!el) return false;
|
|
377
|
-
const role = el.getAttribute('role');
|
|
378
|
-
const ac = el.getAttribute('aria-autocomplete');
|
|
379
|
-
const list = el.getAttribute('list');
|
|
380
|
-
return role === 'combobox' || ac === 'list' || ac === 'both' || !!list;
|
|
381
|
-
})()
|
|
382
|
-
`);
|
|
463
|
+
// __resolved is already set by typeText's resolver call
|
|
464
|
+
const isAutocomplete = await page.evaluate(isAutocompleteResolvedJs());
|
|
383
465
|
if (isAutocomplete) {
|
|
384
466
|
await page.wait(0.4);
|
|
385
467
|
console.log(`Typed "${text}" into autocomplete [${index}] — use state to see suggestions`);
|
|
@@ -388,23 +470,11 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
388
470
|
console.log(`Typed "${text}" into element [${index}]`);
|
|
389
471
|
}
|
|
390
472
|
}));
|
|
391
|
-
browser.command('select').argument('<index>', 'Element index of <select>').argument('<option>', 'Option text')
|
|
473
|
+
addBrowserTabOption(browser.command('select').argument('<index>', 'Element index of <select>').argument('<option>', 'Option text'))
|
|
392
474
|
.description('Select dropdown option')
|
|
393
475
|
.action(browserAction(async (page, index, option) => {
|
|
394
|
-
|
|
395
|
-
const result = await page.evaluate(
|
|
396
|
-
(function() {
|
|
397
|
-
var sel = document.querySelector('[data-opencli-ref="' + ${safeIdx} + '"]');
|
|
398
|
-
if (!sel || sel.tagName !== 'SELECT') return { error: 'Not a <select>' };
|
|
399
|
-
var match = Array.from(sel.options).find(o => o.text.trim() === ${JSON.stringify(option)} || o.value === ${JSON.stringify(option)});
|
|
400
|
-
if (!match) return { error: 'Option not found', available: Array.from(sel.options).map(o => o.text.trim()) };
|
|
401
|
-
var setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
402
|
-
if (setter) setter.call(sel, match.value); else sel.value = match.value;
|
|
403
|
-
sel.dispatchEvent(new Event('input', {bubbles:true}));
|
|
404
|
-
sel.dispatchEvent(new Event('change', {bubbles:true}));
|
|
405
|
-
return { selected: match.text };
|
|
406
|
-
})()
|
|
407
|
-
`);
|
|
476
|
+
await resolveRef(page, String(index));
|
|
477
|
+
const result = await page.evaluate(selectResolvedJs(option));
|
|
408
478
|
if (result?.error) {
|
|
409
479
|
console.error(`Error: ${result.error}${result.available ? ` — Available: ${result.available.join(', ')}` : ''}`);
|
|
410
480
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
@@ -413,14 +483,14 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
413
483
|
console.log(`Selected "${result?.selected}" in element [${index}]`);
|
|
414
484
|
}
|
|
415
485
|
}));
|
|
416
|
-
browser.command('keys').argument('<key>', 'Key to press (Enter, Escape, Tab, Control+a)')
|
|
486
|
+
addBrowserTabOption(browser.command('keys').argument('<key>', 'Key to press (Enter, Escape, Tab, Control+a)'))
|
|
417
487
|
.description('Press keyboard key')
|
|
418
488
|
.action(browserAction(async (page, key) => {
|
|
419
489
|
await page.pressKey(key);
|
|
420
490
|
console.log(`Pressed: ${key}`);
|
|
421
491
|
}));
|
|
422
492
|
// ── Wait commands ──
|
|
423
|
-
browser.command('wait')
|
|
493
|
+
addBrowserTabOption(browser.command('wait'))
|
|
424
494
|
.argument('<type>', 'selector, text, or time')
|
|
425
495
|
.argument('[value]', 'CSS selector, text string, or seconds')
|
|
426
496
|
.option('--timeout <ms>', 'Timeout in milliseconds', '10000')
|
|
@@ -456,16 +526,34 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
456
526
|
}
|
|
457
527
|
}));
|
|
458
528
|
// ── Extract ──
|
|
459
|
-
browser.command('eval')
|
|
460
|
-
.
|
|
461
|
-
|
|
529
|
+
addBrowserTabOption(browser.command('eval')
|
|
530
|
+
.argument('<js>', 'JavaScript code')
|
|
531
|
+
.option('--frame <index>', 'Cross-origin iframe index from "browser frames"')
|
|
532
|
+
.description('Execute JS in page context, return result'))
|
|
533
|
+
.action(browserAction(async (page, js, opts) => {
|
|
534
|
+
let result;
|
|
535
|
+
if (opts.frame !== undefined) {
|
|
536
|
+
const frameIndex = Number.parseInt(opts.frame, 10);
|
|
537
|
+
if (!Number.isInteger(frameIndex) || frameIndex < 0) {
|
|
538
|
+
console.error(`Invalid frame index "${opts.frame}". Use a 0-based index from "browser frames".`);
|
|
539
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (!page.evaluateInFrame) {
|
|
543
|
+
throw new Error('This browser session does not support frame-targeted evaluation');
|
|
544
|
+
}
|
|
545
|
+
result = await page.evaluateInFrame(js, frameIndex);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
result = await page.evaluate(js);
|
|
549
|
+
}
|
|
462
550
|
if (typeof result === 'string')
|
|
463
551
|
console.log(result);
|
|
464
552
|
else
|
|
465
553
|
console.log(JSON.stringify(result, null, 2));
|
|
466
554
|
}));
|
|
467
555
|
// ── Network (API discovery) ──
|
|
468
|
-
browser.command('network')
|
|
556
|
+
addBrowserTabOption(browser.command('network'))
|
|
469
557
|
.option('--detail <index>', 'Show full response body of request at index')
|
|
470
558
|
.option('--all', 'Show all requests including static resources')
|
|
471
559
|
.description('Show captured network requests (auto-captured since last open)')
|
|
@@ -1003,6 +1091,10 @@ cli({
|
|
|
1003
1091
|
});
|
|
1004
1092
|
// ── Built-in: daemon ──────────────────────────────────────────────────────
|
|
1005
1093
|
const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
|
|
1094
|
+
daemonCmd
|
|
1095
|
+
.command('status')
|
|
1096
|
+
.description('Show daemon status')
|
|
1097
|
+
.action(async () => { await daemonStatus(); });
|
|
1006
1098
|
daemonCmd
|
|
1007
1099
|
.command('stop')
|
|
1008
1100
|
.description('Stop the daemon')
|
|
@@ -1063,10 +1155,14 @@ cli({
|
|
|
1063
1155
|
.command('serve')
|
|
1064
1156
|
.description('Start Anthropic-compatible API proxy for Antigravity')
|
|
1065
1157
|
.option('--port <port>', 'Server port (default: 8082)', '8082')
|
|
1158
|
+
.option('--timeout <seconds>', 'Maximum time to wait for a reply (default: 120s)')
|
|
1066
1159
|
.action(async (opts) => {
|
|
1067
1160
|
// @ts-expect-error JS adapter — no type declarations
|
|
1068
1161
|
const { startServe } = await import('../clis/antigravity/serve.js');
|
|
1069
|
-
await startServe({
|
|
1162
|
+
await startServe({
|
|
1163
|
+
port: parseInt(opts.port, 10),
|
|
1164
|
+
timeout: opts.timeout ? parsePositiveIntOption(opts.timeout, '--timeout', 120) : undefined,
|
|
1165
|
+
});
|
|
1070
1166
|
});
|
|
1071
1167
|
// ── Dynamic adapter commands ──────────────────────────────────────────────
|
|
1072
1168
|
const siteGroups = new Map();
|
|
@@ -1124,14 +1220,3 @@ export function resolveBrowserVerifyInvocation(opts = {}) {
|
|
|
1124
1220
|
...(platform === 'win32' ? { shell: true } : {}),
|
|
1125
1221
|
};
|
|
1126
1222
|
}
|
|
1127
|
-
/** Infer a workspace-friendly hostname from a URL, with site override. */
|
|
1128
|
-
function inferHost(url, site) {
|
|
1129
|
-
if (site)
|
|
1130
|
-
return site;
|
|
1131
|
-
try {
|
|
1132
|
-
return new URL(url).host;
|
|
1133
|
-
}
|
|
1134
|
-
catch {
|
|
1135
|
-
return 'default';
|
|
1136
|
-
}
|
|
1137
|
-
}
|