@jackwener/opencli 1.7.15 → 1.7.17
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 +15 -13
- package/README.zh-CN.md +15 -12
- package/cli-manifest.json +165 -209
- package/clis/chatgpt/ask.js +3 -2
- package/clis/chatgpt/commands.test.js +2 -2
- package/clis/chatgpt/detail.js +7 -2
- 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 +3 -2
- package/clis/chatgpt/send.js +3 -2
- package/clis/chatgpt/status.js +1 -1
- package/clis/chatgpt/utils.js +259 -25
- package/clis/chatgpt/utils.test.js +166 -2
- package/clis/claude/ask.js +23 -8
- package/clis/claude/detail.js +10 -3
- package/clis/claude/history.js +1 -1
- package/clis/claude/new.js +9 -3
- package/clis/claude/read.js +3 -2
- package/clis/claude/send.js +9 -4
- package/clis/claude/status.js +1 -1
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +22 -9
- package/clis/deepseek/detail.js +10 -2
- package/clis/deepseek/history.js +1 -1
- package/clis/deepseek/new.js +14 -3
- package/clis/deepseek/read.js +3 -2
- package/clis/deepseek/send.js +1 -1
- package/clis/deepseek/status.js +1 -1
- package/clis/deepseek/utils.js +8 -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/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/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- 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/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- 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 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +2 -0
- package/clis/reddit/read.test.js +4 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +1 -0
- package/clis/reddit/subreddit.js +1 -0
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +1 -0
- package/clis/reddit/user-posts.js +1 -0
- package/clis/reddit/user.js +1 -0
- package/clis/twitter/article.js +7 -4
- package/clis/twitter/bookmark-folder.js +3 -5
- package/clis/twitter/bookmark-folder.test.js +5 -2
- package/clis/twitter/bookmark-folders.js +3 -5
- package/clis/twitter/bookmark-folders.test.js +3 -1
- package/clis/twitter/bookmarks.js +3 -5
- package/clis/twitter/download.js +1 -0
- package/clis/twitter/followers.js +1 -0
- package/clis/twitter/following.js +3 -6
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/likes.js +3 -5
- package/clis/twitter/list-add.js +4 -3
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +4 -3
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +3 -5
- package/clis/twitter/lists.js +3 -5
- package/clis/twitter/notifications.js +1 -0
- package/clis/twitter/profile.js +7 -4
- package/clis/twitter/search.js +1 -0
- package/clis/twitter/thread.js +5 -7
- package/clis/twitter/timeline.js +5 -7
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +3 -6
- package/clis/youtube/like.js +6 -2
- package/clis/youtube/subscribe.js +6 -2
- package/clis/youtube/unlike.js +6 -2
- package/clis/youtube/unsubscribe.js +6 -2
- package/clis/youtube/utils.js +19 -13
- package/clis/youtube/utils.test.js +17 -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 +4 -1
- package/dist/src/browser/bridge.js +3 -1
- package/dist/src/browser/cdp.d.ts +4 -1
- package/dist/src/browser/daemon-client.d.ts +9 -16
- package/dist/src/browser/daemon-client.js +8 -9
- package/dist/src/browser/daemon-client.test.js +10 -0
- 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 +9 -7
- package/dist/src/browser/page.js +27 -16
- package/dist/src/browser/page.test.js +60 -30
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli.js +91 -125
- package/dist/src/cli.test.js +293 -180
- package/dist/src/commanderAdapter.js +9 -0
- package/dist/src/discovery.js +1 -1
- package/dist/src/doctor.d.ts +0 -4
- package/dist/src/doctor.js +8 -72
- package/dist/src/doctor.test.js +26 -97
- package/dist/src/execution.d.ts +3 -0
- package/dist/src/execution.js +47 -23
- package/dist/src/execution.test.js +68 -45
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +1 -0
- package/dist/src/help.js +36 -1
- package/dist/src/main.js +0 -29
- 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 +10 -2
- package/dist/src/runtime.js +4 -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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* YouTube unsubscribe — unsubscribe from a channel via InnerTube subscription API.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import { prepareYoutubeApiPage, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js';
|
|
5
|
+
import { prepareYoutubeApiPage, readYoutubeSapisid, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js';
|
|
6
6
|
import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors';
|
|
7
7
|
|
|
8
8
|
cli({
|
|
@@ -19,6 +19,10 @@ cli({
|
|
|
19
19
|
func: async (page, kwargs) => {
|
|
20
20
|
const channelInput = String(kwargs.channel);
|
|
21
21
|
await prepareYoutubeApiPage(page);
|
|
22
|
+
// Read SAPISID directly from the cookie store via CDP — zero document.cookie round-trip
|
|
23
|
+
const sapisid = await readYoutubeSapisid(page);
|
|
24
|
+
if (!sapisid)
|
|
25
|
+
throw new AuthRequiredError('www.youtube.com', 'Not logged in (SAPISID cookie missing)');
|
|
22
26
|
const result = await page.evaluate(`
|
|
23
27
|
(async () => {
|
|
24
28
|
${SAPISID_HASH_FN}
|
|
@@ -28,7 +32,7 @@ cli({
|
|
|
28
32
|
const context = cfg.INNERTUBE_CONTEXT;
|
|
29
33
|
if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' };
|
|
30
34
|
|
|
31
|
-
const authHash = await getSapisidHash('https://www.youtube.com');
|
|
35
|
+
const authHash = await getSapisidHash(${JSON.stringify(sapisid)}, 'https://www.youtube.com');
|
|
32
36
|
if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' };
|
|
33
37
|
|
|
34
38
|
${RESOLVE_CHANNEL_HANDLE_FN}
|
package/clis/youtube/utils.js
CHANGED
|
@@ -189,21 +189,13 @@ async function resolveChannelHandle(input, apiKey, context) {
|
|
|
189
189
|
* Inline SAPISIDHASH helper for use inside page.evaluate() strings.
|
|
190
190
|
* YouTube write APIs (like, subscribe) require:
|
|
191
191
|
* Authorization: SAPISIDHASH {time}_{SHA1(time + " " + SAPISID + " " + origin)}
|
|
192
|
+
*
|
|
193
|
+
* The SAPISID cookie value must be hoisted from the cookie store on the Node side
|
|
194
|
+
* (via `readYoutubeSapisid(page)`) and passed in here — keeps `crypto.subtle.digest`
|
|
195
|
+
* (browser Web Crypto) call site, but no `document.cookie` round-trip.
|
|
192
196
|
*/
|
|
193
197
|
export const SAPISID_HASH_FN = `
|
|
194
|
-
async function getSapisidHash(origin) {
|
|
195
|
-
const cookies = document.cookie.split('; ');
|
|
196
|
-
let sapisid = '';
|
|
197
|
-
for (const c of cookies) {
|
|
198
|
-
const eq = c.indexOf('=');
|
|
199
|
-
if (eq === -1) continue;
|
|
200
|
-
const name = c.slice(0, eq);
|
|
201
|
-
const val = c.slice(eq + 1);
|
|
202
|
-
if (name === '__Secure-3PAPISID' || name === 'SAPISID') {
|
|
203
|
-
sapisid = val;
|
|
204
|
-
if (name === '__Secure-3PAPISID') break;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
198
|
+
async function getSapisidHash(sapisid, origin) {
|
|
207
199
|
if (!sapisid) return null;
|
|
208
200
|
const time = Math.floor(Date.now() / 1000);
|
|
209
201
|
const msgBuffer = new TextEncoder().encode(time + ' ' + sapisid + ' ' + origin);
|
|
@@ -212,3 +204,17 @@ async function getSapisidHash(origin) {
|
|
|
212
204
|
return 'SAPISIDHASH ' + time + '_' + hashHex;
|
|
213
205
|
}
|
|
214
206
|
`;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Read the YouTube SAPISID cookie via CDP, preferring `__Secure-3PAPISID`
|
|
210
|
+
* (current first-party cookie) and falling back to the legacy `SAPISID` name.
|
|
211
|
+
* Returns the cookie value, or null if neither is present.
|
|
212
|
+
*/
|
|
213
|
+
export async function readYoutubeSapisid(page) {
|
|
214
|
+
const cookies = await page.getCookies({ url: 'https://www.youtube.com' });
|
|
215
|
+
return (
|
|
216
|
+
cookies.find((c) => c.name === '__Secure-3PAPISID')?.value
|
|
217
|
+
|| cookies.find((c) => c.name === 'SAPISID')?.value
|
|
218
|
+
|| null
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { extractJsonAssignmentFromHtml, extractSubscriptionChannel, prepareYoutubeApiPage } from './utils.js';
|
|
2
|
+
import { extractJsonAssignmentFromHtml, extractSubscriptionChannel, prepareYoutubeApiPage, readYoutubeSapisid } from './utils.js';
|
|
3
3
|
describe('youtube utils', () => {
|
|
4
4
|
it('extractJsonAssignmentFromHtml parses bootstrap objects with nested braces in strings', () => {
|
|
5
5
|
const html = `
|
|
@@ -34,6 +34,22 @@ describe('youtube utils', () => {
|
|
|
34
34
|
expect(page.goto).toHaveBeenCalledWith('https://www.youtube.com', { waitUntil: 'none' });
|
|
35
35
|
expect(page.wait).toHaveBeenCalledWith(2);
|
|
36
36
|
});
|
|
37
|
+
it('readYoutubeSapisid reads URL-scoped cookies and prefers secure SAPISID', async () => {
|
|
38
|
+
const page = {
|
|
39
|
+
getCookies: vi.fn().mockResolvedValue([
|
|
40
|
+
{ name: 'SAPISID', value: 'legacy' },
|
|
41
|
+
{ name: '__Secure-3PAPISID', value: 'secure' },
|
|
42
|
+
]),
|
|
43
|
+
};
|
|
44
|
+
await expect(readYoutubeSapisid(page)).resolves.toBe('secure');
|
|
45
|
+
expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://www.youtube.com' });
|
|
46
|
+
});
|
|
47
|
+
it('readYoutubeSapisid falls back to legacy SAPISID', async () => {
|
|
48
|
+
const page = {
|
|
49
|
+
getCookies: vi.fn().mockResolvedValue([{ name: 'SAPISID', value: 'legacy' }]),
|
|
50
|
+
};
|
|
51
|
+
await expect(readYoutubeSapisid(page)).resolves.toBe('legacy');
|
|
52
|
+
});
|
|
37
53
|
it('extractSubscriptionChannel prefers explicit handle and subscriber count fields', () => {
|
|
38
54
|
expect(extractSubscriptionChannel({
|
|
39
55
|
title: { simpleText: 'OpenAI' },
|
package/clis/yuanbao/ask.js
CHANGED
package/clis/yuanbao/detail.js
CHANGED
package/clis/yuanbao/history.js
CHANGED
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
domain: YUANBAO_DOMAIN,
|
|
18
18
|
strategy: Strategy.COOKIE,
|
|
19
19
|
browser: true,
|
|
20
|
-
|
|
20
|
+
siteSession: 'persistent',
|
|
21
21
|
navigateBefore: false,
|
|
22
22
|
args: [
|
|
23
23
|
{ name: 'limit', type: 'int', default: 20, help: 'Max conversations to list (sidebar virtual scroll caps actual count)' },
|
package/clis/yuanbao/new.js
CHANGED
package/clis/yuanbao/read.js
CHANGED
package/clis/yuanbao/send.js
CHANGED
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
domain: YUANBAO_DOMAIN,
|
|
19
19
|
strategy: Strategy.COOKIE,
|
|
20
20
|
browser: true,
|
|
21
|
-
|
|
21
|
+
siteSession: 'persistent',
|
|
22
22
|
navigateBefore: false,
|
|
23
23
|
args: [
|
|
24
24
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send to Yuanbao' },
|
package/clis/yuanbao/status.js
CHANGED
|
@@ -15,7 +15,7 @@ cli({
|
|
|
15
15
|
domain: YUANBAO_DOMAIN,
|
|
16
16
|
strategy: Strategy.COOKIE,
|
|
17
17
|
browser: true,
|
|
18
|
-
|
|
18
|
+
siteSession: 'persistent',
|
|
19
19
|
navigateBefore: false,
|
|
20
20
|
args: [],
|
|
21
21
|
columns: ['Status', 'Login', 'Model', 'ModelId', 'AgentId', 'SessionId', 'Url'],
|
|
@@ -14,9 +14,12 @@ export declare class BrowserBridge implements IBrowserFactory {
|
|
|
14
14
|
get state(): BrowserBridgeState;
|
|
15
15
|
connect(opts?: {
|
|
16
16
|
timeout?: number;
|
|
17
|
-
|
|
17
|
+
session?: string;
|
|
18
18
|
idleTimeout?: number;
|
|
19
19
|
contextId?: string;
|
|
20
|
+
windowMode?: 'foreground' | 'background';
|
|
21
|
+
surface?: 'browser' | 'adapter';
|
|
22
|
+
siteSession?: 'ephemeral' | 'persistent';
|
|
20
23
|
}): Promise<IPage>;
|
|
21
24
|
close(): Promise<void>;
|
|
22
25
|
private _ensureDaemon;
|
|
@@ -32,7 +32,9 @@ export class BrowserBridge {
|
|
|
32
32
|
try {
|
|
33
33
|
const contextId = opts.contextId ?? resolveProfileContextId();
|
|
34
34
|
await this._ensureDaemon(opts.timeout, contextId);
|
|
35
|
-
|
|
35
|
+
if (!opts.session?.trim())
|
|
36
|
+
throw new Error('Browser session is required');
|
|
37
|
+
this._page = new Page(opts.session.trim(), opts.idleTimeout, contextId, opts.windowMode, opts.surface, opts.siteSession);
|
|
36
38
|
this._state = 'connected';
|
|
37
39
|
return this._page;
|
|
38
40
|
}
|
|
@@ -23,10 +23,13 @@ export declare class CDPBridge implements IBrowserFactory {
|
|
|
23
23
|
private _eventListeners;
|
|
24
24
|
connect(opts?: {
|
|
25
25
|
timeout?: number;
|
|
26
|
-
|
|
26
|
+
session?: string;
|
|
27
27
|
cdpEndpoint?: string;
|
|
28
28
|
contextId?: string;
|
|
29
29
|
idleTimeout?: number;
|
|
30
|
+
windowMode?: 'foreground' | 'background';
|
|
31
|
+
surface?: 'browser' | 'adapter';
|
|
32
|
+
siteSession?: 'ephemeral' | 'persistent';
|
|
30
33
|
}): Promise<IPage>;
|
|
31
34
|
close(): Promise<void>;
|
|
32
35
|
send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides a typed send() function that posts a Command and returns a Result.
|
|
5
5
|
*/
|
|
6
|
-
import type { BrowserSessionInfo } from '../types.js';
|
|
7
6
|
export interface DaemonCommand {
|
|
8
7
|
id: string;
|
|
9
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | '
|
|
8
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames';
|
|
10
9
|
/** Target page identity (targetId). Cross-layer contract with the extension. */
|
|
11
10
|
page?: string;
|
|
12
11
|
code?: string;
|
|
13
|
-
|
|
12
|
+
session?: string;
|
|
13
|
+
surface?: 'browser' | 'adapter';
|
|
14
|
+
/** Adapter site session lifecycle. Persistent site sessions do not idle-expire. */
|
|
15
|
+
siteSession?: 'ephemeral' | 'persistent';
|
|
14
16
|
url?: string;
|
|
15
17
|
op?: string;
|
|
16
18
|
index?: number;
|
|
17
19
|
domain?: string;
|
|
18
|
-
matchDomain?: string;
|
|
19
|
-
matchPathPrefix?: string;
|
|
20
20
|
format?: 'png' | 'jpeg';
|
|
21
21
|
quality?: number;
|
|
22
22
|
fullPage?: boolean;
|
|
@@ -36,12 +36,10 @@ export interface DaemonCommand {
|
|
|
36
36
|
timeoutMs?: number;
|
|
37
37
|
cdpMethod?: string;
|
|
38
38
|
cdpParams?: Record<string, unknown>;
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
/** Custom idle timeout in seconds for this
|
|
39
|
+
/** Window foreground/background policy for owned Browser Bridge containers. */
|
|
40
|
+
windowMode?: 'foreground' | 'background';
|
|
41
|
+
/** Custom idle timeout in seconds for this session. Overrides the default. */
|
|
42
42
|
idleTimeout?: number;
|
|
43
|
-
/** Explicitly allow navigation inside a borrowed bound tab. */
|
|
44
|
-
allowBoundNavigation?: boolean;
|
|
45
43
|
/** Frame index for cross-frame operations (0-based, from 'frames' action) */
|
|
46
44
|
frameIndex?: number;
|
|
47
45
|
/** Browser profile/context to route the command to. */
|
|
@@ -129,11 +127,6 @@ export declare function sendCommandFull(action: DaemonCommand['action'], params?
|
|
|
129
127
|
data: unknown;
|
|
130
128
|
page?: string;
|
|
131
129
|
}>;
|
|
132
|
-
export declare function
|
|
133
|
-
contextId?: string;
|
|
134
|
-
}): Promise<BrowserSessionInfo[]>;
|
|
135
|
-
export declare function bindTab(workspace: string, opts?: {
|
|
136
|
-
matchDomain?: string;
|
|
137
|
-
matchPathPrefix?: string;
|
|
130
|
+
export declare function bindTab(session: string, opts?: {
|
|
138
131
|
contextId?: string;
|
|
139
132
|
}): Promise<unknown>;
|
|
@@ -89,10 +89,13 @@ async function sendCommandRaw(action, params) {
|
|
|
89
89
|
const maxRetries = 4;
|
|
90
90
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
91
91
|
const id = generateId();
|
|
92
|
-
const
|
|
93
|
-
const
|
|
92
|
+
const rawWindowMode = process.env.OPENCLI_WINDOW;
|
|
93
|
+
const envWindowMode = rawWindowMode === 'foreground' || rawWindowMode === 'background'
|
|
94
|
+
? rawWindowMode
|
|
95
|
+
: undefined;
|
|
94
96
|
const contextId = params.contextId ?? resolveProfileContextId();
|
|
95
|
-
const
|
|
97
|
+
const windowMode = params.windowMode ?? envWindowMode;
|
|
98
|
+
const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowMode && { windowMode }) };
|
|
96
99
|
try {
|
|
97
100
|
const res = await requestDaemon('/command', {
|
|
98
101
|
method: 'POST',
|
|
@@ -143,10 +146,6 @@ export async function sendCommandFull(action, params = {}) {
|
|
|
143
146
|
const result = await sendCommandRaw(action, params);
|
|
144
147
|
return { data: result.data, page: result.page };
|
|
145
148
|
}
|
|
146
|
-
export async function
|
|
147
|
-
|
|
148
|
-
return Array.isArray(result) ? result : [];
|
|
149
|
-
}
|
|
150
|
-
export async function bindTab(workspace, opts = {}) {
|
|
151
|
-
return sendCommand('bind', { workspace, ...opts });
|
|
149
|
+
export async function bindTab(session, opts = {}) {
|
|
150
|
+
return sendCommand('bind', { session, surface: 'browser', ...opts });
|
|
152
151
|
}
|
|
@@ -144,6 +144,16 @@ describe('daemon-client', () => {
|
|
|
144
144
|
const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
|
|
145
145
|
expect(body.contextId).toBe('work');
|
|
146
146
|
});
|
|
147
|
+
it('sendCommand uses explicit windowMode before OPENCLI_WINDOW env fallback', async () => {
|
|
148
|
+
vi.stubEnv('OPENCLI_WINDOW', 'foreground');
|
|
149
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
150
|
+
status: 200,
|
|
151
|
+
json: () => Promise.resolve({ id: 'server', ok: true, data: 'ok' }),
|
|
152
|
+
});
|
|
153
|
+
await sendCommand('exec', { code: '1 + 1', windowMode: 'background' });
|
|
154
|
+
const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
|
|
155
|
+
expect(body.windowMode).toBe('background');
|
|
156
|
+
});
|
|
147
157
|
it('sendCommand retries with a new id when daemon reports a duplicate pending id', async () => {
|
|
148
158
|
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_123);
|
|
149
159
|
const fetchMock = vi.mocked(fetch);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* stable references to request bodies after running other commands,
|
|
7
7
|
* so every `browser network` call snapshots its results to disk.
|
|
8
8
|
*
|
|
9
|
-
* Layout: <cacheDir>/browser-network/<
|
|
9
|
+
* Layout: <cacheDir>/browser-network/<session>.json
|
|
10
10
|
* Entries expire after DEFAULT_TTL_MS (24h).
|
|
11
11
|
*/
|
|
12
12
|
export declare const DEFAULT_TTL_MS: number;
|
|
@@ -29,12 +29,12 @@ export interface CachedNetworkEntry {
|
|
|
29
29
|
}
|
|
30
30
|
export interface NetworkCacheFile {
|
|
31
31
|
version: 1;
|
|
32
|
-
|
|
32
|
+
session: string;
|
|
33
33
|
savedAt: string;
|
|
34
34
|
entries: CachedNetworkEntry[];
|
|
35
35
|
}
|
|
36
|
-
export declare function getCachePath(
|
|
37
|
-
export declare function saveNetworkCache(
|
|
36
|
+
export declare function getCachePath(session: string, baseDir?: string): string;
|
|
37
|
+
export declare function saveNetworkCache(session: string, entries: CachedNetworkEntry[], baseDir?: string): void;
|
|
38
38
|
export interface LoadOptions {
|
|
39
39
|
baseDir?: string;
|
|
40
40
|
ttlMs?: number;
|
|
@@ -45,5 +45,5 @@ export interface LoadResult {
|
|
|
45
45
|
file?: NetworkCacheFile;
|
|
46
46
|
ageMs?: number;
|
|
47
47
|
}
|
|
48
|
-
export declare function loadNetworkCache(
|
|
48
|
+
export declare function loadNetworkCache(session: string, opts?: LoadOptions): LoadResult;
|
|
49
49
|
export declare function findEntry(file: NetworkCacheFile, key: string): CachedNetworkEntry | null;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* stable references to request bodies after running other commands,
|
|
7
7
|
* so every `browser network` call snapshots its results to disk.
|
|
8
8
|
*
|
|
9
|
-
* Layout: <cacheDir>/browser-network/<
|
|
9
|
+
* Layout: <cacheDir>/browser-network/<session>.json
|
|
10
10
|
* Entries expire after DEFAULT_TTL_MS (24h).
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from 'node:fs';
|
|
@@ -16,23 +16,23 @@ export const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
16
16
|
function getDefaultCacheDir() {
|
|
17
17
|
return process.env.OPENCLI_CACHE_DIR || path.join(os.homedir(), '.opencli', 'cache');
|
|
18
18
|
}
|
|
19
|
-
export function getCachePath(
|
|
20
|
-
const safe =
|
|
19
|
+
export function getCachePath(session, baseDir = getDefaultCacheDir()) {
|
|
20
|
+
const safe = session.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
21
21
|
return path.join(baseDir, 'browser-network', `${safe}.json`);
|
|
22
22
|
}
|
|
23
|
-
export function saveNetworkCache(
|
|
24
|
-
const target = getCachePath(
|
|
23
|
+
export function saveNetworkCache(session, entries, baseDir) {
|
|
24
|
+
const target = getCachePath(session, baseDir);
|
|
25
25
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
26
26
|
const payload = {
|
|
27
27
|
version: 1,
|
|
28
|
-
|
|
28
|
+
session,
|
|
29
29
|
savedAt: new Date().toISOString(),
|
|
30
30
|
entries,
|
|
31
31
|
};
|
|
32
32
|
fs.writeFileSync(target, JSON.stringify(payload), 'utf-8');
|
|
33
33
|
}
|
|
34
|
-
export function loadNetworkCache(
|
|
35
|
-
const target = getCachePath(
|
|
34
|
+
export function loadNetworkCache(session, opts = {}) {
|
|
35
|
+
const target = getCachePath(session, opts.baseDir);
|
|
36
36
|
let raw;
|
|
37
37
|
try {
|
|
38
38
|
raw = fs.readFileSync(target, 'utf-8');
|
|
@@ -14,9 +14,9 @@ describe('network-cache', () => {
|
|
|
14
14
|
afterEach(() => {
|
|
15
15
|
fs.rmSync(baseDir, { recursive: true, force: true });
|
|
16
16
|
});
|
|
17
|
-
it('sanitizes
|
|
18
|
-
const p = getCachePath('
|
|
19
|
-
expect(path.basename(p)).toBe('
|
|
17
|
+
it('sanitizes session names into safe filenames', () => {
|
|
18
|
+
const p = getCachePath('twitter/agent 1', baseDir);
|
|
19
|
+
expect(path.basename(p)).toBe('twitter_agent_1.json');
|
|
20
20
|
});
|
|
21
21
|
it('round-trips entries through save + load', () => {
|
|
22
22
|
saveNetworkCache('ws', [makeEntry('UserTweets'), makeEntry('UserByScreenName')], baseDir);
|
|
@@ -49,7 +49,7 @@ describe('network-cache', () => {
|
|
|
49
49
|
});
|
|
50
50
|
it('findEntry returns matching entry or null', () => {
|
|
51
51
|
const file = {
|
|
52
|
-
version: 1,
|
|
52
|
+
version: 1, session: 'ws', savedAt: new Date().toISOString(),
|
|
53
53
|
entries: [makeEntry('A'), makeEntry('B')],
|
|
54
54
|
};
|
|
55
55
|
expect(findEntry(file, 'B')?.key).toBe('B');
|
|
@@ -14,22 +14,24 @@ import { BasePage } from './base-page.js';
|
|
|
14
14
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
15
15
|
*/
|
|
16
16
|
export declare class Page extends BasePage {
|
|
17
|
-
private readonly
|
|
17
|
+
private readonly session;
|
|
18
18
|
readonly contextId?: string | undefined;
|
|
19
|
+
private readonly windowMode?;
|
|
20
|
+
private readonly surface;
|
|
21
|
+
private readonly siteSession?;
|
|
19
22
|
private readonly _idleTimeout;
|
|
20
|
-
constructor(
|
|
23
|
+
constructor(session: string, idleTimeout?: number, contextId?: string | undefined, windowMode?: "foreground" | "background" | undefined, surface?: 'browser' | 'adapter', siteSession?: "ephemeral" | "persistent" | undefined);
|
|
21
24
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
22
25
|
private _page;
|
|
23
26
|
private _networkCaptureUnsupported;
|
|
24
27
|
private _networkCaptureWarned;
|
|
25
|
-
/** Helper: spread
|
|
26
|
-
private
|
|
27
|
-
/** Helper: spread
|
|
28
|
+
/** Helper: spread session into command params */
|
|
29
|
+
private _sessionOpts;
|
|
30
|
+
/** Helper: spread session + page identity into command params */
|
|
28
31
|
private _cmdOpts;
|
|
29
32
|
goto(url: string, options?: {
|
|
30
33
|
waitUntil?: 'load' | 'none';
|
|
31
34
|
settleMs?: number;
|
|
32
|
-
allowBoundNavigation?: boolean;
|
|
33
35
|
}): Promise<void>;
|
|
34
36
|
/** Get the active page identity (targetId) */
|
|
35
37
|
getActivePage(): string | undefined;
|
|
@@ -41,7 +43,7 @@ export declare class Page extends BasePage {
|
|
|
41
43
|
domain?: string;
|
|
42
44
|
url?: string;
|
|
43
45
|
}): Promise<BrowserCookie[]>;
|
|
44
|
-
/** Release the current
|
|
46
|
+
/** Release the current browser session lease in the extension */
|
|
45
47
|
closeWindow(): Promise<void>;
|
|
46
48
|
tabs(): Promise<unknown[]>;
|
|
47
49
|
newTab(url?: string): Promise<string | undefined>;
|
package/dist/src/browser/page.js
CHANGED
|
@@ -26,41 +26,52 @@ function isUnsupportedNetworkCaptureError(err) {
|
|
|
26
26
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
27
27
|
*/
|
|
28
28
|
export class Page extends BasePage {
|
|
29
|
-
|
|
29
|
+
session;
|
|
30
30
|
contextId;
|
|
31
|
+
windowMode;
|
|
32
|
+
surface;
|
|
33
|
+
siteSession;
|
|
31
34
|
_idleTimeout;
|
|
32
|
-
constructor(
|
|
35
|
+
constructor(session, idleTimeout, contextId, windowMode, surface = 'browser', siteSession) {
|
|
33
36
|
super();
|
|
34
|
-
this.
|
|
37
|
+
this.session = session;
|
|
35
38
|
this.contextId = contextId;
|
|
39
|
+
this.windowMode = windowMode;
|
|
40
|
+
this.surface = surface;
|
|
41
|
+
this.siteSession = siteSession;
|
|
36
42
|
this._idleTimeout = idleTimeout;
|
|
37
43
|
}
|
|
38
44
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
39
45
|
_page;
|
|
40
46
|
_networkCaptureUnsupported = false;
|
|
41
47
|
_networkCaptureWarned = false;
|
|
42
|
-
/** Helper: spread
|
|
43
|
-
|
|
48
|
+
/** Helper: spread session into command params */
|
|
49
|
+
_sessionOpts() {
|
|
44
50
|
return {
|
|
45
|
-
|
|
51
|
+
session: this.session,
|
|
52
|
+
surface: this.surface,
|
|
46
53
|
...(this.contextId && { contextId: this.contextId }),
|
|
47
54
|
...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
|
|
55
|
+
...(this.windowMode && { windowMode: this.windowMode }),
|
|
56
|
+
...(this.siteSession && { siteSession: this.siteSession }),
|
|
48
57
|
};
|
|
49
58
|
}
|
|
50
|
-
/** Helper: spread
|
|
59
|
+
/** Helper: spread session + page identity into command params */
|
|
51
60
|
_cmdOpts() {
|
|
52
61
|
return {
|
|
53
|
-
|
|
62
|
+
session: this.session,
|
|
63
|
+
surface: this.surface,
|
|
54
64
|
...(this.contextId && { contextId: this.contextId }),
|
|
55
65
|
...(this._page !== undefined && { page: this._page }),
|
|
56
66
|
...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
|
|
67
|
+
...(this.windowMode && { windowMode: this.windowMode }),
|
|
68
|
+
...(this.siteSession && { siteSession: this.siteSession }),
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
71
|
async goto(url, options) {
|
|
60
72
|
const result = await sendCommandFull('navigate', {
|
|
61
73
|
url,
|
|
62
74
|
...this._cmdOpts(),
|
|
63
|
-
...(options?.allowBoundNavigation === true && { allowBoundNavigation: true }),
|
|
64
75
|
});
|
|
65
76
|
// Remember the page identity (targetId) for subsequent calls
|
|
66
77
|
if (result.page) {
|
|
@@ -140,13 +151,13 @@ export class Page extends BasePage {
|
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
async getCookies(opts = {}) {
|
|
143
|
-
const result = await sendCommand('cookies', { ...this.
|
|
154
|
+
const result = await sendCommand('cookies', { ...this._sessionOpts(), ...opts });
|
|
144
155
|
return Array.isArray(result) ? result : [];
|
|
145
156
|
}
|
|
146
|
-
/** Release the current
|
|
157
|
+
/** Release the current browser session lease in the extension */
|
|
147
158
|
async closeWindow() {
|
|
148
159
|
try {
|
|
149
|
-
await sendCommand('close-window', { ...this.
|
|
160
|
+
await sendCommand('close-window', { ...this._sessionOpts() });
|
|
150
161
|
}
|
|
151
162
|
catch {
|
|
152
163
|
// Window may already be closed or daemon may be down
|
|
@@ -159,20 +170,20 @@ export class Page extends BasePage {
|
|
|
159
170
|
}
|
|
160
171
|
}
|
|
161
172
|
async tabs() {
|
|
162
|
-
const result = await sendCommand('tabs', { op: 'list', ...this.
|
|
173
|
+
const result = await sendCommand('tabs', { op: 'list', ...this._sessionOpts() });
|
|
163
174
|
return Array.isArray(result) ? result : [];
|
|
164
175
|
}
|
|
165
176
|
async newTab(url) {
|
|
166
177
|
const result = await sendCommandFull('tabs', {
|
|
167
178
|
op: 'new',
|
|
168
179
|
...(url !== undefined && { url }),
|
|
169
|
-
...this.
|
|
180
|
+
...this._sessionOpts(),
|
|
170
181
|
});
|
|
171
182
|
this._lastUrl = null;
|
|
172
183
|
return result.page;
|
|
173
184
|
}
|
|
174
185
|
async closeTab(target) {
|
|
175
|
-
const params = { op: 'close', ...this.
|
|
186
|
+
const params = { op: 'close', ...this._sessionOpts() };
|
|
176
187
|
if (typeof target === 'number')
|
|
177
188
|
params.index = target;
|
|
178
189
|
else if (typeof target === 'string')
|
|
@@ -190,7 +201,7 @@ export class Page extends BasePage {
|
|
|
190
201
|
const result = await sendCommandFull('tabs', {
|
|
191
202
|
op: 'select',
|
|
192
203
|
...(typeof target === 'number' ? { index: target } : { page: target }),
|
|
193
|
-
...this.
|
|
204
|
+
...this._sessionOpts(),
|
|
194
205
|
});
|
|
195
206
|
if (result.page)
|
|
196
207
|
this._page = result.page;
|