@jackwener/opencli 1.0.1 → 1.0.4
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/.github/workflows/build-extension.yml +80 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/docs.yml +52 -0
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +42 -34
- package/README.zh-CN.md +42 -34
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +42 -0
- package/dist/browser/cdp.js +339 -0
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +39 -123
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +49 -1
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +45 -0
- package/dist/cli-manifest.json +885 -48
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/twitter/post.js +9 -2
- package/dist/clis/twitter/search.js +14 -33
- package/dist/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +4 -4
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +25 -14
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +48 -103
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/main.js +4 -193
- package/dist/output.d.ts +2 -1
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +4 -3
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/synthesize.js +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +386 -438
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +124 -53
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +367 -0
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/index.ts +4 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +47 -124
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +56 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +53 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +205 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/twitter/post.ts +9 -2
- package/src/clis/twitter/search.ts +15 -33
- package/src/clis/xiaohongshu/download.ts +4 -4
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +20 -13
- package/src/explore.ts +54 -103
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/main.ts +4 -180
- package/src/output.ts +15 -13
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +9 -5
- package/src/runtime.ts +3 -2
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/synthesize.ts +1 -1
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
type Listener<T extends (...args: any[]) => void> = { addListener: (fn: T) => void };
|
|
4
|
+
|
|
5
|
+
type MockTab = {
|
|
6
|
+
id: number;
|
|
7
|
+
windowId: number;
|
|
8
|
+
url?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
active?: boolean;
|
|
11
|
+
status?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
class MockWebSocket {
|
|
15
|
+
static OPEN = 1;
|
|
16
|
+
static CONNECTING = 0;
|
|
17
|
+
readyState = MockWebSocket.CONNECTING;
|
|
18
|
+
onopen: (() => void) | null = null;
|
|
19
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
20
|
+
onclose: (() => void) | null = null;
|
|
21
|
+
onerror: (() => void) | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(_url: string) {}
|
|
24
|
+
send(_data: string): void {}
|
|
25
|
+
close(): void {
|
|
26
|
+
this.onclose?.();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createChromeMock() {
|
|
31
|
+
let nextTabId = 10;
|
|
32
|
+
const tabs: MockTab[] = [
|
|
33
|
+
{ id: 1, windowId: 1, url: 'https://automation.example', title: 'automation', active: true, status: 'complete' },
|
|
34
|
+
{ id: 2, windowId: 2, url: 'https://user.example', title: 'user', active: true, status: 'complete' },
|
|
35
|
+
{ id: 3, windowId: 1, url: 'chrome://extensions', title: 'chrome', active: false, status: 'complete' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const query = vi.fn(async (queryInfo: { windowId?: number } = {}) => {
|
|
39
|
+
return tabs.filter((tab) => queryInfo.windowId === undefined || tab.windowId === queryInfo.windowId);
|
|
40
|
+
});
|
|
41
|
+
const create = vi.fn(async ({ windowId, url, active }: { windowId?: number; url?: string; active?: boolean }) => {
|
|
42
|
+
const tab: MockTab = {
|
|
43
|
+
id: nextTabId++,
|
|
44
|
+
windowId: windowId ?? 999,
|
|
45
|
+
url,
|
|
46
|
+
title: url ?? 'blank',
|
|
47
|
+
active: !!active,
|
|
48
|
+
status: 'complete',
|
|
49
|
+
};
|
|
50
|
+
tabs.push(tab);
|
|
51
|
+
return tab;
|
|
52
|
+
});
|
|
53
|
+
const update = vi.fn(async (tabId: number, updates: { active?: boolean; url?: string }) => {
|
|
54
|
+
const tab = tabs.find((entry) => entry.id === tabId);
|
|
55
|
+
if (!tab) throw new Error(`Unknown tab ${tabId}`);
|
|
56
|
+
if (updates.active !== undefined) tab.active = updates.active;
|
|
57
|
+
if (updates.url !== undefined) tab.url = updates.url;
|
|
58
|
+
return tab;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const chrome = {
|
|
62
|
+
tabs: {
|
|
63
|
+
query,
|
|
64
|
+
create,
|
|
65
|
+
update,
|
|
66
|
+
remove: vi.fn(async (_tabId: number) => {}),
|
|
67
|
+
get: vi.fn(async (tabId: number) => {
|
|
68
|
+
const tab = tabs.find((entry) => entry.id === tabId);
|
|
69
|
+
if (!tab) throw new Error(`Unknown tab ${tabId}`);
|
|
70
|
+
return tab;
|
|
71
|
+
}),
|
|
72
|
+
onUpdated: { addListener: vi.fn(), removeListener: vi.fn() } as Listener<(id: number, info: chrome.tabs.TabChangeInfo) => void>,
|
|
73
|
+
},
|
|
74
|
+
windows: {
|
|
75
|
+
get: vi.fn(async (windowId: number) => ({ id: windowId })),
|
|
76
|
+
create: vi.fn(async ({ url, focused, width, height, type }: any) => ({ id: 1, url, focused, width, height, type })),
|
|
77
|
+
remove: vi.fn(async (_windowId: number) => {}),
|
|
78
|
+
onRemoved: { addListener: vi.fn() } as Listener<(windowId: number) => void>,
|
|
79
|
+
},
|
|
80
|
+
alarms: {
|
|
81
|
+
create: vi.fn(),
|
|
82
|
+
onAlarm: { addListener: vi.fn() } as Listener<(alarm: { name: string }) => void>,
|
|
83
|
+
},
|
|
84
|
+
runtime: {
|
|
85
|
+
onInstalled: { addListener: vi.fn() } as Listener<() => void>,
|
|
86
|
+
onStartup: { addListener: vi.fn() } as Listener<() => void>,
|
|
87
|
+
},
|
|
88
|
+
cookies: {
|
|
89
|
+
getAll: vi.fn(async () => []),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return { chrome, tabs, query, create, update };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('background tab isolation', () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.resetModules();
|
|
99
|
+
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('lists only automation-window web tabs', async () => {
|
|
103
|
+
const { chrome } = createChromeMock();
|
|
104
|
+
vi.stubGlobal('chrome', chrome);
|
|
105
|
+
|
|
106
|
+
const mod = await import('./background');
|
|
107
|
+
mod.__test__.setAutomationWindowId('site:twitter', 1);
|
|
108
|
+
|
|
109
|
+
const result = await mod.__test__.handleTabs({ id: '1', action: 'tabs', op: 'list', workspace: 'site:twitter' }, 'site:twitter');
|
|
110
|
+
|
|
111
|
+
expect(result.ok).toBe(true);
|
|
112
|
+
expect(result.data).toEqual([
|
|
113
|
+
{
|
|
114
|
+
index: 0,
|
|
115
|
+
tabId: 1,
|
|
116
|
+
url: 'https://automation.example',
|
|
117
|
+
title: 'automation',
|
|
118
|
+
active: true,
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('creates new tabs inside the automation window', async () => {
|
|
124
|
+
const { chrome, create } = createChromeMock();
|
|
125
|
+
vi.stubGlobal('chrome', chrome);
|
|
126
|
+
|
|
127
|
+
const mod = await import('./background');
|
|
128
|
+
mod.__test__.setAutomationWindowId('site:twitter', 1);
|
|
129
|
+
|
|
130
|
+
const result = await mod.__test__.handleTabs({ id: '2', action: 'tabs', op: 'new', url: 'https://new.example', workspace: 'site:twitter' }, 'site:twitter');
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(true);
|
|
133
|
+
expect(create).toHaveBeenCalledWith({ windowId: 1, url: 'https://new.example', active: true });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('reports sessions per workspace', async () => {
|
|
137
|
+
const { chrome } = createChromeMock();
|
|
138
|
+
vi.stubGlobal('chrome', chrome);
|
|
139
|
+
|
|
140
|
+
const mod = await import('./background');
|
|
141
|
+
mod.__test__.setAutomationWindowId('site:twitter', 1);
|
|
142
|
+
mod.__test__.setAutomationWindowId('site:zhihu', 2);
|
|
143
|
+
|
|
144
|
+
const result = await mod.__test__.handleSessions({ id: '3', action: 'sessions' });
|
|
145
|
+
expect(result.ok).toBe(true);
|
|
146
|
+
expect(result.data).toEqual(expect.arrayContaining([
|
|
147
|
+
expect.objectContaining({ workspace: 'site:twitter', windowId: 1 }),
|
|
148
|
+
expect.objectContaining({ workspace: 'site:zhihu', windowId: 2 }),
|
|
149
|
+
]));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* OpenCLI — Service Worker (background script).
|
|
3
3
|
*
|
|
4
4
|
* Connects to the opencli daemon via WebSocket, receives commands,
|
|
5
5
|
* dispatches them to Chrome APIs (debugger/tabs/cookies), returns results.
|
|
@@ -90,36 +90,48 @@ function scheduleReconnect(): void {
|
|
|
90
90
|
// user's active browsing session is never touched.
|
|
91
91
|
// The window auto-closes after 30s of idle (no commands).
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
type AutomationSession = {
|
|
94
|
+
windowId: number;
|
|
95
|
+
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
96
|
+
idleDeadlineAt: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const automationSessions = new Map<string, AutomationSession>();
|
|
95
100
|
const WINDOW_IDLE_TIMEOUT = 30000; // 30s
|
|
96
101
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
function getWorkspaceKey(workspace?: string): string {
|
|
103
|
+
return workspace?.trim() || 'default';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resetWindowIdleTimer(workspace: string): void {
|
|
107
|
+
const session = automationSessions.get(workspace);
|
|
108
|
+
if (!session) return;
|
|
109
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
110
|
+
session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
|
|
111
|
+
session.idleTimer = setTimeout(async () => {
|
|
112
|
+
const current = automationSessions.get(workspace);
|
|
113
|
+
if (!current) return;
|
|
114
|
+
try {
|
|
115
|
+
await chrome.windows.remove(current.windowId);
|
|
116
|
+
console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
|
|
117
|
+
} catch {
|
|
118
|
+
// Already gone
|
|
108
119
|
}
|
|
109
|
-
|
|
120
|
+
automationSessions.delete(workspace);
|
|
110
121
|
}, WINDOW_IDLE_TIMEOUT);
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
/** Get or create the dedicated automation window. */
|
|
114
|
-
async function getAutomationWindow(): Promise<number> {
|
|
125
|
+
async function getAutomationWindow(workspace: string): Promise<number> {
|
|
115
126
|
// Check if our window is still alive
|
|
116
|
-
|
|
127
|
+
const existing = automationSessions.get(workspace);
|
|
128
|
+
if (existing) {
|
|
117
129
|
try {
|
|
118
|
-
await chrome.windows.get(
|
|
119
|
-
return
|
|
130
|
+
await chrome.windows.get(existing.windowId);
|
|
131
|
+
return existing.windowId;
|
|
120
132
|
} catch {
|
|
121
133
|
// Window was closed by user
|
|
122
|
-
|
|
134
|
+
automationSessions.delete(workspace);
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -131,17 +143,25 @@ async function getAutomationWindow(): Promise<number> {
|
|
|
131
143
|
height: 900,
|
|
132
144
|
type: 'normal',
|
|
133
145
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
const session: AutomationSession = {
|
|
147
|
+
windowId: win.id!,
|
|
148
|
+
idleTimer: null,
|
|
149
|
+
idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
|
|
150
|
+
};
|
|
151
|
+
automationSessions.set(workspace, session);
|
|
152
|
+
console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
|
|
153
|
+
resetWindowIdleTimer(workspace);
|
|
154
|
+
return session.windowId;
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
// Clean up when the automation window is closed
|
|
140
158
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
for (const [workspace, session] of automationSessions.entries()) {
|
|
160
|
+
if (session.windowId === windowId) {
|
|
161
|
+
console.log(`[opencli] Automation window closed (${workspace})`);
|
|
162
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
163
|
+
automationSessions.delete(workspace);
|
|
164
|
+
}
|
|
145
165
|
}
|
|
146
166
|
});
|
|
147
167
|
|
|
@@ -155,7 +175,7 @@ function initialize(): void {
|
|
|
155
175
|
chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
|
|
156
176
|
executor.registerListeners();
|
|
157
177
|
connect();
|
|
158
|
-
console.log('[opencli]
|
|
178
|
+
console.log('[opencli] OpenCLI extension initialized');
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
chrome.runtime.onInstalled.addListener(() => {
|
|
@@ -173,22 +193,25 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
173
193
|
// ─── Command dispatcher ─────────────────────────────────────────────
|
|
174
194
|
|
|
175
195
|
async function handleCommand(cmd: Command): Promise<Result> {
|
|
196
|
+
const workspace = getWorkspaceKey(cmd.workspace);
|
|
176
197
|
// Reset idle timer on every command (window stays alive while active)
|
|
177
|
-
resetWindowIdleTimer();
|
|
198
|
+
resetWindowIdleTimer(workspace);
|
|
178
199
|
try {
|
|
179
200
|
switch (cmd.action) {
|
|
180
201
|
case 'exec':
|
|
181
|
-
return await handleExec(cmd);
|
|
202
|
+
return await handleExec(cmd, workspace);
|
|
182
203
|
case 'navigate':
|
|
183
|
-
return await handleNavigate(cmd);
|
|
204
|
+
return await handleNavigate(cmd, workspace);
|
|
184
205
|
case 'tabs':
|
|
185
|
-
return await handleTabs(cmd);
|
|
206
|
+
return await handleTabs(cmd, workspace);
|
|
186
207
|
case 'cookies':
|
|
187
208
|
return await handleCookies(cmd);
|
|
188
209
|
case 'screenshot':
|
|
189
|
-
return await handleScreenshot(cmd);
|
|
210
|
+
return await handleScreenshot(cmd, workspace);
|
|
190
211
|
case 'close-window':
|
|
191
|
-
return await handleCloseWindow(cmd);
|
|
212
|
+
return await handleCloseWindow(cmd, workspace);
|
|
213
|
+
case 'sessions':
|
|
214
|
+
return await handleSessions(cmd);
|
|
192
215
|
default:
|
|
193
216
|
return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
|
|
194
217
|
}
|
|
@@ -214,11 +237,11 @@ function isWebUrl(url?: string): boolean {
|
|
|
214
237
|
* If explicit tabId is given, use that directly.
|
|
215
238
|
* Otherwise, find or create a tab in the dedicated automation window.
|
|
216
239
|
*/
|
|
217
|
-
async function resolveTabId(tabId
|
|
240
|
+
async function resolveTabId(tabId: number | undefined, workspace: string): Promise<number> {
|
|
218
241
|
if (tabId !== undefined) return tabId;
|
|
219
242
|
|
|
220
243
|
// Get (or create) the automation window
|
|
221
|
-
const windowId = await getAutomationWindow();
|
|
244
|
+
const windowId = await getAutomationWindow(workspace);
|
|
222
245
|
|
|
223
246
|
// Find the active tab in our automation window
|
|
224
247
|
const tabs = await chrome.tabs.query({ windowId });
|
|
@@ -234,9 +257,25 @@ async function resolveTabId(tabId?: number): Promise<number> {
|
|
|
234
257
|
return newTab.id;
|
|
235
258
|
}
|
|
236
259
|
|
|
237
|
-
async function
|
|
260
|
+
async function listAutomationTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
|
|
261
|
+
const session = automationSessions.get(workspace);
|
|
262
|
+
if (!session) return [];
|
|
263
|
+
try {
|
|
264
|
+
return await chrome.tabs.query({ windowId: session.windowId });
|
|
265
|
+
} catch {
|
|
266
|
+
automationSessions.delete(workspace);
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function listAutomationWebTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
|
|
272
|
+
const tabs = await listAutomationTabs(workspace);
|
|
273
|
+
return tabs.filter((tab) => isWebUrl(tab.url));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function handleExec(cmd: Command, workspace: string): Promise<Result> {
|
|
238
277
|
if (!cmd.code) return { id: cmd.id, ok: false, error: 'Missing code' };
|
|
239
|
-
const tabId = await resolveTabId(cmd.tabId);
|
|
278
|
+
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
240
279
|
try {
|
|
241
280
|
const data = await executor.evaluateAsync(tabId, cmd.code);
|
|
242
281
|
return { id: cmd.id, ok: true, data };
|
|
@@ -245,9 +284,9 @@ async function handleExec(cmd: Command): Promise<Result> {
|
|
|
245
284
|
}
|
|
246
285
|
}
|
|
247
286
|
|
|
248
|
-
async function handleNavigate(cmd: Command): Promise<Result> {
|
|
287
|
+
async function handleNavigate(cmd: Command, workspace: string): Promise<Result> {
|
|
249
288
|
if (!cmd.url) return { id: cmd.id, ok: false, error: 'Missing url' };
|
|
250
|
-
const tabId = await resolveTabId(cmd.tabId);
|
|
289
|
+
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
251
290
|
await chrome.tabs.update(tabId, { url: cmd.url });
|
|
252
291
|
|
|
253
292
|
// Wait for page to finish loading, checking current status first to avoid race
|
|
@@ -275,12 +314,11 @@ async function handleNavigate(cmd: Command): Promise<Result> {
|
|
|
275
314
|
return { id: cmd.id, ok: true, data: { title: tab.title, url: tab.url, tabId } };
|
|
276
315
|
}
|
|
277
316
|
|
|
278
|
-
async function handleTabs(cmd: Command): Promise<Result> {
|
|
317
|
+
async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
|
|
279
318
|
switch (cmd.op) {
|
|
280
319
|
case 'list': {
|
|
281
|
-
const tabs = await
|
|
320
|
+
const tabs = await listAutomationWebTabs(workspace);
|
|
282
321
|
const data = tabs
|
|
283
|
-
.filter((t) => isWebUrl(t.url))
|
|
284
322
|
.map((t, i) => ({
|
|
285
323
|
index: i,
|
|
286
324
|
tabId: t.id,
|
|
@@ -291,19 +329,20 @@ async function handleTabs(cmd: Command): Promise<Result> {
|
|
|
291
329
|
return { id: cmd.id, ok: true, data };
|
|
292
330
|
}
|
|
293
331
|
case 'new': {
|
|
294
|
-
const
|
|
332
|
+
const windowId = await getAutomationWindow(workspace);
|
|
333
|
+
const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? 'about:blank', active: true });
|
|
295
334
|
return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
|
|
296
335
|
}
|
|
297
336
|
case 'close': {
|
|
298
337
|
if (cmd.index !== undefined) {
|
|
299
|
-
const tabs = await
|
|
338
|
+
const tabs = await listAutomationWebTabs(workspace);
|
|
300
339
|
const target = tabs[cmd.index];
|
|
301
340
|
if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
|
|
302
341
|
await chrome.tabs.remove(target.id);
|
|
303
342
|
executor.detach(target.id);
|
|
304
343
|
return { id: cmd.id, ok: true, data: { closed: target.id } };
|
|
305
344
|
}
|
|
306
|
-
const tabId = await resolveTabId(cmd.tabId);
|
|
345
|
+
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
307
346
|
await chrome.tabs.remove(tabId);
|
|
308
347
|
executor.detach(tabId);
|
|
309
348
|
return { id: cmd.id, ok: true, data: { closed: tabId } };
|
|
@@ -315,7 +354,7 @@ async function handleTabs(cmd: Command): Promise<Result> {
|
|
|
315
354
|
await chrome.tabs.update(cmd.tabId, { active: true });
|
|
316
355
|
return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
|
|
317
356
|
}
|
|
318
|
-
const tabs = await
|
|
357
|
+
const tabs = await listAutomationWebTabs(workspace);
|
|
319
358
|
const target = tabs[cmd.index!];
|
|
320
359
|
if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
|
|
321
360
|
await chrome.tabs.update(target.id, { active: true });
|
|
@@ -343,8 +382,8 @@ async function handleCookies(cmd: Command): Promise<Result> {
|
|
|
343
382
|
return { id: cmd.id, ok: true, data };
|
|
344
383
|
}
|
|
345
384
|
|
|
346
|
-
async function handleScreenshot(cmd: Command): Promise<Result> {
|
|
347
|
-
const tabId = await resolveTabId(cmd.tabId);
|
|
385
|
+
async function handleScreenshot(cmd: Command, workspace: string): Promise<Result> {
|
|
386
|
+
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
348
387
|
try {
|
|
349
388
|
const data = await executor.screenshot(tabId, {
|
|
350
389
|
format: cmd.format,
|
|
@@ -357,14 +396,46 @@ async function handleScreenshot(cmd: Command): Promise<Result> {
|
|
|
357
396
|
}
|
|
358
397
|
}
|
|
359
398
|
|
|
360
|
-
async function handleCloseWindow(cmd: Command): Promise<Result> {
|
|
361
|
-
|
|
399
|
+
async function handleCloseWindow(cmd: Command, workspace: string): Promise<Result> {
|
|
400
|
+
const session = automationSessions.get(workspace);
|
|
401
|
+
if (session) {
|
|
362
402
|
try {
|
|
363
|
-
await chrome.windows.remove(
|
|
403
|
+
await chrome.windows.remove(session.windowId);
|
|
364
404
|
} catch {
|
|
365
405
|
// Window may already be closed
|
|
366
406
|
}
|
|
367
|
-
|
|
407
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
408
|
+
automationSessions.delete(workspace);
|
|
368
409
|
}
|
|
369
410
|
return { id: cmd.id, ok: true, data: { closed: true } };
|
|
370
411
|
}
|
|
412
|
+
|
|
413
|
+
async function handleSessions(cmd: Command): Promise<Result> {
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
|
|
416
|
+
workspace,
|
|
417
|
+
windowId: session.windowId,
|
|
418
|
+
tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isWebUrl(tab.url)).length,
|
|
419
|
+
idleMsRemaining: Math.max(0, session.idleDeadlineAt - now),
|
|
420
|
+
})));
|
|
421
|
+
return { id: cmd.id, ok: true, data };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export const __test__ = {
|
|
425
|
+
handleTabs,
|
|
426
|
+
handleSessions,
|
|
427
|
+
getAutomationWindowId: (workspace: string = 'default') => automationSessions.get(workspace)?.windowId ?? null,
|
|
428
|
+
setAutomationWindowId: (workspace: string, windowId: number | null) => {
|
|
429
|
+
if (windowId === null) {
|
|
430
|
+
const session = automationSessions.get(workspace);
|
|
431
|
+
if (session?.idleTimer) clearTimeout(session.idleTimer);
|
|
432
|
+
automationSessions.delete(workspace);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
automationSessions.set(workspace, {
|
|
436
|
+
windowId,
|
|
437
|
+
idleTimer: null,
|
|
438
|
+
idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Everything else is just JS code sent via 'exec'.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
|
|
8
|
+
export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions';
|
|
9
9
|
|
|
10
10
|
export interface Command {
|
|
11
11
|
/** Unique request ID */
|
|
@@ -16,6 +16,8 @@ export interface Command {
|
|
|
16
16
|
tabId?: number;
|
|
17
17
|
/** JS code to evaluate in page context (exec action) */
|
|
18
18
|
code?: string;
|
|
19
|
+
/** Logical workspace for automation session reuse */
|
|
20
|
+
workspace?: string;
|
|
19
21
|
/** URL to navigate to (navigate action) */
|
|
20
22
|
url?: string;
|
|
21
23
|
/** Sub-operation for tabs: list, new, close, select */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"test": "vitest run",
|
|
28
28
|
"test:site": "node scripts/test-site.mjs",
|
|
29
|
-
"test:watch": "vitest"
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"docs:dev": "vitepress dev docs",
|
|
31
|
+
"docs:build": "vitepress build docs",
|
|
32
|
+
"docs:preview": "vitepress preview docs"
|
|
30
33
|
},
|
|
31
34
|
"keywords": [
|
|
32
35
|
"cli",
|
|
@@ -49,10 +52,11 @@
|
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"@types/js-yaml": "^4.0.9",
|
|
52
|
-
"@types/ws": "^8.5.13",
|
|
53
55
|
"@types/node": "^22.13.10",
|
|
56
|
+
"@types/ws": "^8.5.13",
|
|
54
57
|
"tsx": "^4.19.3",
|
|
55
58
|
"typescript": "^5.8.2",
|
|
59
|
+
"vitepress": "^1.6.4",
|
|
56
60
|
"vitest": "^4.1.0"
|
|
57
61
|
}
|
|
58
62
|
}
|