@jackwener/opencli 1.7.7 → 1.7.9
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 +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +3 -22
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -31,9 +31,52 @@ import { buildExtractHtmlJs, runExtractFromHtml } from './browser/extract.js';
|
|
|
31
31
|
import { analyzeSite } from './browser/analyze.js';
|
|
32
32
|
import { daemonStatus, daemonStop } from './commands/daemon.js';
|
|
33
33
|
import { log } from './logger.js';
|
|
34
|
+
import { bindTab, BrowserCommandError, fetchDaemonStatus, sendCommand } from './browser/daemon-client.js';
|
|
35
|
+
import { aliasForContextId, loadProfileConfig, renameProfile, resolveProfileContextId, setDefaultProfile } from './browser/profile.js';
|
|
34
36
|
const CLI_FILE = fileURLToPath(import.meta.url);
|
|
35
37
|
const DEFAULT_BROWSER_WORKSPACE = 'browser:default';
|
|
38
|
+
const DEFAULT_BOUND_WORKSPACE = 'bound:default';
|
|
36
39
|
const BROWSER_TAB_OPTION_DESCRIPTION = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
|
|
40
|
+
const FOLLOW_POLL_MS = 1_000;
|
|
41
|
+
function parseDurationMs(raw, flagName) {
|
|
42
|
+
if (raw === undefined || raw === null || raw === '')
|
|
43
|
+
return null;
|
|
44
|
+
const str = String(raw).trim();
|
|
45
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(str);
|
|
46
|
+
if (!match)
|
|
47
|
+
return { error: `--${flagName} must be a duration like 500ms, 30s, 2m, got "${str}"` };
|
|
48
|
+
const value = Number.parseFloat(match[1]);
|
|
49
|
+
const unit = match[2] ?? 'ms';
|
|
50
|
+
const multiplier = unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : unit === 's' ? 1_000 : 1;
|
|
51
|
+
return Math.round(value * multiplier);
|
|
52
|
+
}
|
|
53
|
+
function timestampFromRaw(value) {
|
|
54
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : Date.now();
|
|
55
|
+
}
|
|
56
|
+
function toIsoTimestamp(timestamp) {
|
|
57
|
+
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp <= 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
return new Date(timestamp).toISOString();
|
|
60
|
+
}
|
|
61
|
+
function filterByTimeWindow(items, opts, now = Date.now()) {
|
|
62
|
+
const sinceTs = opts.sinceMs != null ? now - opts.sinceMs : undefined;
|
|
63
|
+
const untilTs = opts.untilMs != null ? now - opts.untilMs : undefined;
|
|
64
|
+
return items.filter((item) => {
|
|
65
|
+
const ts = item.timestamp ?? now;
|
|
66
|
+
if (sinceTs !== undefined && ts < sinceTs)
|
|
67
|
+
return false;
|
|
68
|
+
if (untilTs !== undefined && ts > untilTs)
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function selectFreshByTimestamp(items, lastSeenTs) {
|
|
74
|
+
const fresh = items.filter((item) => Number(item.timestamp ?? 0) > lastSeenTs);
|
|
75
|
+
const nextSeenTs = fresh.length > 0
|
|
76
|
+
? Math.max(lastSeenTs, ...fresh.map((item) => Number(item.timestamp ?? 0)).filter(Number.isFinite))
|
|
77
|
+
: lastSeenTs;
|
|
78
|
+
return { fresh, lastSeenTs: nextSeenTs };
|
|
79
|
+
}
|
|
37
80
|
/**
|
|
38
81
|
* Normalize raw capture entries (from daemon/CDP `readNetworkCapture` or
|
|
39
82
|
* the JS interceptor's `window.__opencli_net`) into a consistent shape.
|
|
@@ -69,13 +112,15 @@ async function captureNetworkItems(page) {
|
|
|
69
112
|
body,
|
|
70
113
|
bodyFullSize: fullSize,
|
|
71
114
|
bodyTruncated: truncated,
|
|
115
|
+
timestamp: timestampFromRaw(e.timestamp),
|
|
72
116
|
};
|
|
73
117
|
});
|
|
74
118
|
}
|
|
75
119
|
}
|
|
76
120
|
const raw = await page.evaluate(`(function(){ var out = window.__opencli_net || []; window.__opencli_net = []; return JSON.stringify(out); })()`);
|
|
77
121
|
try {
|
|
78
|
-
|
|
122
|
+
const parsed = JSON.parse(raw);
|
|
123
|
+
return parsed.map((item) => ({ ...item, timestamp: timestampFromRaw(item.timestamp) }));
|
|
79
124
|
}
|
|
80
125
|
catch {
|
|
81
126
|
if (process.env.OPENCLI_VERBOSE)
|
|
@@ -85,9 +130,12 @@ async function captureNetworkItems(page) {
|
|
|
85
130
|
}
|
|
86
131
|
/** Drop static-resource / telemetry noise so agents see only API-shaped traffic. */
|
|
87
132
|
function filterNetworkItems(items) {
|
|
88
|
-
return items.filter((r) =>
|
|
89
|
-
|
|
90
|
-
|
|
133
|
+
return items.filter((r) => {
|
|
134
|
+
const ct = r.ct?.toLowerCase() ?? '';
|
|
135
|
+
return ((ct.includes('json') || ct.includes('xml') || ct.includes('text/plain') || ct.includes('javascript')) &&
|
|
136
|
+
!/\.(js|css|png|jpg|gif|svg|woff|ico|map)(\?|$)/i.test(r.url) &&
|
|
137
|
+
!/analytics|tracking|telemetry|beacon|pixel|gtag|fbevents/i.test(r.url));
|
|
138
|
+
});
|
|
91
139
|
}
|
|
92
140
|
/** Exit codes by network error code — usage errors vs runtime failures. */
|
|
93
141
|
const NETWORK_ERROR_EXIT = {
|
|
@@ -253,6 +301,9 @@ async function resolveBrowserTargetInSession(page, targetPage, opts) {
|
|
|
253
301
|
throw new Error(`Target tab ${candidate} is not part of the current browser session. ` +
|
|
254
302
|
'The Browser Bridge workspace may have restarted; re-run "opencli browser tab list" and choose a current target.');
|
|
255
303
|
}
|
|
304
|
+
function getBrowserScope(workspace, contextId) {
|
|
305
|
+
return contextId ? `${contextId}:${workspace}` : workspace;
|
|
306
|
+
}
|
|
256
307
|
async function resolveStoredBrowserTarget(page, scope = DEFAULT_BROWSER_WORKSPACE) {
|
|
257
308
|
const defaultPage = loadBrowserTargetState(scope)?.defaultPage?.trim();
|
|
258
309
|
if (!defaultPage)
|
|
@@ -260,19 +311,21 @@ async function resolveStoredBrowserTarget(page, scope = DEFAULT_BROWSER_WORKSPAC
|
|
|
260
311
|
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
261
312
|
}
|
|
262
313
|
/** Create a browser page for browser commands. Uses a dedicated browser workspace for session persistence. */
|
|
263
|
-
async function getBrowserPage(targetPage) {
|
|
314
|
+
async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE, contextId) {
|
|
264
315
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
265
316
|
const bridge = new BrowserBridge();
|
|
266
317
|
const envTimeout = process.env.OPENCLI_BROWSER_TIMEOUT;
|
|
267
318
|
const idleTimeout = envTimeout ? parseInt(envTimeout, 10) : undefined;
|
|
268
319
|
const page = await bridge.connect({
|
|
269
320
|
timeout: 30,
|
|
270
|
-
workspace
|
|
321
|
+
workspace,
|
|
322
|
+
...(contextId && { contextId }),
|
|
271
323
|
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
272
324
|
});
|
|
325
|
+
const targetScope = getBrowserScope(workspace, contextId);
|
|
273
326
|
const resolvedTargetPage = targetPage
|
|
274
|
-
? await resolveBrowserTargetInSession(page, targetPage, { scope:
|
|
275
|
-
: await resolveStoredBrowserTarget(page,
|
|
327
|
+
? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
|
|
328
|
+
: await resolveStoredBrowserTarget(page, targetScope);
|
|
276
329
|
if (resolvedTargetPage) {
|
|
277
330
|
if (!page.setActivePage) {
|
|
278
331
|
throw new Error('This browser session does not support explicit tab targeting');
|
|
@@ -290,11 +343,38 @@ function getBrowserTargetId(command) {
|
|
|
290
343
|
const opts = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
|
|
291
344
|
return typeof opts.tab === 'string' && opts.tab.trim() ? opts.tab.trim() : undefined;
|
|
292
345
|
}
|
|
346
|
+
function getCommandOption(command, option) {
|
|
347
|
+
let current = command;
|
|
348
|
+
while (current) {
|
|
349
|
+
const opts = current.opts();
|
|
350
|
+
if (Object.prototype.hasOwnProperty.call(opts, option) && opts[option] !== undefined)
|
|
351
|
+
return opts[option];
|
|
352
|
+
current = current.parent;
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
function getBrowserWorkspace(command) {
|
|
357
|
+
const raw = getCommandOption(command, 'workspace');
|
|
358
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : DEFAULT_BROWSER_WORKSPACE;
|
|
359
|
+
}
|
|
360
|
+
function getBrowserContextId(command) {
|
|
361
|
+
const raw = getCommandOption(command, 'profile');
|
|
362
|
+
return resolveProfileContextId(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
|
|
363
|
+
}
|
|
364
|
+
function getPageWorkspace(page) {
|
|
365
|
+
const workspace = page.workspace;
|
|
366
|
+
return typeof workspace === 'string' && workspace.trim() ? workspace.trim() : DEFAULT_BROWSER_WORKSPACE;
|
|
367
|
+
}
|
|
368
|
+
function getPageScope(page) {
|
|
369
|
+
const contextId = page.contextId;
|
|
370
|
+
return getBrowserScope(getPageWorkspace(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
|
|
371
|
+
}
|
|
293
372
|
function resolveBrowserTabTarget(targetId, opts) {
|
|
294
373
|
if (typeof targetId === 'string' && targetId.trim())
|
|
295
374
|
return targetId.trim();
|
|
296
|
-
|
|
297
|
-
|
|
375
|
+
const tab = opts instanceof Command ? opts.opts().tab : opts?.tab;
|
|
376
|
+
if (typeof tab === 'string' && tab.trim())
|
|
377
|
+
return tab.trim();
|
|
298
378
|
return undefined;
|
|
299
379
|
}
|
|
300
380
|
function parsePositiveIntOption(val, label, fallback) {
|
|
@@ -319,17 +399,17 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
319
399
|
.name('opencli')
|
|
320
400
|
.description('Make any website your CLI. Zero setup. AI-powered.')
|
|
321
401
|
.version(PKG_VERSION)
|
|
402
|
+
.option('--profile <name>', 'Chrome profile/context alias for Browser Bridge commands')
|
|
322
403
|
.enablePositionalOptions();
|
|
323
404
|
// ── Built-in: list ────────────────────────────────────────────────────────
|
|
324
405
|
program
|
|
325
406
|
.command('list')
|
|
326
407
|
.description('List all available CLI commands')
|
|
327
408
|
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
328
|
-
.option('--json', 'JSON output (deprecated)')
|
|
329
409
|
.action((opts) => {
|
|
330
410
|
const registry = getRegistry();
|
|
331
411
|
const commands = [...new Set(registry.values())].sort((a, b) => fullName(a).localeCompare(fullName(b)));
|
|
332
|
-
const fmt = opts.
|
|
412
|
+
const fmt = opts.format;
|
|
333
413
|
const isStructured = fmt === 'json' || fmt === 'yaml';
|
|
334
414
|
if (fmt !== 'table') {
|
|
335
415
|
const rows = isStructured
|
|
@@ -414,6 +494,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
414
494
|
// All commands wrapped in browserAction() for consistent error handling.
|
|
415
495
|
const browser = program
|
|
416
496
|
.command('browser')
|
|
497
|
+
.option('--workspace <name>', 'Browser workspace to use (default: browser:default; bound tabs use bound:<name>)')
|
|
417
498
|
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)');
|
|
418
499
|
/**
|
|
419
500
|
* Resolve a `<target>` (numeric ref or CSS selector) via the unified resolver.
|
|
@@ -466,7 +547,9 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
466
547
|
try {
|
|
467
548
|
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
468
549
|
const targetPage = getBrowserTargetId(command);
|
|
469
|
-
const
|
|
550
|
+
const workspace = getBrowserWorkspace(command);
|
|
551
|
+
const contextId = getBrowserContextId(command);
|
|
552
|
+
const page = await getBrowserPage(targetPage, workspace, contextId);
|
|
470
553
|
await fn(page, ...args);
|
|
471
554
|
}
|
|
472
555
|
catch (err) {
|
|
@@ -475,6 +558,20 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
475
558
|
if (err.hint)
|
|
476
559
|
log.error(`Hint: ${err.hint}`);
|
|
477
560
|
}
|
|
561
|
+
else if (err instanceof BrowserCommandError) {
|
|
562
|
+
if (err.code) {
|
|
563
|
+
console.log(JSON.stringify({
|
|
564
|
+
error: {
|
|
565
|
+
code: err.code,
|
|
566
|
+
message: err.message,
|
|
567
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
568
|
+
},
|
|
569
|
+
}, null, 2));
|
|
570
|
+
}
|
|
571
|
+
log.error(err.message);
|
|
572
|
+
if (err.hint)
|
|
573
|
+
log.error(`Hint: ${err.hint}`);
|
|
574
|
+
}
|
|
478
575
|
else if (err instanceof TargetError) {
|
|
479
576
|
// Agent-facing structured envelope on stdout + short human line on stderr.
|
|
480
577
|
emitTargetError(err);
|
|
@@ -495,6 +592,105 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
495
592
|
}
|
|
496
593
|
};
|
|
497
594
|
}
|
|
595
|
+
browser.command('bind')
|
|
596
|
+
.option('--domain <host>', 'Only bind a current/visible tab whose hostname matches this domain')
|
|
597
|
+
.option('--path-prefix <path>', 'Only bind a current/visible tab whose pathname starts with this prefix')
|
|
598
|
+
.option('--workspace <name>', 'Bound workspace name (must start with bound:)')
|
|
599
|
+
.description('Bind a bound:* workspace to the current Chrome tab/window')
|
|
600
|
+
.action(async (optsOrCommand, maybeCommand) => {
|
|
601
|
+
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
602
|
+
const opts = command?.opts() ?? optsOrCommand ?? {};
|
|
603
|
+
const rawWorkspace = getCommandOption(command, 'workspace');
|
|
604
|
+
const workspace = typeof rawWorkspace === 'string' && rawWorkspace.trim()
|
|
605
|
+
? rawWorkspace.trim()
|
|
606
|
+
: DEFAULT_BOUND_WORKSPACE;
|
|
607
|
+
if (!workspace.startsWith('bound:')) {
|
|
608
|
+
console.log(JSON.stringify({
|
|
609
|
+
error: {
|
|
610
|
+
code: 'invalid_bind_workspace',
|
|
611
|
+
message: `--workspace must start with "bound:", got "${workspace}"`,
|
|
612
|
+
hint: 'Use the default bound:default or pass --workspace bound:<name>.',
|
|
613
|
+
},
|
|
614
|
+
}, null, 2));
|
|
615
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
const { BrowserBridge } = await import('./browser/index.js');
|
|
620
|
+
const bridge = new BrowserBridge();
|
|
621
|
+
const contextId = getBrowserContextId(command);
|
|
622
|
+
await bridge.connect({ timeout: 30, workspace, ...(contextId && { contextId }) });
|
|
623
|
+
const data = await bindTab(workspace, {
|
|
624
|
+
...(contextId && { contextId }),
|
|
625
|
+
...(typeof opts.domain === 'string' && opts.domain.trim() ? { matchDomain: opts.domain.trim() } : {}),
|
|
626
|
+
...(typeof opts.pathPrefix === 'string' && opts.pathPrefix.trim() ? { matchPathPrefix: opts.pathPrefix.trim() } : {}),
|
|
627
|
+
});
|
|
628
|
+
saveBrowserTargetState(undefined, getBrowserScope(workspace, contextId));
|
|
629
|
+
console.log(JSON.stringify({ workspace, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
if (err instanceof BrowserCommandError && err.code) {
|
|
633
|
+
console.log(JSON.stringify({
|
|
634
|
+
error: {
|
|
635
|
+
code: err.code,
|
|
636
|
+
message: err.message,
|
|
637
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
638
|
+
},
|
|
639
|
+
}, null, 2));
|
|
640
|
+
}
|
|
641
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
642
|
+
if (err instanceof BrowserCommandError && err.hint)
|
|
643
|
+
log.error(`Hint: ${err.hint}`);
|
|
644
|
+
process.exitCode = err instanceof BrowserCommandError && err.code === 'invalid_bind_workspace'
|
|
645
|
+
? EXIT_CODES.USAGE_ERROR
|
|
646
|
+
: EXIT_CODES.GENERIC_ERROR;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
browser.command('unbind')
|
|
650
|
+
.option('--workspace <name>', 'Bound workspace name to detach')
|
|
651
|
+
.description('Detach a bound:* workspace without closing the user tab/window')
|
|
652
|
+
.action(async (optsOrCommand, maybeCommand) => {
|
|
653
|
+
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
654
|
+
const rawWorkspace = getCommandOption(command, 'workspace');
|
|
655
|
+
const workspace = typeof rawWorkspace === 'string' && rawWorkspace.trim()
|
|
656
|
+
? rawWorkspace.trim()
|
|
657
|
+
: DEFAULT_BOUND_WORKSPACE;
|
|
658
|
+
if (!workspace.startsWith('bound:')) {
|
|
659
|
+
console.log(JSON.stringify({
|
|
660
|
+
error: {
|
|
661
|
+
code: 'invalid_bind_workspace',
|
|
662
|
+
message: `--workspace must start with "bound:", got "${workspace}"`,
|
|
663
|
+
hint: 'Use the default bound:default or pass --workspace bound:<name>.',
|
|
664
|
+
},
|
|
665
|
+
}, null, 2));
|
|
666
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const { BrowserBridge } = await import('./browser/index.js');
|
|
671
|
+
const bridge = new BrowserBridge();
|
|
672
|
+
const contextId = getBrowserContextId(command);
|
|
673
|
+
await bridge.connect({ timeout: 30, workspace, ...(contextId && { contextId }) });
|
|
674
|
+
await sendCommand('close-window', { workspace, ...(contextId && { contextId }) });
|
|
675
|
+
saveBrowserTargetState(undefined, getBrowserScope(workspace, contextId));
|
|
676
|
+
console.log(JSON.stringify({ unbound: true, workspace }, null, 2));
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
if (err instanceof BrowserCommandError && err.code) {
|
|
680
|
+
console.log(JSON.stringify({
|
|
681
|
+
error: {
|
|
682
|
+
code: err.code,
|
|
683
|
+
message: err.message,
|
|
684
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
685
|
+
},
|
|
686
|
+
}, null, 2));
|
|
687
|
+
}
|
|
688
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
689
|
+
if (err instanceof BrowserCommandError && err.hint)
|
|
690
|
+
log.error(`Hint: ${err.hint}`);
|
|
691
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
692
|
+
}
|
|
693
|
+
});
|
|
498
694
|
const browserTab = browser
|
|
499
695
|
.command('tab')
|
|
500
696
|
.description('Tab management — list, create, and close tabs in the automation window');
|
|
@@ -526,7 +722,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
526
722
|
throw new Error('Target tab required. Pass it as an argument or --tab <targetId>.');
|
|
527
723
|
}
|
|
528
724
|
await page.selectTab(resolvedTarget);
|
|
529
|
-
saveBrowserTargetState(resolvedTarget,
|
|
725
|
+
saveBrowserTargetState(resolvedTarget, getPageScope(page));
|
|
530
726
|
console.log(JSON.stringify({ selected: resolvedTarget }, null, 2));
|
|
531
727
|
}));
|
|
532
728
|
addBrowserTabOption(browserTab.command('close')
|
|
@@ -541,15 +737,16 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
541
737
|
throw new Error('Target tab required. Pass it as an argument or --tab <targetId>.');
|
|
542
738
|
}
|
|
543
739
|
const validatedTarget = await resolveBrowserTargetInSession(page, resolvedTarget, {
|
|
544
|
-
scope:
|
|
740
|
+
scope: getPageScope(page),
|
|
545
741
|
source: 'explicit',
|
|
546
742
|
});
|
|
547
743
|
if (!validatedTarget) {
|
|
548
744
|
throw new Error(`Target tab ${resolvedTarget} is not part of the current browser session.`);
|
|
549
745
|
}
|
|
550
746
|
await page.closeTab(validatedTarget);
|
|
551
|
-
|
|
552
|
-
|
|
747
|
+
const scope = getPageScope(page);
|
|
748
|
+
if (loadBrowserTargetState(scope)?.defaultPage === validatedTarget) {
|
|
749
|
+
saveBrowserTargetState(undefined, scope);
|
|
553
750
|
}
|
|
554
751
|
console.log(JSON.stringify({ closed: validatedTarget }, null, 2));
|
|
555
752
|
}));
|
|
@@ -564,12 +761,17 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
564
761
|
* silently dropping the body. Per-entry cap is 1 MiB and the ring is
|
|
565
762
|
* capped at 200 entries, bounding worst-case in-page memory.
|
|
566
763
|
*/
|
|
567
|
-
const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}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();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}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')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()`;
|
|
568
|
-
addBrowserTabOption(browser.command('open').argument('<url>').description('Open URL in automation window'))
|
|
569
|
-
.action(browserAction(async (page, url) => {
|
|
764
|
+
const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body,timestamp:Date.now()};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}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();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}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')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()`;
|
|
765
|
+
addBrowserTabOption(browser.command('open').argument('<url>').option('--allow-navigate-bound', 'Allow navigating a bound user tab', false).description('Open URL in automation window'))
|
|
766
|
+
.action(browserAction(async (page, url, opts) => {
|
|
570
767
|
// Start session-level capture before navigation (catches initial requests)
|
|
571
768
|
const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
|
|
572
|
-
|
|
769
|
+
if (opts.allowNavigateBound === true) {
|
|
770
|
+
await page.goto(url, { allowBoundNavigation: true });
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
await page.goto(url);
|
|
774
|
+
}
|
|
573
775
|
await page.wait(2);
|
|
574
776
|
// Fallback: inject JS interceptor when session capture is unavailable
|
|
575
777
|
if (!hasSessionCapture) {
|
|
@@ -583,8 +785,19 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
583
785
|
...(page.getActivePage?.() ? { page: page.getActivePage?.() } : {}),
|
|
584
786
|
}, null, 2));
|
|
585
787
|
}));
|
|
586
|
-
addBrowserTabOption(browser.command('back').description('Go back in browser history'))
|
|
587
|
-
.action(browserAction(async (page) => {
|
|
788
|
+
addBrowserTabOption(browser.command('back').option('--allow-navigate-bound', 'Allow history navigation in a bound user tab', false).description('Go back in browser history'))
|
|
789
|
+
.action(browserAction(async (page, opts) => {
|
|
790
|
+
if (getPageWorkspace(page).startsWith('bound:') && opts.allowNavigateBound !== true) {
|
|
791
|
+
console.log(JSON.stringify({
|
|
792
|
+
error: {
|
|
793
|
+
code: 'bound_navigation_blocked',
|
|
794
|
+
message: `Workspace "${getPageWorkspace(page)}" is bound to a user tab; history navigation is blocked by default.`,
|
|
795
|
+
hint: 'Pass --allow-navigate-bound only if you intentionally want to navigate the bound tab.',
|
|
796
|
+
},
|
|
797
|
+
}, null, 2));
|
|
798
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
588
801
|
await page.evaluate('history.back()');
|
|
589
802
|
await page.wait(2);
|
|
590
803
|
console.log('Navigated back');
|
|
@@ -624,6 +837,69 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
624
837
|
console.log(await page.screenshot({ format: 'png' }));
|
|
625
838
|
}
|
|
626
839
|
}));
|
|
840
|
+
addBrowserTabOption(browser.command('console'))
|
|
841
|
+
.option('--level <level>', 'Console level: all, error, warning, log, info, debug', 'all')
|
|
842
|
+
.option('--since <duration>', 'Only include messages from the last duration (for example: 30s, 2m)')
|
|
843
|
+
.option('--until <duration>', 'Only include messages older than the duration from now')
|
|
844
|
+
.option('--follow', 'Continuously print new console messages as JSON lines', false)
|
|
845
|
+
.description('Read recent browser console messages')
|
|
846
|
+
.action(browserAction(async (page, opts) => {
|
|
847
|
+
const sinceMs = parseDurationMs(opts.since, 'since');
|
|
848
|
+
const untilMs = parseDurationMs(opts.until, 'until');
|
|
849
|
+
if (sinceMs && typeof sinceMs === 'object') {
|
|
850
|
+
console.log(JSON.stringify({ error: { code: 'invalid_since', message: sinceMs.error } }, null, 2));
|
|
851
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (untilMs && typeof untilMs === 'object') {
|
|
855
|
+
console.log(JSON.stringify({ error: { code: 'invalid_until', message: untilMs.error } }, null, 2));
|
|
856
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const normalize = (messages) => messages.map((message) => {
|
|
860
|
+
if (message && typeof message === 'object') {
|
|
861
|
+
const record = message;
|
|
862
|
+
return {
|
|
863
|
+
...record,
|
|
864
|
+
timestamp: timestampFromRaw(record.timestamp),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
return { type: 'log', text: String(message), timestamp: Date.now() };
|
|
868
|
+
});
|
|
869
|
+
const filter = (messages) => filterByTimeWindow(messages, { sinceMs, untilMs }).filter((message) => {
|
|
870
|
+
if (opts.level === 'all')
|
|
871
|
+
return true;
|
|
872
|
+
const type = String(message.type ?? message.level ?? '').toLowerCase();
|
|
873
|
+
return opts.level === 'error'
|
|
874
|
+
? type === 'error' || type === 'warning'
|
|
875
|
+
: type === String(opts.level).toLowerCase();
|
|
876
|
+
});
|
|
877
|
+
if (opts.follow) {
|
|
878
|
+
let lastSeenTs = 0;
|
|
879
|
+
while (true) {
|
|
880
|
+
const messages = filter(normalize(await page.consoleMessages('all')));
|
|
881
|
+
const next = selectFreshByTimestamp(messages, lastSeenTs);
|
|
882
|
+
for (const message of next.fresh) {
|
|
883
|
+
console.log(JSON.stringify({
|
|
884
|
+
...message,
|
|
885
|
+
timestamp: toIsoTimestamp(message.timestamp),
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
lastSeenTs = next.lastSeenTs;
|
|
889
|
+
await new Promise((resolve) => setTimeout(resolve, FOLLOW_POLL_MS));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const messages = filter(normalize(await page.consoleMessages(opts.level)));
|
|
893
|
+
console.log(JSON.stringify({
|
|
894
|
+
workspace: getPageWorkspace(page),
|
|
895
|
+
captured_at: new Date().toISOString(),
|
|
896
|
+
count: messages.length,
|
|
897
|
+
messages: messages.map((message) => ({
|
|
898
|
+
...message,
|
|
899
|
+
timestamp: toIsoTimestamp(message.timestamp),
|
|
900
|
+
})),
|
|
901
|
+
}, null, 2));
|
|
902
|
+
}));
|
|
627
903
|
// ── Analyze (site recon, agent-native) ──
|
|
628
904
|
//
|
|
629
905
|
// Mechanizes the `site-recon.md` decision tree into one CLI call. The agent
|
|
@@ -1196,14 +1472,28 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1196
1472
|
.option('--all', 'Include static resources (js/css/images/telemetry)')
|
|
1197
1473
|
.option('--raw', 'Emit full bodies for every entry (skip shape preview)')
|
|
1198
1474
|
.option('--filter <fields>', 'Comma-separated field names; keep only entries whose body shape has ALL names as path segments')
|
|
1475
|
+
.option('--since <duration>', 'Only include entries from the last duration (for example: 30s, 2m)')
|
|
1476
|
+
.option('--until <duration>', 'Only include entries older than the duration from now')
|
|
1477
|
+
.option('--follow', 'Continuously print new matching entries as JSON lines', false)
|
|
1478
|
+
.option('--failed', 'Only include failed HTTP requests (status 0 or >= 400)', false)
|
|
1199
1479
|
.option('--max-body <chars>', 'With --detail: cap the emitted body at N chars (0 = unlimited, default)', '0')
|
|
1200
1480
|
.option('--ttl <ms>', 'Cache TTL in ms for --detail lookups', String(DEFAULT_TTL_MS))
|
|
1201
1481
|
.description('Capture network requests as shape previews; retrieve full bodies by key')
|
|
1202
1482
|
.action(browserAction(async (page, opts) => {
|
|
1203
1483
|
const ttlMs = parsePositiveIntOption(opts.ttl, 'ttl', DEFAULT_TTL_MS);
|
|
1204
|
-
const workspace =
|
|
1484
|
+
const workspace = getPageWorkspace(page);
|
|
1205
1485
|
const hasDetail = typeof opts.detail === 'string' && opts.detail.length > 0;
|
|
1206
1486
|
const hasFilter = typeof opts.filter === 'string';
|
|
1487
|
+
const sinceMs = parseDurationMs(opts.since, 'since');
|
|
1488
|
+
const untilMs = parseDurationMs(opts.until, 'until');
|
|
1489
|
+
if (sinceMs && typeof sinceMs === 'object') {
|
|
1490
|
+
emitNetworkError('invalid_since', sinceMs.error);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (untilMs && typeof untilMs === 'object') {
|
|
1494
|
+
emitNetworkError('invalid_until', untilMs.error);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1207
1497
|
// --detail and --filter do different things (one request by key vs. narrow
|
|
1208
1498
|
// the list by shape), don't compose, and combining them has no sensible
|
|
1209
1499
|
// semantic. Reject up front with a structured error instead of silently
|
|
@@ -1221,6 +1511,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1221
1511
|
}
|
|
1222
1512
|
filterFields = parsed.fields;
|
|
1223
1513
|
}
|
|
1514
|
+
if (hasDetail && opts.follow) {
|
|
1515
|
+
emitNetworkError('invalid_args', '--follow cannot be used with --detail.');
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1224
1518
|
// --detail short-circuits: read from cache only, no live capture needed.
|
|
1225
1519
|
if (hasDetail) {
|
|
1226
1520
|
const res = loadNetworkCache(workspace, { ttlMs });
|
|
@@ -1267,6 +1561,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1267
1561
|
status: entry.status,
|
|
1268
1562
|
ct: entry.ct,
|
|
1269
1563
|
size: entry.size,
|
|
1564
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: toIsoTimestamp(entry.timestamp) } : {}),
|
|
1270
1565
|
shape: inferShape(entry.body),
|
|
1271
1566
|
body: outputBody,
|
|
1272
1567
|
};
|
|
@@ -1280,6 +1575,38 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1280
1575
|
console.log(JSON.stringify(detailEnvelope, null, 2));
|
|
1281
1576
|
return;
|
|
1282
1577
|
}
|
|
1578
|
+
if (opts.follow) {
|
|
1579
|
+
if (!await page.startNetworkCapture?.()) {
|
|
1580
|
+
try {
|
|
1581
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
1582
|
+
}
|
|
1583
|
+
catch { /* non-fatal */ }
|
|
1584
|
+
}
|
|
1585
|
+
while (true) {
|
|
1586
|
+
const rawItems = await captureNetworkItems(page).catch((err) => {
|
|
1587
|
+
emitNetworkError('capture_failed', `Could not read network capture: ${err.message}`);
|
|
1588
|
+
return [];
|
|
1589
|
+
});
|
|
1590
|
+
let items = opts.all ? rawItems : filterNetworkItems(rawItems);
|
|
1591
|
+
items = filterByTimeWindow(items, { sinceMs, untilMs });
|
|
1592
|
+
if (opts.failed)
|
|
1593
|
+
items = items.filter((item) => item.status === 0 || item.status >= 400);
|
|
1594
|
+
const keyed = assignKeys(items);
|
|
1595
|
+
for (const item of keyed) {
|
|
1596
|
+
console.log(JSON.stringify({
|
|
1597
|
+
key: item.key,
|
|
1598
|
+
timestamp: toIsoTimestamp(item.timestamp),
|
|
1599
|
+
method: item.method,
|
|
1600
|
+
status: item.status,
|
|
1601
|
+
url: item.url,
|
|
1602
|
+
ct: item.ct,
|
|
1603
|
+
size: item.size,
|
|
1604
|
+
...(item.bodyTruncated ? { body_truncated: true } : {}),
|
|
1605
|
+
}));
|
|
1606
|
+
}
|
|
1607
|
+
await new Promise((resolve) => setTimeout(resolve, FOLLOW_POLL_MS));
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1283
1610
|
// Fresh capture path.
|
|
1284
1611
|
let rawItems;
|
|
1285
1612
|
try {
|
|
@@ -1289,7 +1616,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1289
1616
|
emitNetworkError('capture_failed', `Could not read network capture: ${err.message}`);
|
|
1290
1617
|
return;
|
|
1291
1618
|
}
|
|
1292
|
-
|
|
1619
|
+
let items = opts.all ? rawItems : filterNetworkItems(rawItems);
|
|
1620
|
+
items = filterByTimeWindow(items, { sinceMs, untilMs });
|
|
1621
|
+
if (opts.failed)
|
|
1622
|
+
items = items.filter((item) => item.status === 0 || item.status >= 400);
|
|
1293
1623
|
const filteredOut = rawItems.length - items.length;
|
|
1294
1624
|
const keyed = assignKeys(items);
|
|
1295
1625
|
const cacheEntries = keyed.map((it) => ({
|
|
@@ -1300,6 +1630,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1300
1630
|
size: it.size,
|
|
1301
1631
|
ct: it.ct,
|
|
1302
1632
|
body: it.body,
|
|
1633
|
+
...(typeof it.timestamp === 'number' ? { timestamp: it.timestamp } : {}),
|
|
1303
1634
|
...(it.bodyTruncated ? { body_truncated: true } : {}),
|
|
1304
1635
|
...(it.bodyTruncated && typeof it.bodyFullSize === 'number'
|
|
1305
1636
|
? { body_full_size: it.bodyFullSize }
|
|
@@ -1342,12 +1673,16 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1342
1673
|
envelope.body_truncated_hint = 'Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix.';
|
|
1343
1674
|
}
|
|
1344
1675
|
if (opts.raw) {
|
|
1345
|
-
envelope.entries = visible.map((s) =>
|
|
1676
|
+
envelope.entries = visible.map((s) => ({
|
|
1677
|
+
...s.entry,
|
|
1678
|
+
...(typeof s.entry.timestamp === 'number' ? { timestamp: toIsoTimestamp(s.entry.timestamp) } : {}),
|
|
1679
|
+
}));
|
|
1346
1680
|
}
|
|
1347
1681
|
else {
|
|
1348
1682
|
envelope.entries = visible.map((s) => ({
|
|
1349
1683
|
key: s.entry.key,
|
|
1350
1684
|
method: s.entry.method,
|
|
1685
|
+
...(typeof s.entry.timestamp === 'number' ? { timestamp: toIsoTimestamp(s.entry.timestamp) } : {}),
|
|
1351
1686
|
status: s.entry.status,
|
|
1352
1687
|
url: s.entry.url,
|
|
1353
1688
|
ct: s.entry.ct,
|
|
@@ -1412,10 +1747,10 @@ cli({
|
|
|
1412
1747
|
{ name: 'limit', type: 'int', default: 10, help: 'Number of items' },
|
|
1413
1748
|
],
|
|
1414
1749
|
columns: [], // TODO: field names for table output (e.g. ['title', 'score', 'url'])
|
|
1415
|
-
func: async (
|
|
1750
|
+
func: async (kwargs) => {
|
|
1416
1751
|
// TODO: implement data fetching
|
|
1417
1752
|
// Prefer API calls (fetch) over browser automation
|
|
1418
|
-
//
|
|
1753
|
+
// If you set browser: true, change this to: async (page, kwargs) => { ... }
|
|
1419
1754
|
return [];
|
|
1420
1755
|
},
|
|
1421
1756
|
});
|
|
@@ -1884,6 +2219,78 @@ cli({
|
|
|
1884
2219
|
? `✅ Reset "${site}". Now using official baseline.`
|
|
1885
2220
|
: `✅ Removed custom site "${site}".`));
|
|
1886
2221
|
});
|
|
2222
|
+
// ── Built-in: browser profile selection ──────────────────────────────────
|
|
2223
|
+
const profileCmd = program.command('profile').description('Manage Browser Bridge Chrome profiles');
|
|
2224
|
+
profileCmd
|
|
2225
|
+
.command('list')
|
|
2226
|
+
.description('List Chrome profiles connected through the Browser Bridge extension')
|
|
2227
|
+
.action(async () => {
|
|
2228
|
+
const status = await fetchDaemonStatus();
|
|
2229
|
+
const config = loadProfileConfig();
|
|
2230
|
+
const profiles = status?.profiles ?? [];
|
|
2231
|
+
if (!status) {
|
|
2232
|
+
console.log(styleText('yellow', 'Daemon is not running. Run opencli doctor after opening Chrome.'));
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (profiles.length === 0) {
|
|
2236
|
+
console.log(styleText('yellow', 'No Browser Bridge profiles connected.'));
|
|
2237
|
+
console.log(styleText('dim', 'Open a Chrome profile with the OpenCLI extension installed, then run opencli profile list again.'));
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const knownContextIds = new Set(profiles.map((profile) => profile.contextId));
|
|
2241
|
+
console.log(styleText('bold', 'Connected Browser Bridge profiles'));
|
|
2242
|
+
console.log();
|
|
2243
|
+
for (const profile of profiles) {
|
|
2244
|
+
const alias = aliasForContextId(config, profile.contextId);
|
|
2245
|
+
const defaultMark = config.defaultContextId === profile.contextId ? styleText('green', ' default') : '';
|
|
2246
|
+
const aliasText = alias ? ` ${styleText('cyan', alias)}` : '';
|
|
2247
|
+
const version = profile.extensionVersion ? ` v${profile.extensionVersion}` : ' version unknown';
|
|
2248
|
+
console.log(` ${profile.contextId}${aliasText}${defaultMark} — connected${version}`);
|
|
2249
|
+
}
|
|
2250
|
+
const disconnectedAliases = Object.entries(config.aliases)
|
|
2251
|
+
.filter(([, contextId]) => !knownContextIds.has(contextId));
|
|
2252
|
+
if (disconnectedAliases.length > 0 || (config.defaultContextId && !knownContextIds.has(config.defaultContextId))) {
|
|
2253
|
+
console.log();
|
|
2254
|
+
console.log(styleText('dim', 'Disconnected saved profiles:'));
|
|
2255
|
+
const shown = new Set();
|
|
2256
|
+
for (const [alias, contextId] of disconnectedAliases) {
|
|
2257
|
+
shown.add(contextId);
|
|
2258
|
+
console.log(styleText('dim', ` ${contextId} ${alias} — not connected`));
|
|
2259
|
+
}
|
|
2260
|
+
if (config.defaultContextId && !shown.has(config.defaultContextId) && !knownContextIds.has(config.defaultContextId)) {
|
|
2261
|
+
console.log(styleText('dim', ` ${config.defaultContextId} — default, not connected`));
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
profileCmd
|
|
2266
|
+
.command('rename')
|
|
2267
|
+
.description('Assign a local alias to a connected Browser Bridge profile')
|
|
2268
|
+
.argument('<contextId>', 'Profile contextId from opencli profile list')
|
|
2269
|
+
.argument('<alias>', 'Local alias, e.g. work or personal')
|
|
2270
|
+
.action((contextId, alias) => {
|
|
2271
|
+
try {
|
|
2272
|
+
renameProfile(contextId, alias);
|
|
2273
|
+
console.log(`Profile ${contextId} is now aliased as ${styleText('cyan', alias)}.`);
|
|
2274
|
+
}
|
|
2275
|
+
catch (err) {
|
|
2276
|
+
console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
|
|
2277
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
profileCmd
|
|
2281
|
+
.command('use')
|
|
2282
|
+
.description('Set the default Browser Bridge profile for future commands')
|
|
2283
|
+
.argument('<profile>', 'Profile alias or contextId')
|
|
2284
|
+
.action((profile) => {
|
|
2285
|
+
try {
|
|
2286
|
+
const config = setDefaultProfile(profile);
|
|
2287
|
+
console.log(`Default Browser Bridge profile: ${styleText('cyan', config.defaultContextId ?? profile)}`);
|
|
2288
|
+
}
|
|
2289
|
+
catch (err) {
|
|
2290
|
+
console.error(styleText('red', `Error: ${getErrorMessage(err)}`));
|
|
2291
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
1887
2294
|
// ── Built-in: daemon ──────────────────────────────────────────────────────
|
|
1888
2295
|
const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
|
|
1889
2296
|
daemonCmd
|
|
@@ -1896,7 +2303,10 @@ cli({
|
|
|
1896
2303
|
.action(async () => { await daemonStop(); });
|
|
1897
2304
|
// ── External CLIs ─────────────────────────────────────────────────────────
|
|
1898
2305
|
const externalClis = loadExternalClis();
|
|
1899
|
-
program
|
|
2306
|
+
const externalCmd = program
|
|
2307
|
+
.command('external')
|
|
2308
|
+
.description('Manage external CLI passthrough commands');
|
|
2309
|
+
externalCmd
|
|
1900
2310
|
.command('install')
|
|
1901
2311
|
.description('Install an external CLI')
|
|
1902
2312
|
.argument('<name>', 'Name of the external CLI')
|
|
@@ -1909,7 +2319,7 @@ cli({
|
|
|
1909
2319
|
}
|
|
1910
2320
|
installExternalCli(ext);
|
|
1911
2321
|
});
|
|
1912
|
-
|
|
2322
|
+
externalCmd
|
|
1913
2323
|
.command('register')
|
|
1914
2324
|
.description('Register an external CLI')
|
|
1915
2325
|
.argument('<name>', 'Name of the CLI')
|
|
@@ -1919,6 +2329,26 @@ cli({
|
|
|
1919
2329
|
.action((name, opts) => {
|
|
1920
2330
|
registerExternalCli(name, { binary: opts.binary, install: opts.install, description: opts.desc });
|
|
1921
2331
|
});
|
|
2332
|
+
externalCmd
|
|
2333
|
+
.command('list')
|
|
2334
|
+
.description('List registered external CLIs')
|
|
2335
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
2336
|
+
.action((opts) => {
|
|
2337
|
+
const rows = loadExternalClis().map((ext) => ({
|
|
2338
|
+
name: ext.name,
|
|
2339
|
+
binary: ext.binary,
|
|
2340
|
+
installed: isBinaryInstalled(ext.binary),
|
|
2341
|
+
description: ext.description ?? '',
|
|
2342
|
+
homepage: ext.homepage ?? '',
|
|
2343
|
+
tags: ext.tags?.join(', ') ?? '',
|
|
2344
|
+
}));
|
|
2345
|
+
renderOutput(rows, {
|
|
2346
|
+
fmt: opts.format,
|
|
2347
|
+
columns: ['name', 'binary', 'installed', 'description', 'homepage', 'tags'],
|
|
2348
|
+
title: 'opencli/external/list',
|
|
2349
|
+
source: 'opencli external list',
|
|
2350
|
+
});
|
|
2351
|
+
});
|
|
1922
2352
|
function passthroughExternal(name, parsedArgs) {
|
|
1923
2353
|
const args = parsedArgs ?? (() => {
|
|
1924
2354
|
const idx = process.argv.indexOf(name);
|
|
@@ -1965,12 +2395,12 @@ cli({
|
|
|
1965
2395
|
registerAllCommands(program, siteGroups);
|
|
1966
2396
|
// ── Unknown command fallback ──────────────────────────────────────────────
|
|
1967
2397
|
// Security: do NOT auto-discover and register arbitrary system binaries.
|
|
1968
|
-
// Only explicitly registered external CLIs
|
|
2398
|
+
// Only explicitly registered external CLIs are allowed.
|
|
1969
2399
|
program.on('command:*', (operands) => {
|
|
1970
2400
|
const binary = operands[0];
|
|
1971
2401
|
console.error(styleText('red', `error: unknown command '${binary}'`));
|
|
1972
2402
|
if (isBinaryInstalled(binary)) {
|
|
1973
|
-
console.error(styleText('dim', ` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
|
|
2403
|
+
console.error(styleText('dim', ` Tip: '${binary}' exists on your PATH. Use 'opencli external register ${binary}' to add it as an external CLI.`));
|
|
1974
2404
|
}
|
|
1975
2405
|
program.outputHelp();
|
|
1976
2406
|
process.exitCode = EXIT_CODES.USAGE_ERROR;
|