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