@jackwener/opencli 1.7.16 → 1.7.18
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 +11 -9
- package/README.zh-CN.md +10 -8
- package/cli-manifest.json +377 -271
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/commands.test.js +2 -2
- package/clis/chatgpt/detail.js +1 -1
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/image.js +38 -4
- package/clis/chatgpt/image.test.js +68 -1
- package/clis/chatgpt/new.js +1 -1
- package/clis/chatgpt/read.js +1 -1
- package/clis/chatgpt/send.js +1 -1
- package/clis/chatgpt/status.js +1 -1
- package/clis/chatgpt/utils.js +208 -16
- package/clis/chatgpt/utils.test.js +131 -2
- package/clis/claude/ask.js +1 -1
- package/clis/claude/detail.js +1 -1
- package/clis/claude/history.js +1 -1
- package/clis/claude/new.js +1 -1
- package/clis/claude/read.js +1 -1
- package/clis/claude/send.js +1 -1
- package/clis/claude/status.js +1 -1
- package/clis/deepseek/ask.js +1 -1
- package/clis/deepseek/detail.js +1 -1
- package/clis/deepseek/history.js +1 -1
- package/clis/deepseek/new.js +1 -1
- package/clis/deepseek/read.js +1 -1
- package/clis/deepseek/send.js +1 -1
- package/clis/deepseek/status.js +1 -1
- package/clis/doubao/ask.js +1 -1
- package/clis/doubao/detail.js +1 -1
- package/clis/doubao/history.js +1 -1
- package/clis/doubao/meeting-summary.js +1 -1
- package/clis/doubao/meeting-transcript.js +1 -1
- package/clis/doubao/new.js +1 -1
- package/clis/doubao/read.js +1 -1
- package/clis/doubao/send.js +1 -1
- package/clis/doubao/status.js +1 -1
- package/clis/doubao/utils.js +17 -0
- package/clis/doubao/utils.test.js +61 -0
- package/clis/gemini/ask.js +1 -1
- package/clis/gemini/deep-research-result.js +1 -1
- package/clis/gemini/deep-research.js +1 -1
- package/clis/gemini/image.js +1 -1
- package/clis/gemini/new.js +1 -1
- package/clis/grok/ask.js +1 -1
- package/clis/grok/detail.js +1 -1
- package/clis/grok/history.js +1 -1
- package/clis/grok/image.js +1 -1
- package/clis/grok/new.js +1 -1
- package/clis/grok/read.js +1 -1
- package/clis/grok/send.js +1 -1
- package/clis/grok/status.js +1 -1
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +2 -2
- package/clis/notebooklm/open.test.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/qwen/ask.js +1 -1
- package/clis/qwen/detail.js +1 -1
- package/clis/qwen/history.js +1 -1
- package/clis/qwen/image.js +1 -1
- package/clis/qwen/new.js +1 -1
- package/clis/qwen/read.js +1 -1
- package/clis/qwen/send.js +1 -1
- package/clis/qwen/status.js +1 -1
- package/clis/reddit/comment.js +1 -1
- package/clis/reddit/frontpage.js +1 -1
- package/clis/reddit/popular.js +1 -1
- package/clis/reddit/read.js +1 -1
- package/clis/reddit/read.test.js +2 -2
- package/clis/reddit/reply.js +182 -0
- package/clis/reddit/reply.test.js +89 -0
- package/clis/reddit/save.js +1 -1
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +1 -1
- package/clis/reddit/subreddit.js +1 -1
- package/clis/reddit/subscribe.js +1 -1
- package/clis/reddit/upvote.js +1 -1
- package/clis/reddit/upvoted.js +1 -1
- package/clis/reddit/user-comments.js +1 -1
- package/clis/reddit/user-posts.js +1 -1
- package/clis/reddit/user.js +1 -1
- package/clis/rednote/comments.js +76 -0
- package/clis/rednote/download.js +59 -0
- package/clis/rednote/feed.js +95 -0
- package/clis/rednote/navigation.test.js +26 -0
- package/clis/rednote/note.js +68 -0
- package/clis/rednote/notifications.js +139 -0
- package/clis/rednote/rednote.test.js +157 -0
- package/clis/rednote/search.js +97 -0
- package/clis/rednote/user.js +55 -0
- package/clis/twitter/article.js +1 -1
- package/clis/twitter/bookmark-folder.js +1 -1
- package/clis/twitter/bookmark-folders.js +1 -1
- package/clis/twitter/bookmarks.js +1 -1
- package/clis/twitter/download.js +1 -1
- package/clis/twitter/followers.js +1 -1
- package/clis/twitter/following.js +1 -1
- package/clis/twitter/likes.js +1 -1
- package/clis/twitter/list-tweets.js +1 -1
- package/clis/twitter/lists.js +1 -1
- package/clis/twitter/notifications.js +1 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/search.js +1 -1
- package/clis/twitter/thread.js +1 -1
- package/clis/twitter/timeline.js +1 -1
- package/clis/twitter/trending.js +1 -1
- package/clis/twitter/tweets.js +1 -1
- package/clis/xiaohongshu/comments.js +34 -24
- package/clis/xiaohongshu/download.js +32 -23
- package/clis/xiaohongshu/feed.js +23 -15
- package/clis/xiaohongshu/note-helpers.js +16 -6
- package/clis/xiaohongshu/note.js +26 -20
- package/clis/xiaohongshu/notifications.js +26 -19
- package/clis/xiaohongshu/search.js +37 -28
- package/clis/xiaohongshu/user-helpers.js +13 -4
- package/clis/xiaohongshu/user-helpers.test.js +20 -0
- package/clis/xiaohongshu/user.js +9 -4
- package/clis/youtube/transcript.js +28 -3
- package/clis/youtube/transcript.test.js +90 -1
- package/clis/yuanbao/ask.js +1 -1
- package/clis/yuanbao/detail.js +1 -1
- package/clis/yuanbao/history.js +1 -1
- package/clis/yuanbao/new.js +1 -1
- package/clis/yuanbao/read.js +1 -1
- package/clis/yuanbao/send.js +1 -1
- package/clis/yuanbao/status.js +1 -1
- package/dist/src/browser/bridge.d.ts +3 -1
- package/dist/src/browser/bridge.js +3 -1
- package/dist/src/browser/cdp.d.ts +3 -1
- package/dist/src/browser/daemon-client.d.ts +7 -14
- package/dist/src/browser/daemon-client.js +2 -6
- package/dist/src/browser/network-cache.d.ts +5 -5
- package/dist/src/browser/network-cache.js +8 -8
- package/dist/src/browser/network-cache.test.js +4 -4
- package/dist/src/browser/page.d.ts +8 -7
- package/dist/src/browser/page.js +23 -16
- package/dist/src/browser/page.test.js +60 -30
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli.js +60 -162
- package/dist/src/cli.test.js +184 -198
- package/dist/src/commanderAdapter.js +2 -0
- package/dist/src/discovery.js +1 -1
- package/dist/src/doctor.d.ts +0 -4
- package/dist/src/doctor.js +14 -73
- package/dist/src/doctor.test.js +28 -97
- package/dist/src/execution.d.ts +1 -0
- package/dist/src/execution.js +20 -21
- package/dist/src/execution.test.js +27 -31
- package/dist/src/help.js +7 -1
- package/dist/src/main.js +0 -19
- package/dist/src/manifest-types.d.ts +2 -4
- package/dist/src/observation/artifact.js +1 -1
- package/dist/src/observation/artifact.test.js +3 -3
- package/dist/src/observation/events.d.ts +1 -1
- package/dist/src/observation/manager.js +1 -1
- package/dist/src/observation/manager.test.js +3 -3
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +3 -12
- package/dist/src/registry.js +6 -10
- package/dist/src/runtime.d.ts +7 -2
- package/dist/src/runtime.js +3 -1
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/types.d.ts +0 -15
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -36,8 +36,6 @@ import { bindTab, BrowserCommandError, fetchDaemonStatus, sendCommand } from './
|
|
|
36
36
|
import { aliasForContextId, loadProfileConfig, renameProfile, resolveProfileContextId, setDefaultProfile } from './browser/profile.js';
|
|
37
37
|
import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
|
|
38
38
|
const CLI_FILE = fileURLToPath(import.meta.url);
|
|
39
|
-
const DEFAULT_BROWSER_WORKSPACE = 'browser:default';
|
|
40
|
-
const DEFAULT_BOUND_WORKSPACE = 'bound:default';
|
|
41
39
|
const BROWSER_TAB_OPTION_DESCRIPTION = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
|
|
42
40
|
const FOLLOW_POLL_MS = 1_000;
|
|
43
41
|
function parseDurationMs(raw, flagName) {
|
|
@@ -245,11 +243,11 @@ export function renderVerifyPreview(rows, opts = {}) {
|
|
|
245
243
|
function getBrowserCacheDir() {
|
|
246
244
|
return process.env.OPENCLI_CACHE_DIR || path.join(os.homedir(), '.opencli', 'cache');
|
|
247
245
|
}
|
|
248
|
-
function getBrowserTargetStatePath(scope
|
|
249
|
-
const
|
|
250
|
-
return path.join(getBrowserCacheDir(), 'browser-state', `${
|
|
246
|
+
function getBrowserTargetStatePath(scope) {
|
|
247
|
+
const safeSession = scope.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
248
|
+
return path.join(getBrowserCacheDir(), 'browser-state', `${safeSession}.json`);
|
|
251
249
|
}
|
|
252
|
-
function loadBrowserTargetState(scope
|
|
250
|
+
function loadBrowserTargetState(scope) {
|
|
253
251
|
try {
|
|
254
252
|
const raw = fs.readFileSync(getBrowserTargetStatePath(scope), 'utf-8');
|
|
255
253
|
const parsed = JSON.parse(raw);
|
|
@@ -259,7 +257,7 @@ function loadBrowserTargetState(scope = DEFAULT_BROWSER_WORKSPACE) {
|
|
|
259
257
|
return null;
|
|
260
258
|
}
|
|
261
259
|
}
|
|
262
|
-
function saveBrowserTargetState(defaultPage, scope
|
|
260
|
+
function saveBrowserTargetState(defaultPage, scope) {
|
|
263
261
|
const target = getBrowserTargetStatePath(scope);
|
|
264
262
|
if (!defaultPage) {
|
|
265
263
|
fs.rmSync(target, { force: true });
|
|
@@ -291,7 +289,7 @@ async function resolveBrowserTargetInSession(page, targetPage, opts) {
|
|
|
291
289
|
return undefined;
|
|
292
290
|
}
|
|
293
291
|
throw new Error(`Target tab ${candidate} could not be validated in the current browser session. ` +
|
|
294
|
-
'The Browser Bridge
|
|
292
|
+
'The Browser Bridge session may have restarted; re-run "opencli browser tab list" and choose a current target.', { cause: err });
|
|
295
293
|
}
|
|
296
294
|
if (Array.isArray(tabs) && hasBrowserTabTarget(tabs, candidate)) {
|
|
297
295
|
return candidate;
|
|
@@ -301,33 +299,33 @@ async function resolveBrowserTargetInSession(page, targetPage, opts) {
|
|
|
301
299
|
return undefined;
|
|
302
300
|
}
|
|
303
301
|
throw new Error(`Target tab ${candidate} is not part of the current browser session. ` +
|
|
304
|
-
'The Browser Bridge
|
|
302
|
+
'The Browser Bridge session may have restarted; re-run "opencli browser tab list" and choose a current target.');
|
|
305
303
|
}
|
|
306
|
-
function getBrowserScope(
|
|
307
|
-
return contextId ? `${contextId}:${
|
|
304
|
+
function getBrowserScope(session, contextId) {
|
|
305
|
+
return contextId ? `${contextId}:${session}` : session;
|
|
308
306
|
}
|
|
309
|
-
async function resolveStoredBrowserTarget(page, scope
|
|
307
|
+
async function resolveStoredBrowserTarget(page, scope) {
|
|
310
308
|
const defaultPage = loadBrowserTargetState(scope)?.defaultPage?.trim();
|
|
311
309
|
if (!defaultPage)
|
|
312
310
|
return undefined;
|
|
313
311
|
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
314
312
|
}
|
|
315
|
-
/** Create a browser page for browser commands. Uses a
|
|
316
|
-
async function getBrowserPage(
|
|
313
|
+
/** Create a browser page for browser commands. Uses a named browser session for continuity. */
|
|
314
|
+
async function getBrowserPage(session, targetPage, contextId, opts = {}) {
|
|
317
315
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
318
316
|
const bridge = new BrowserBridge();
|
|
319
|
-
//
|
|
320
|
-
// (controls when the automation tab is released). Not the per-command runtime timeout.
|
|
317
|
+
// Internal GC timeout for browser sessions. Not the per-command runtime timeout.
|
|
321
318
|
const envTimeout = process.env.OPENCLI_BROWSER_IDLE_TIMEOUT;
|
|
322
319
|
const idleTimeout = envTimeout ? parseInt(envTimeout, 10) : undefined;
|
|
323
320
|
const page = await bridge.connect({
|
|
324
321
|
timeout: 30,
|
|
325
|
-
|
|
322
|
+
session,
|
|
323
|
+
surface: 'browser',
|
|
326
324
|
...(contextId && { contextId }),
|
|
327
325
|
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
328
326
|
windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
|
|
329
327
|
});
|
|
330
|
-
const targetScope = getBrowserScope(
|
|
328
|
+
const targetScope = getBrowserScope(session, contextId);
|
|
331
329
|
const resolvedTargetPage = targetPage
|
|
332
330
|
? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
|
|
333
331
|
: await resolveStoredBrowserTarget(page, targetScope);
|
|
@@ -354,28 +352,6 @@ function getBrowserWindowMode(command, defaultMode) {
|
|
|
354
352
|
}
|
|
355
353
|
return defaultMode;
|
|
356
354
|
}
|
|
357
|
-
function parseBrowserBoolean(name, raw) {
|
|
358
|
-
if (raw === undefined || raw === '')
|
|
359
|
-
return undefined;
|
|
360
|
-
if (raw === 'true')
|
|
361
|
-
return true;
|
|
362
|
-
if (raw === 'false')
|
|
363
|
-
return false;
|
|
364
|
-
throw new Error(`${name} must be one of: true, false. Received: "${String(raw)}"`);
|
|
365
|
-
}
|
|
366
|
-
function getBrowserKeepTab(command, defaultValue) {
|
|
367
|
-
return parseBrowserBoolean('--keep-tab', getCommandOption(command, 'keepTab'))
|
|
368
|
-
?? parseBrowserBoolean('OPENCLI_KEEP_TAB', process.env.OPENCLI_KEEP_TAB)
|
|
369
|
-
?? defaultValue;
|
|
370
|
-
}
|
|
371
|
-
function hasExplicitBrowserWindowOption(command) {
|
|
372
|
-
const raw = getCommandOption(command, 'window');
|
|
373
|
-
return raw !== undefined && raw !== '';
|
|
374
|
-
}
|
|
375
|
-
function hasExplicitBrowserKeepTabOption(command) {
|
|
376
|
-
const raw = getCommandOption(command, 'keepTab');
|
|
377
|
-
return raw !== undefined && raw !== '';
|
|
378
|
-
}
|
|
379
355
|
function addBrowserTabOption(command) {
|
|
380
356
|
return command.option('--tab <targetId>', BROWSER_TAB_OPTION_DESCRIPTION);
|
|
381
357
|
}
|
|
@@ -395,21 +371,25 @@ function getCommandOption(command, option) {
|
|
|
395
371
|
}
|
|
396
372
|
return undefined;
|
|
397
373
|
}
|
|
398
|
-
function
|
|
399
|
-
const raw = getCommandOption(command, '
|
|
400
|
-
|
|
374
|
+
function getBrowserSession(command) {
|
|
375
|
+
const raw = getCommandOption(command, 'session');
|
|
376
|
+
if (typeof raw === 'string' && raw.trim())
|
|
377
|
+
return raw.trim();
|
|
378
|
+
throw new Error('--session <name> is required for opencli browser commands');
|
|
401
379
|
}
|
|
402
380
|
function getBrowserContextId(command) {
|
|
403
381
|
const raw = getCommandOption(command, 'profile');
|
|
404
382
|
return resolveProfileContextId(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
|
|
405
383
|
}
|
|
406
|
-
function
|
|
407
|
-
const
|
|
408
|
-
|
|
384
|
+
function getPageSession(page) {
|
|
385
|
+
const session = page.session;
|
|
386
|
+
if (typeof session === 'string' && session.trim())
|
|
387
|
+
return session.trim();
|
|
388
|
+
throw new Error('Browser page is missing a session');
|
|
409
389
|
}
|
|
410
390
|
function getPageScope(page) {
|
|
411
391
|
const contextId = page.contextId;
|
|
412
|
-
return getBrowserScope(
|
|
392
|
+
return getBrowserScope(getPageSession(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
|
|
413
393
|
}
|
|
414
394
|
function snapshotMetricText(snapshot) {
|
|
415
395
|
return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
|
|
@@ -620,9 +600,8 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
620
600
|
// All commands wrapped in browserAction() for consistent error handling.
|
|
621
601
|
const browser = program
|
|
622
602
|
.command('browser')
|
|
623
|
-
.
|
|
603
|
+
.requiredOption('--session <name>', 'Browser session to use (required)')
|
|
624
604
|
.option('--window <mode>', 'Browser window mode: foreground or background')
|
|
625
|
-
.option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes')
|
|
626
605
|
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)');
|
|
627
606
|
const originalBrowserDescription = browser.description();
|
|
628
607
|
/**
|
|
@@ -687,19 +666,13 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
687
666
|
function browserAction(fn) {
|
|
688
667
|
return async (...args) => {
|
|
689
668
|
let page = null;
|
|
690
|
-
let shouldReleasePage = false;
|
|
691
669
|
try {
|
|
692
670
|
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
693
671
|
const targetPage = getBrowserTargetId(command);
|
|
694
|
-
const
|
|
672
|
+
const session = getBrowserSession(command);
|
|
695
673
|
const contextId = getBrowserContextId(command);
|
|
696
674
|
const windowMode = getBrowserWindowMode(command, 'foreground');
|
|
697
|
-
|
|
698
|
-
shouldReleasePage = !keepTab && !workspace.startsWith('bound:');
|
|
699
|
-
if (workspace.startsWith('bound:') && (hasExplicitBrowserWindowOption(command) || hasExplicitBrowserKeepTabOption(command))) {
|
|
700
|
-
log.warn('--window/--keep-tab ignored for bound:* workspaces; bound tabs are user-owned.');
|
|
701
|
-
}
|
|
702
|
-
page = await getBrowserPage(targetPage, workspace, contextId, { windowMode });
|
|
675
|
+
page = await getBrowserPage(session, targetPage, contextId, { windowMode });
|
|
703
676
|
await fn(page, ...args);
|
|
704
677
|
}
|
|
705
678
|
catch (err) {
|
|
@@ -747,51 +720,22 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
747
720
|
}
|
|
748
721
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
749
722
|
}
|
|
750
|
-
finally {
|
|
751
|
-
if (shouldReleasePage && page?.closeWindow) {
|
|
752
|
-
await page.closeWindow().catch((err) => {
|
|
753
|
-
if (process.env.OPENCLI_VERBOSE)
|
|
754
|
-
log.warn(`[browser] Failed to release tab lease: ${getErrorMessage(err)}`);
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
723
|
};
|
|
759
724
|
}
|
|
760
725
|
browser.command('bind')
|
|
761
|
-
.option('--
|
|
762
|
-
.
|
|
763
|
-
.option('--workspace <name>', 'Bound workspace name (must start with bound:)')
|
|
764
|
-
.description('Bind a bound:* workspace to the current Chrome tab/window')
|
|
726
|
+
.option('--session <name>', 'Browser session name to bind (required)')
|
|
727
|
+
.description('Bind the current Chrome tab/window to a browser session')
|
|
765
728
|
.action(async (optsOrCommand, maybeCommand) => {
|
|
766
729
|
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
767
|
-
const
|
|
768
|
-
const rawWorkspace = getCommandOption(command, 'workspace');
|
|
769
|
-
const workspace = typeof rawWorkspace === 'string' && rawWorkspace.trim()
|
|
770
|
-
? rawWorkspace.trim()
|
|
771
|
-
: DEFAULT_BOUND_WORKSPACE;
|
|
772
|
-
if (!workspace.startsWith('bound:')) {
|
|
773
|
-
console.log(JSON.stringify({
|
|
774
|
-
error: {
|
|
775
|
-
code: 'invalid_bind_workspace',
|
|
776
|
-
message: `--workspace must start with "bound:", got "${workspace}"`,
|
|
777
|
-
hint: 'Use the default bound:default or pass --workspace bound:<name>.',
|
|
778
|
-
},
|
|
779
|
-
}, null, 2));
|
|
780
|
-
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
730
|
+
const session = getBrowserSession(command);
|
|
783
731
|
try {
|
|
784
732
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
785
733
|
const bridge = new BrowserBridge();
|
|
786
734
|
const contextId = getBrowserContextId(command);
|
|
787
|
-
await bridge.connect({ timeout: 30,
|
|
788
|
-
const data = await bindTab(
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
...(typeof opts.pathPrefix === 'string' && opts.pathPrefix.trim() ? { matchPathPrefix: opts.pathPrefix.trim() } : {}),
|
|
792
|
-
});
|
|
793
|
-
saveBrowserTargetState(undefined, getBrowserScope(workspace, contextId));
|
|
794
|
-
console.log(JSON.stringify({ workspace, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
735
|
+
await bridge.connect({ timeout: 30, session, surface: 'browser', ...(contextId && { contextId }) });
|
|
736
|
+
const data = await bindTab(session, { ...(contextId && { contextId }) });
|
|
737
|
+
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
738
|
+
console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
795
739
|
}
|
|
796
740
|
catch (err) {
|
|
797
741
|
if (err instanceof BrowserCommandError && err.code) {
|
|
@@ -806,39 +750,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
806
750
|
log.error(err instanceof Error ? err.message : String(err));
|
|
807
751
|
if (err instanceof BrowserCommandError && err.hint)
|
|
808
752
|
log.error(`Hint: ${err.hint}`);
|
|
809
|
-
process.exitCode =
|
|
810
|
-
? EXIT_CODES.USAGE_ERROR
|
|
811
|
-
: EXIT_CODES.GENERIC_ERROR;
|
|
753
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
812
754
|
}
|
|
813
755
|
});
|
|
814
756
|
browser.command('unbind')
|
|
815
|
-
.option('--
|
|
816
|
-
.description('Detach a bound
|
|
757
|
+
.option('--session <name>', 'Browser session name to detach (required)')
|
|
758
|
+
.description('Detach a bound browser session without closing the user tab/window')
|
|
817
759
|
.action(async (optsOrCommand, maybeCommand) => {
|
|
818
760
|
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
819
|
-
const
|
|
820
|
-
const workspace = typeof rawWorkspace === 'string' && rawWorkspace.trim()
|
|
821
|
-
? rawWorkspace.trim()
|
|
822
|
-
: DEFAULT_BOUND_WORKSPACE;
|
|
823
|
-
if (!workspace.startsWith('bound:')) {
|
|
824
|
-
console.log(JSON.stringify({
|
|
825
|
-
error: {
|
|
826
|
-
code: 'invalid_bind_workspace',
|
|
827
|
-
message: `--workspace must start with "bound:", got "${workspace}"`,
|
|
828
|
-
hint: 'Use the default bound:default or pass --workspace bound:<name>.',
|
|
829
|
-
},
|
|
830
|
-
}, null, 2));
|
|
831
|
-
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
761
|
+
const session = getBrowserSession(command);
|
|
834
762
|
try {
|
|
835
763
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
836
764
|
const bridge = new BrowserBridge();
|
|
837
765
|
const contextId = getBrowserContextId(command);
|
|
838
|
-
await bridge.connect({ timeout: 30,
|
|
839
|
-
await sendCommand('close-window', {
|
|
840
|
-
saveBrowserTargetState(undefined, getBrowserScope(
|
|
841
|
-
console.log(JSON.stringify({ unbound: true,
|
|
766
|
+
await bridge.connect({ timeout: 30, session, surface: 'browser', ...(contextId && { contextId }) });
|
|
767
|
+
await sendCommand('close-window', { session, surface: 'browser', ...(contextId && { contextId }) });
|
|
768
|
+
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
769
|
+
console.log(JSON.stringify({ unbound: true, session }, null, 2));
|
|
842
770
|
}
|
|
843
771
|
catch (err) {
|
|
844
772
|
if (err instanceof BrowserCommandError && err.code) {
|
|
@@ -858,9 +786,9 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
858
786
|
});
|
|
859
787
|
const browserTab = browser
|
|
860
788
|
.command('tab')
|
|
861
|
-
.description('Tab management — list, create, and close tabs in the
|
|
789
|
+
.description('Tab management — list, create, and close tabs in the browser session');
|
|
862
790
|
browserTab.command('list')
|
|
863
|
-
.description('List tabs in the
|
|
791
|
+
.description('List tabs in the browser session with target IDs')
|
|
864
792
|
.action(browserAction(async (page) => {
|
|
865
793
|
const tabs = await page.tabs();
|
|
866
794
|
console.log(JSON.stringify(tabs, null, 2));
|
|
@@ -927,16 +855,11 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
927
855
|
* capped at 200 entries, bounding worst-case in-page memory.
|
|
928
856
|
*/
|
|
929
857
|
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)}})()`;
|
|
930
|
-
addBrowserTabOption(browser.command('open').argument('<url>').
|
|
858
|
+
addBrowserTabOption(browser.command('open').argument('<url>').description('Open URL in the browser session'))
|
|
931
859
|
.action(browserAction(async (page, url, opts) => {
|
|
932
860
|
// Start session-level capture before navigation (catches initial requests)
|
|
933
861
|
const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
|
|
934
|
-
|
|
935
|
-
await page.goto(url, { allowBoundNavigation: true });
|
|
936
|
-
}
|
|
937
|
-
else {
|
|
938
|
-
await page.goto(url);
|
|
939
|
-
}
|
|
862
|
+
await page.goto(url);
|
|
940
863
|
await page.wait(2);
|
|
941
864
|
// Fallback: inject JS interceptor when session capture is unavailable
|
|
942
865
|
if (!hasSessionCapture) {
|
|
@@ -950,19 +873,8 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
950
873
|
...(page.getActivePage?.() ? { page: page.getActivePage?.() } : {}),
|
|
951
874
|
}, null, 2));
|
|
952
875
|
}));
|
|
953
|
-
addBrowserTabOption(browser.command('back').
|
|
876
|
+
addBrowserTabOption(browser.command('back').description('Go back in browser history'))
|
|
954
877
|
.action(browserAction(async (page, opts) => {
|
|
955
|
-
if (getPageWorkspace(page).startsWith('bound:') && opts.allowNavigateBound !== true) {
|
|
956
|
-
console.log(JSON.stringify({
|
|
957
|
-
error: {
|
|
958
|
-
code: 'bound_navigation_blocked',
|
|
959
|
-
message: `Workspace "${getPageWorkspace(page)}" is bound to a user tab; history navigation is blocked by default.`,
|
|
960
|
-
hint: 'Pass --allow-navigate-bound only if you intentionally want to navigate the bound tab.',
|
|
961
|
-
},
|
|
962
|
-
}, null, 2));
|
|
963
|
-
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
878
|
await page.evaluate('history.back()');
|
|
967
879
|
await page.wait(2);
|
|
968
880
|
console.log('Navigated back');
|
|
@@ -1093,7 +1005,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
1093
1005
|
}
|
|
1094
1006
|
const messages = filter(normalize(await page.consoleMessages(opts.level)));
|
|
1095
1007
|
console.log(JSON.stringify({
|
|
1096
|
-
|
|
1008
|
+
session: getPageSession(page),
|
|
1097
1009
|
captured_at: new Date().toISOString(),
|
|
1098
1010
|
count: messages.length,
|
|
1099
1011
|
messages: messages.map((message) => ({
|
|
@@ -2185,7 +2097,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
2185
2097
|
// Default output is JSON (agent-native). Each entry carries a stable `key`
|
|
2186
2098
|
// (GraphQL operationName or `METHOD host+pathname`) so agents can fetch
|
|
2187
2099
|
// full bodies with `--detail <key>` even after subsequent commands.
|
|
2188
|
-
// Captures are persisted per
|
|
2100
|
+
// Captures are persisted per browser session under ~/.opencli/cache/browser-network/.
|
|
2189
2101
|
addBrowserTabOption(browser.command('network'))
|
|
2190
2102
|
.option('--detail <key>', 'Emit full body for the entry with this key')
|
|
2191
2103
|
.option('--all', 'Include static resources (js/css/images/telemetry)')
|
|
@@ -2200,7 +2112,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
2200
2112
|
.description('Capture network requests as shape previews; retrieve full bodies by key')
|
|
2201
2113
|
.action(browserAction(async (page, opts) => {
|
|
2202
2114
|
const ttlMs = parsePositiveIntOption(opts.ttl, 'ttl', DEFAULT_TTL_MS);
|
|
2203
|
-
const
|
|
2115
|
+
const session = getPageSession(page);
|
|
2204
2116
|
const hasDetail = typeof opts.detail === 'string' && opts.detail.length > 0;
|
|
2205
2117
|
const hasFilter = typeof opts.filter === 'string';
|
|
2206
2118
|
const sinceMs = parseDurationMs(opts.since, 'since');
|
|
@@ -2236,9 +2148,9 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
2236
2148
|
}
|
|
2237
2149
|
// --detail short-circuits: read from cache only, no live capture needed.
|
|
2238
2150
|
if (hasDetail) {
|
|
2239
|
-
const res = loadNetworkCache(
|
|
2151
|
+
const res = loadNetworkCache(session, { ttlMs });
|
|
2240
2152
|
if (res.status === 'missing') {
|
|
2241
|
-
emitNetworkError('cache_missing', `No cached capture. Run "browser network" first (in
|
|
2153
|
+
emitNetworkError('cache_missing', `No cached capture. Run "browser network" first (in session "${session}").`);
|
|
2242
2154
|
return;
|
|
2243
2155
|
}
|
|
2244
2156
|
if (res.status === 'expired') {
|
|
@@ -2359,7 +2271,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
2359
2271
|
// via the output envelope rather than erroring out the whole command.
|
|
2360
2272
|
let cacheWarning = null;
|
|
2361
2273
|
try {
|
|
2362
|
-
saveNetworkCache(
|
|
2274
|
+
saveNetworkCache(session, cacheEntries);
|
|
2363
2275
|
}
|
|
2364
2276
|
catch (err) {
|
|
2365
2277
|
cacheWarning = `Could not persist capture cache: ${err.message}. --detail lookups may miss this capture.`;
|
|
@@ -2375,7 +2287,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
2375
2287
|
: shaped;
|
|
2376
2288
|
const filterDropped = filterFields ? shaped.length - visible.length : 0;
|
|
2377
2289
|
const envelope = {
|
|
2378
|
-
|
|
2290
|
+
session,
|
|
2379
2291
|
captured_at: new Date().toISOString(),
|
|
2380
2292
|
count: visible.length,
|
|
2381
2293
|
filtered_out: filteredOut,
|
|
@@ -2440,19 +2352,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
2440
2352
|
console.log(`Adapter already exists: ${filePath}`);
|
|
2441
2353
|
return;
|
|
2442
2354
|
}
|
|
2443
|
-
// Try to detect domain from the last browser session
|
|
2444
2355
|
let domain = site;
|
|
2445
|
-
try {
|
|
2446
|
-
const page = await getBrowserPage();
|
|
2447
|
-
const url = await page.getCurrentUrl?.();
|
|
2448
|
-
if (url) {
|
|
2449
|
-
try {
|
|
2450
|
-
domain = new URL(url).hostname;
|
|
2451
|
-
}
|
|
2452
|
-
catch { }
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
catch { /* no active session */ }
|
|
2456
2356
|
const template = `import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2457
2357
|
|
|
2458
2358
|
cli({
|
|
@@ -2640,22 +2540,20 @@ cli({
|
|
|
2640
2540
|
}
|
|
2641
2541
|
});
|
|
2642
2542
|
// ── Session ──
|
|
2643
|
-
browser.command('close').description('Release the current
|
|
2543
|
+
browser.command('close').description('Release the current browser session tab lease')
|
|
2644
2544
|
.action(browserAction(async (page) => {
|
|
2645
2545
|
await page.closeWindow?.();
|
|
2646
|
-
console.log('
|
|
2546
|
+
console.log('Browser session tab lease released');
|
|
2647
2547
|
}));
|
|
2648
2548
|
// ── Built-in: doctor / completion ──────────────────────────────────────────
|
|
2649
2549
|
program
|
|
2650
2550
|
.command('doctor')
|
|
2651
2551
|
.description('Diagnose opencli browser bridge connectivity')
|
|
2652
|
-
.option('--no-live', 'Skip live browser connectivity test')
|
|
2653
|
-
.option('--sessions', 'Show active automation sessions', false)
|
|
2654
2552
|
.option('-v, --verbose', 'Debug output')
|
|
2655
2553
|
.action(async (opts) => {
|
|
2656
2554
|
applyVerbose(opts);
|
|
2657
2555
|
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
2658
|
-
const report = await runBrowserDoctor({
|
|
2556
|
+
const report = await runBrowserDoctor({ cliVersion: PKG_VERSION });
|
|
2659
2557
|
console.log(renderBrowserDoctorReport(report));
|
|
2660
2558
|
});
|
|
2661
2559
|
program
|