@jackwener/opencli 0.1.0
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/ci.yml +26 -0
- package/.github/workflows/release.yml +40 -0
- package/README.md +67 -0
- package/SKILL.md +230 -0
- package/dist/bilibili.d.ts +13 -0
- package/dist/bilibili.js +93 -0
- package/dist/browser.d.ts +48 -0
- package/dist/browser.js +261 -0
- package/dist/clis/bilibili/favorite.d.ts +1 -0
- package/dist/clis/bilibili/favorite.js +39 -0
- package/dist/clis/bilibili/feed.d.ts +1 -0
- package/dist/clis/bilibili/feed.js +64 -0
- package/dist/clis/bilibili/history.d.ts +1 -0
- package/dist/clis/bilibili/history.js +44 -0
- package/dist/clis/bilibili/me.d.ts +1 -0
- package/dist/clis/bilibili/me.js +13 -0
- package/dist/clis/bilibili/search.d.ts +1 -0
- package/dist/clis/bilibili/search.js +24 -0
- package/dist/clis/bilibili/user-videos.d.ts +1 -0
- package/dist/clis/bilibili/user-videos.js +38 -0
- package/dist/clis/github/search.d.ts +1 -0
- package/dist/clis/github/search.js +20 -0
- package/dist/clis/index.d.ts +13 -0
- package/dist/clis/index.js +16 -0
- package/dist/clis/zhihu/search.d.ts +1 -0
- package/dist/clis/zhihu/search.js +58 -0
- package/dist/engine.d.ts +6 -0
- package/dist/engine.js +77 -0
- package/dist/explore.d.ts +17 -0
- package/dist/explore.js +603 -0
- package/dist/generate.d.ts +11 -0
- package/dist/generate.js +134 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +117 -0
- package/dist/output.d.ts +11 -0
- package/dist/output.js +98 -0
- package/dist/pipeline.d.ts +9 -0
- package/dist/pipeline.js +315 -0
- package/dist/promote.d.ts +1 -0
- package/dist/promote.js +3 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +2 -0
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +42 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +27 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +2 -0
- package/dist/smoke.d.ts +2 -0
- package/dist/smoke.js +2 -0
- package/dist/snapshotFormatter.d.ts +9 -0
- package/dist/snapshotFormatter.js +41 -0
- package/dist/synthesize.d.ts +10 -0
- package/dist/synthesize.js +191 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +73 -0
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +9 -0
- package/package.json +47 -0
- package/src/bilibili.ts +111 -0
- package/src/browser.ts +260 -0
- package/src/clis/bilibili/favorite.ts +42 -0
- package/src/clis/bilibili/feed.ts +71 -0
- package/src/clis/bilibili/history.ts +48 -0
- package/src/clis/bilibili/hot.yaml +38 -0
- package/src/clis/bilibili/me.ts +14 -0
- package/src/clis/bilibili/search.ts +25 -0
- package/src/clis/bilibili/user-videos.ts +42 -0
- package/src/clis/github/search.ts +21 -0
- package/src/clis/github/trending.yaml +58 -0
- package/src/clis/hackernews/top.yaml +36 -0
- package/src/clis/index.ts +19 -0
- package/src/clis/twitter/trending.yaml +40 -0
- package/src/clis/v2ex/hot.yaml +29 -0
- package/src/clis/v2ex/latest.yaml +28 -0
- package/src/clis/zhihu/hot.yaml +28 -0
- package/src/clis/zhihu/search.ts +65 -0
- package/src/engine.ts +86 -0
- package/src/explore.ts +648 -0
- package/src/generate.ts +145 -0
- package/src/main.ts +103 -0
- package/src/output.ts +96 -0
- package/src/pipeline.ts +295 -0
- package/src/promote.ts +3 -0
- package/src/register.ts +2 -0
- package/src/registry.ts +87 -0
- package/src/runtime.ts +36 -0
- package/src/scaffold.ts +2 -0
- package/src/smoke.ts +2 -0
- package/src/snapshotFormatter.ts +51 -0
- package/src/synthesize.ts +210 -0
- package/src/validate.ts +55 -0
- package/src/verify.ts +9 -0
- package/tsconfig.json +17 -0
package/dist/browser.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
|
+
* Connects to an existing Chrome browser through the extension's stdio JSON-RPC.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn, execSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { formatSnapshot } from './snapshotFormatter.js';
|
|
11
|
+
const EXTENSION_LOCK_TIMEOUT = parseInt(process.env.OPENCLI_EXTENSION_LOCK_TIMEOUT ?? '120', 10);
|
|
12
|
+
const EXTENSION_LOCK_POLL = parseInt(process.env.OPENCLI_EXTENSION_LOCK_POLL_INTERVAL ?? '1', 10);
|
|
13
|
+
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
14
|
+
const LOCK_DIR = path.join(os.tmpdir(), 'opencli-mcp-lock');
|
|
15
|
+
// JSON-RPC helpers
|
|
16
|
+
let _nextId = 1;
|
|
17
|
+
function jsonRpcRequest(method, params = {}) {
|
|
18
|
+
return JSON.stringify({ jsonrpc: '2.0', id: _nextId++, method, params }) + '\n';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
22
|
+
*/
|
|
23
|
+
export class Page {
|
|
24
|
+
_send;
|
|
25
|
+
_recv;
|
|
26
|
+
constructor(_send, _recv) {
|
|
27
|
+
this._send = _send;
|
|
28
|
+
this._recv = _recv;
|
|
29
|
+
}
|
|
30
|
+
async call(method, params = {}) {
|
|
31
|
+
this._send(jsonRpcRequest(method, params));
|
|
32
|
+
const resp = await this._recv();
|
|
33
|
+
if (resp.error)
|
|
34
|
+
throw new Error(`page.${method}: ${resp.error.message ?? JSON.stringify(resp.error)}`);
|
|
35
|
+
// Extract text content from MCP result
|
|
36
|
+
const result = resp.result;
|
|
37
|
+
if (result?.content) {
|
|
38
|
+
const textParts = result.content.filter((c) => c.type === 'text');
|
|
39
|
+
if (textParts.length === 1) {
|
|
40
|
+
const text = textParts[0].text;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
// --- High-level methods ---
|
|
52
|
+
async goto(url) {
|
|
53
|
+
await this.call('tools/call', { name: 'browser_navigate', arguments: { url } });
|
|
54
|
+
}
|
|
55
|
+
async evaluate(js) {
|
|
56
|
+
return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: js } });
|
|
57
|
+
}
|
|
58
|
+
async snapshot(opts = {}) {
|
|
59
|
+
const raw = await this.call('tools/call', { name: 'browser_snapshot', arguments: {} });
|
|
60
|
+
if (opts.raw)
|
|
61
|
+
return raw;
|
|
62
|
+
if (typeof raw === 'string')
|
|
63
|
+
return formatSnapshot(raw, opts);
|
|
64
|
+
return raw;
|
|
65
|
+
}
|
|
66
|
+
async click(ref) {
|
|
67
|
+
await this.call('tools/call', { name: 'browser_click', arguments: { element: 'click target', ref } });
|
|
68
|
+
}
|
|
69
|
+
async typeText(ref, text) {
|
|
70
|
+
await this.call('tools/call', { name: 'browser_type', arguments: { element: 'type target', ref, text } });
|
|
71
|
+
}
|
|
72
|
+
async pressKey(key) {
|
|
73
|
+
await this.call('tools/call', { name: 'browser_press_key', arguments: { key } });
|
|
74
|
+
}
|
|
75
|
+
async wait(seconds) {
|
|
76
|
+
await this.call('tools/call', { name: 'browser_wait_for', arguments: { time: seconds } });
|
|
77
|
+
}
|
|
78
|
+
async tabs() {
|
|
79
|
+
return this.call('tools/call', { name: 'browser_tabs', arguments: { action: 'list' } });
|
|
80
|
+
}
|
|
81
|
+
async closeTab(index) {
|
|
82
|
+
await this.call('tools/call', { name: 'browser_tabs', arguments: { action: 'close', ...(index !== undefined ? { index } : {}) } });
|
|
83
|
+
}
|
|
84
|
+
async newTab() {
|
|
85
|
+
await this.call('tools/call', { name: 'browser_tabs', arguments: { action: 'new' } });
|
|
86
|
+
}
|
|
87
|
+
async selectTab(index) {
|
|
88
|
+
await this.call('tools/call', { name: 'browser_tabs', arguments: { action: 'select', index } });
|
|
89
|
+
}
|
|
90
|
+
async networkRequests(includeStatic = false) {
|
|
91
|
+
return this.call('tools/call', { name: 'browser_network_requests', arguments: { includeStatic } });
|
|
92
|
+
}
|
|
93
|
+
async consoleMessages(level = 'info') {
|
|
94
|
+
return this.call('tools/call', { name: 'browser_console_messages', arguments: { level } });
|
|
95
|
+
}
|
|
96
|
+
async scroll(direction = 'down', amount = 500) {
|
|
97
|
+
await this.call('tools/call', { name: 'browser_press_key', arguments: { key: direction === 'down' ? 'PageDown' : 'PageUp' } });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Playwright MCP process manager.
|
|
102
|
+
*/
|
|
103
|
+
export class PlaywrightMCP {
|
|
104
|
+
_proc = null;
|
|
105
|
+
_buffer = '';
|
|
106
|
+
_waiters = [];
|
|
107
|
+
_lockAcquired = false;
|
|
108
|
+
_initialTabCount = 0;
|
|
109
|
+
async connect(opts = {}) {
|
|
110
|
+
await this._acquireLock();
|
|
111
|
+
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
112
|
+
const mcpPath = findMcpServerPath();
|
|
113
|
+
if (!mcpPath)
|
|
114
|
+
throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const timer = setTimeout(() => reject(new Error(`Timed out connecting to browser (${timeout}s)`)), timeout * 1000);
|
|
117
|
+
this._proc = spawn('node', [mcpPath, '--extension'], {
|
|
118
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
|
+
env: { ...process.env, ...(process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN ? { PLAYWRIGHT_MCP_EXTENSION_TOKEN: process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN } : {}) },
|
|
120
|
+
});
|
|
121
|
+
// Increase max listeners to avoid warnings
|
|
122
|
+
this._proc.setMaxListeners(20);
|
|
123
|
+
if (this._proc.stdout)
|
|
124
|
+
this._proc.stdout.setMaxListeners(20);
|
|
125
|
+
const page = new Page((msg) => { if (this._proc?.stdin?.writable)
|
|
126
|
+
this._proc.stdin.write(msg); }, () => new Promise((res) => { this._waiters.push(res); }));
|
|
127
|
+
this._proc.stdout?.on('data', (chunk) => {
|
|
128
|
+
this._buffer += chunk.toString();
|
|
129
|
+
const lines = this._buffer.split('\n');
|
|
130
|
+
this._buffer = lines.pop() ?? '';
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (!line.trim())
|
|
133
|
+
continue;
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(line);
|
|
136
|
+
const waiter = this._waiters.shift();
|
|
137
|
+
if (waiter)
|
|
138
|
+
waiter(parsed);
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
this._proc.stderr?.on('data', () => { });
|
|
144
|
+
this._proc.on('error', (err) => { clearTimeout(timer); reject(err); });
|
|
145
|
+
// Initialize: send initialize request
|
|
146
|
+
const initMsg = jsonRpcRequest('initialize', {
|
|
147
|
+
protocolVersion: '2024-11-05',
|
|
148
|
+
capabilities: {},
|
|
149
|
+
clientInfo: { name: 'opencli', version: '0.1.0' },
|
|
150
|
+
});
|
|
151
|
+
this._proc.stdin?.write(initMsg);
|
|
152
|
+
// Wait for initialize response, then send initialized notification
|
|
153
|
+
const origRecv = () => new Promise((res) => { this._waiters.push(res); });
|
|
154
|
+
origRecv().then((resp) => {
|
|
155
|
+
if (resp.error) {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
reject(new Error(`MCP init failed: ${resp.error.message}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this._proc?.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
161
|
+
// Get initial tab count for cleanup
|
|
162
|
+
page.tabs().then((tabs) => {
|
|
163
|
+
if (typeof tabs === 'string') {
|
|
164
|
+
this._initialTabCount = (tabs.match(/Tab \d+/g) || []).length;
|
|
165
|
+
}
|
|
166
|
+
else if (Array.isArray(tabs)) {
|
|
167
|
+
this._initialTabCount = tabs.length;
|
|
168
|
+
}
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
resolve(page);
|
|
171
|
+
}).catch(() => { clearTimeout(timer); resolve(page); });
|
|
172
|
+
}).catch((err) => { clearTimeout(timer); reject(err); });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async close() {
|
|
176
|
+
try {
|
|
177
|
+
if (this._proc && !this._proc.killed) {
|
|
178
|
+
this._proc.kill('SIGTERM');
|
|
179
|
+
await new Promise((res) => { this._proc?.on('exit', () => res()); setTimeout(res, 3000); });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this._releaseLock();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async _acquireLock() {
|
|
187
|
+
const start = Date.now();
|
|
188
|
+
while (true) {
|
|
189
|
+
try {
|
|
190
|
+
fs.mkdirSync(LOCK_DIR, { recursive: false });
|
|
191
|
+
this._lockAcquired = true;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
if (e.code !== 'EEXIST')
|
|
196
|
+
throw e;
|
|
197
|
+
if ((Date.now() - start) / 1000 > EXTENSION_LOCK_TIMEOUT) {
|
|
198
|
+
// Force remove stale lock
|
|
199
|
+
try {
|
|
200
|
+
fs.rmdirSync(LOCK_DIR);
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
await new Promise(r => setTimeout(r, EXTENSION_LOCK_POLL * 1000));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
_releaseLock() {
|
|
210
|
+
if (this._lockAcquired) {
|
|
211
|
+
try {
|
|
212
|
+
fs.rmdirSync(LOCK_DIR);
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
215
|
+
this._lockAcquired = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function findMcpServerPath() {
|
|
220
|
+
// Check local node_modules first (@playwright/mcp is the modern package)
|
|
221
|
+
const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
|
|
222
|
+
if (fs.existsSync(localMcp))
|
|
223
|
+
return localMcp;
|
|
224
|
+
// Check project-relative path
|
|
225
|
+
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
226
|
+
const projectMcp = path.resolve(__dirname2, '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
|
|
227
|
+
if (fs.existsSync(projectMcp))
|
|
228
|
+
return projectMcp;
|
|
229
|
+
// Check common locations
|
|
230
|
+
const candidates = [
|
|
231
|
+
path.join(os.homedir(), '.npm', '_npx'),
|
|
232
|
+
path.join(os.homedir(), 'node_modules', '.bin'),
|
|
233
|
+
'/usr/local/lib/node_modules',
|
|
234
|
+
];
|
|
235
|
+
// Try npx resolution (legacy package name)
|
|
236
|
+
try {
|
|
237
|
+
const result = execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
238
|
+
if (result && fs.existsSync(result))
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
// Try which
|
|
243
|
+
try {
|
|
244
|
+
const result = execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
245
|
+
if (result && fs.existsSync(result))
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
// Search in common npx cache
|
|
250
|
+
for (const base of candidates) {
|
|
251
|
+
if (!fs.existsSync(base))
|
|
252
|
+
continue;
|
|
253
|
+
try {
|
|
254
|
+
const found = execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
255
|
+
if (found)
|
|
256
|
+
return found;
|
|
257
|
+
}
|
|
258
|
+
catch { }
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { apiGet, payloadData } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'bilibili',
|
|
5
|
+
name: 'favorite',
|
|
6
|
+
description: '我的默认收藏夹',
|
|
7
|
+
domain: 'www.bilibili.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
11
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'title', 'author', 'plays', 'url'],
|
|
14
|
+
func: async (page, kwargs) => {
|
|
15
|
+
const { limit = 20, page: pageNum = 1 } = kwargs;
|
|
16
|
+
// Get default favorite folder ID
|
|
17
|
+
const foldersPayload = await apiGet(page, '/x/v3/fav/folder/created/list-all', {
|
|
18
|
+
params: { up_mid: 0 },
|
|
19
|
+
signed: true,
|
|
20
|
+
});
|
|
21
|
+
const folders = payloadData(foldersPayload)?.list ?? [];
|
|
22
|
+
if (!folders.length)
|
|
23
|
+
return [];
|
|
24
|
+
const fid = folders[0].id;
|
|
25
|
+
// Fetch favorite items
|
|
26
|
+
const payload = await apiGet(page, '/x/v3/fav/resource/list', {
|
|
27
|
+
params: { media_id: fid, pn: pageNum, ps: Math.min(Number(limit), 40) },
|
|
28
|
+
signed: true,
|
|
29
|
+
});
|
|
30
|
+
const medias = payloadData(payload)?.medias ?? [];
|
|
31
|
+
return medias.slice(0, Number(limit)).map((item, i) => ({
|
|
32
|
+
rank: i + 1,
|
|
33
|
+
title: item.title ?? '',
|
|
34
|
+
author: item.upper?.name ?? '',
|
|
35
|
+
plays: item.cnt_info?.play ?? 0,
|
|
36
|
+
url: item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : '',
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { apiGet, payloadData, stripHtml } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'bilibili',
|
|
5
|
+
name: 'feed',
|
|
6
|
+
description: '关注的人的动态时间线',
|
|
7
|
+
domain: 'www.bilibili.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
11
|
+
{ name: 'type', default: 'all', help: 'Filter: all, video, article' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'author', 'title', 'type', 'url'],
|
|
14
|
+
func: async (page, kwargs) => {
|
|
15
|
+
const { limit = 20, type = 'all' } = kwargs;
|
|
16
|
+
const typeMap = { all: 'all', video: 'video', article: 'article' };
|
|
17
|
+
const updateBaseline = '';
|
|
18
|
+
const payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/all', {
|
|
19
|
+
params: {
|
|
20
|
+
timezone_offset: -480,
|
|
21
|
+
type: typeMap[type] ?? 'all',
|
|
22
|
+
page: 1,
|
|
23
|
+
...(updateBaseline ? { update_baseline: updateBaseline } : {}),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const items = payloadData(payload)?.items ?? [];
|
|
27
|
+
const rows = [];
|
|
28
|
+
for (let i = 0; i < Math.min(items.length, Number(limit)); i++) {
|
|
29
|
+
const item = items[i];
|
|
30
|
+
const modules = item.modules ?? {};
|
|
31
|
+
const authorModule = modules.module_author ?? {};
|
|
32
|
+
const dynamicModule = modules.module_dynamic ?? {};
|
|
33
|
+
const major = dynamicModule.major ?? {};
|
|
34
|
+
let title = '';
|
|
35
|
+
let url = '';
|
|
36
|
+
let itemType = item.type ?? '';
|
|
37
|
+
if (major.archive) {
|
|
38
|
+
title = major.archive.title ?? '';
|
|
39
|
+
url = major.archive.jump_url ? `https:${major.archive.jump_url}` : '';
|
|
40
|
+
itemType = 'video';
|
|
41
|
+
}
|
|
42
|
+
else if (major.article) {
|
|
43
|
+
title = major.article.title ?? '';
|
|
44
|
+
url = major.article.jump_url ? `https:${major.article.jump_url}` : '';
|
|
45
|
+
itemType = 'article';
|
|
46
|
+
}
|
|
47
|
+
else if (dynamicModule.desc) {
|
|
48
|
+
title = stripHtml(dynamicModule.desc.text ?? '').slice(0, 60);
|
|
49
|
+
url = item.id_str ? `https://t.bilibili.com/${item.id_str}` : '';
|
|
50
|
+
itemType = 'dynamic';
|
|
51
|
+
}
|
|
52
|
+
if (!title)
|
|
53
|
+
continue;
|
|
54
|
+
rows.push({
|
|
55
|
+
rank: rows.length + 1,
|
|
56
|
+
author: authorModule.name ?? '',
|
|
57
|
+
title,
|
|
58
|
+
type: itemType,
|
|
59
|
+
url,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { apiGet, payloadData } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'bilibili',
|
|
5
|
+
name: 'history',
|
|
6
|
+
description: '我的观看历史',
|
|
7
|
+
domain: 'www.bilibili.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
11
|
+
],
|
|
12
|
+
columns: ['rank', 'title', 'author', 'progress', 'url'],
|
|
13
|
+
func: async (page, kwargs) => {
|
|
14
|
+
const { limit = 20 } = kwargs;
|
|
15
|
+
const payload = await apiGet(page, '/x/web-interface/history/cursor', {
|
|
16
|
+
params: { ps: Math.min(Number(limit), 30), type: 'archive' },
|
|
17
|
+
});
|
|
18
|
+
const list = payloadData(payload)?.list ?? [];
|
|
19
|
+
return list.slice(0, Number(limit)).map((item, i) => {
|
|
20
|
+
const progress = item.progress ?? 0;
|
|
21
|
+
const duration = item.duration ?? 0;
|
|
22
|
+
let progressStr;
|
|
23
|
+
if (progress < 0 || progress >= duration) {
|
|
24
|
+
progressStr = '已看完';
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const pct = duration > 0 ? Math.round(progress / duration * 100) : 0;
|
|
28
|
+
progressStr = `${formatDuration(progress)}/${formatDuration(duration)} (${pct}%)`;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
rank: i + 1,
|
|
32
|
+
title: item.title ?? '',
|
|
33
|
+
author: item.author_name ?? '',
|
|
34
|
+
progress: progressStr,
|
|
35
|
+
url: item.history?.bvid ? `https://www.bilibili.com/video/${item.history.bvid}` : '',
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
function formatDuration(seconds) {
|
|
41
|
+
const m = Math.floor(seconds / 60);
|
|
42
|
+
const s = seconds % 60;
|
|
43
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { apiGet, getSelfUid } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'bilibili', name: 'me', description: 'My Bilibili profile info', domain: 'www.bilibili.com', strategy: Strategy.COOKIE,
|
|
5
|
+
args: [],
|
|
6
|
+
columns: ['name', 'uid', 'level', 'coins', 'followers', 'following'],
|
|
7
|
+
func: async (page) => {
|
|
8
|
+
const uid = await getSelfUid(page);
|
|
9
|
+
const payload = await apiGet(page, '/x/space/wbi/acc/info', { params: { mid: uid }, signed: true });
|
|
10
|
+
const data = payload?.data ?? {};
|
|
11
|
+
return { name: data.name ?? '', uid: data.mid ?? uid, level: data.level ?? 0, coins: data.coins ?? 0, followers: data.follower ?? 0, following: data.following ?? 0 };
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { apiGet, stripHtml } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'bilibili', name: 'search', description: 'Search Bilibili videos or users', domain: 'www.bilibili.com', strategy: Strategy.COOKIE,
|
|
5
|
+
args: [
|
|
6
|
+
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
7
|
+
{ name: 'type', default: 'video', help: 'video or user' },
|
|
8
|
+
{ name: 'page', type: 'int', default: 1, help: 'Result page' },
|
|
9
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
10
|
+
],
|
|
11
|
+
columns: ['rank', 'title', 'author', 'score', 'url'],
|
|
12
|
+
func: async (page, kwargs) => {
|
|
13
|
+
const { keyword, type = 'video', page: pageNum = 1, limit = 20 } = kwargs;
|
|
14
|
+
const searchType = type === 'user' ? 'bili_user' : 'video';
|
|
15
|
+
const payload = await apiGet(page, '/x/web-interface/wbi/search/type', { params: { search_type: searchType, keyword, page: pageNum }, signed: true });
|
|
16
|
+
const results = payload?.data?.result ?? [];
|
|
17
|
+
return results.slice(0, Number(limit)).map((item, i) => {
|
|
18
|
+
if (searchType === 'bili_user') {
|
|
19
|
+
return { rank: i + 1, title: stripHtml(item.uname ?? ''), author: (item.usign ?? '').trim(), score: item.fans ?? 0, url: item.mid ? `https://space.bilibili.com/${item.mid}` : '' };
|
|
20
|
+
}
|
|
21
|
+
return { rank: i + 1, title: stripHtml(item.title ?? ''), author: item.author ?? '', score: item.play ?? 0, url: item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : '' };
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { apiGet, payloadData, resolveUid } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'bilibili',
|
|
5
|
+
name: 'user-videos',
|
|
6
|
+
description: '查看指定用户的投稿视频',
|
|
7
|
+
domain: 'www.bilibili.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'uid', required: true, help: 'User UID or username' },
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
12
|
+
{ name: 'order', default: 'pubdate', help: 'Sort: pubdate, click, stow' },
|
|
13
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['rank', 'title', 'plays', 'likes', 'date', 'url'],
|
|
16
|
+
func: async (page, kwargs) => {
|
|
17
|
+
const { uid: uidInput, limit = 20, order = 'pubdate', page: pageNum = 1 } = kwargs;
|
|
18
|
+
const uid = await resolveUid(page, String(uidInput));
|
|
19
|
+
const payload = await apiGet(page, '/x/space/wbi/arc/search', {
|
|
20
|
+
params: {
|
|
21
|
+
mid: uid,
|
|
22
|
+
pn: pageNum,
|
|
23
|
+
ps: Math.min(Number(limit), 50),
|
|
24
|
+
order,
|
|
25
|
+
},
|
|
26
|
+
signed: true,
|
|
27
|
+
});
|
|
28
|
+
const vlist = payloadData(payload)?.list?.vlist ?? [];
|
|
29
|
+
return vlist.slice(0, Number(limit)).map((item, i) => ({
|
|
30
|
+
rank: i + 1,
|
|
31
|
+
title: item.title ?? '',
|
|
32
|
+
plays: item.play ?? 0,
|
|
33
|
+
likes: item.like ?? 0,
|
|
34
|
+
date: item.created ? new Date(item.created * 1000).toISOString().slice(0, 10) : '',
|
|
35
|
+
url: item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : '',
|
|
36
|
+
}));
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
cli({
|
|
3
|
+
site: 'github', name: 'search', description: 'Search GitHub repositories', domain: 'github.com', strategy: Strategy.PUBLIC, browser: false,
|
|
4
|
+
args: [
|
|
5
|
+
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
6
|
+
{ name: 'sort', default: 'stars', help: 'Sort by: stars, forks, updated' },
|
|
7
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
8
|
+
],
|
|
9
|
+
columns: ['rank', 'name', 'stars', 'language', 'description'],
|
|
10
|
+
func: async (_page, kwargs) => {
|
|
11
|
+
const { keyword, sort = 'stars', limit = 20 } = kwargs;
|
|
12
|
+
const resp = await fetch(`https://api.github.com/search/repositories?${new URLSearchParams({ q: keyword, sort, order: 'desc', per_page: String(Math.min(Number(limit), 100)) })}`, {
|
|
13
|
+
headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'opencli/0.1' },
|
|
14
|
+
});
|
|
15
|
+
const data = await resp.json();
|
|
16
|
+
return (data.items ?? []).slice(0, Number(limit)).map((item, i) => ({
|
|
17
|
+
rank: i + 1, name: item.full_name, stars: item.stargazers_count, language: item.language ?? '', description: (item.description ?? '').slice(0, 80),
|
|
18
|
+
}));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import all TypeScript CLI adapters so they self-register.
|
|
3
|
+
*
|
|
4
|
+
* Each TS adapter calls cli() on import, which adds itself to the global registry.
|
|
5
|
+
*/
|
|
6
|
+
import './bilibili/search.js';
|
|
7
|
+
import './bilibili/me.js';
|
|
8
|
+
import './bilibili/favorite.js';
|
|
9
|
+
import './bilibili/history.js';
|
|
10
|
+
import './bilibili/feed.js';
|
|
11
|
+
import './bilibili/user-videos.js';
|
|
12
|
+
import './github/search.js';
|
|
13
|
+
import './zhihu/search.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import all TypeScript CLI adapters so they self-register.
|
|
3
|
+
*
|
|
4
|
+
* Each TS adapter calls cli() on import, which adds itself to the global registry.
|
|
5
|
+
*/
|
|
6
|
+
// bilibili
|
|
7
|
+
import './bilibili/search.js';
|
|
8
|
+
import './bilibili/me.js';
|
|
9
|
+
import './bilibili/favorite.js';
|
|
10
|
+
import './bilibili/history.js';
|
|
11
|
+
import './bilibili/feed.js';
|
|
12
|
+
import './bilibili/user-videos.js';
|
|
13
|
+
// github
|
|
14
|
+
import './github/search.js';
|
|
15
|
+
// zhihu
|
|
16
|
+
import './zhihu/search.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { fetchJson } from '../../bilibili.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'zhihu',
|
|
5
|
+
name: 'search',
|
|
6
|
+
description: '搜索知乎问题和回答',
|
|
7
|
+
domain: 'www.zhihu.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
11
|
+
{ name: 'type', default: 'general', help: 'general, article, video' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['rank', 'title', 'author', 'type', 'url'],
|
|
15
|
+
func: async (page, kwargs) => {
|
|
16
|
+
const { keyword, type = 'general', limit = 20 } = kwargs;
|
|
17
|
+
// Navigate to zhihu to ensure cookie context
|
|
18
|
+
await page.goto('https://www.zhihu.com');
|
|
19
|
+
const qs = new URLSearchParams({ q: keyword, type, limit: String(limit) });
|
|
20
|
+
const payload = await fetchJson(page, `https://www.zhihu.com/api/v4/search_v3?${qs}`);
|
|
21
|
+
const data = payload?.data ?? [];
|
|
22
|
+
const rows = [];
|
|
23
|
+
for (let i = 0; i < Math.min(data.length, Number(limit)); i++) {
|
|
24
|
+
const item = data[i];
|
|
25
|
+
const obj = item.object ?? item;
|
|
26
|
+
const itemType = item.type ?? obj.type ?? 'unknown';
|
|
27
|
+
let title = '';
|
|
28
|
+
let author = '';
|
|
29
|
+
let url = '';
|
|
30
|
+
if (itemType === 'search_result') {
|
|
31
|
+
const highlight = obj.highlight ?? {};
|
|
32
|
+
title = (highlight.title ?? obj.title ?? '').replace(/<[^>]+>/g, '');
|
|
33
|
+
author = obj.author?.name ?? '';
|
|
34
|
+
url = obj.url ?? '';
|
|
35
|
+
}
|
|
36
|
+
else if (obj.question) {
|
|
37
|
+
title = (obj.question.title ?? obj.title ?? '').replace(/<[^>]+>/g, '');
|
|
38
|
+
author = obj.author?.name ?? '';
|
|
39
|
+
url = obj.question.url ? `https://www.zhihu.com/question/${obj.question.id}` : '';
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
title = (obj.title ?? obj.name ?? '').replace(/<[^>]+>/g, '');
|
|
43
|
+
author = obj.author?.name ?? '';
|
|
44
|
+
url = obj.url ?? '';
|
|
45
|
+
}
|
|
46
|
+
if (!title)
|
|
47
|
+
continue;
|
|
48
|
+
rows.push({
|
|
49
|
+
rank: rows.length + 1,
|
|
50
|
+
title: title.slice(0, 60),
|
|
51
|
+
author,
|
|
52
|
+
type: itemType.replace('search_result', 'result'),
|
|
53
|
+
url,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
},
|
|
58
|
+
});
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI discovery: finds YAML/TS CLI definitions and registers them.
|
|
3
|
+
*/
|
|
4
|
+
import { type CliCommand } from './registry.js';
|
|
5
|
+
export declare function discoverClis(...dirs: string[]): void;
|
|
6
|
+
export declare function executeCommand(cmd: CliCommand, page: any, kwargs: Record<string, any>, debug?: boolean): Promise<any>;
|