@jackwener/opencli 1.5.0 → 1.5.1
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/dist/browser/cdp.js +5 -0
- package/dist/browser/page.d.ts +3 -0
- package/dist/browser/page.js +24 -1
- package/dist/cli-manifest.json +465 -5
- package/dist/cli.js +34 -3
- package/dist/clis/bluesky/feeds.yaml +29 -0
- package/dist/clis/bluesky/followers.yaml +33 -0
- package/dist/clis/bluesky/following.yaml +33 -0
- package/dist/clis/bluesky/profile.yaml +27 -0
- package/dist/clis/bluesky/search.yaml +34 -0
- package/dist/clis/bluesky/starter-packs.yaml +34 -0
- package/dist/clis/bluesky/thread.yaml +32 -0
- package/dist/clis/bluesky/trending.yaml +27 -0
- package/dist/clis/bluesky/user.yaml +34 -0
- package/dist/clis/twitter/trending.js +29 -61
- package/dist/clis/v2ex/hot.yaml +17 -3
- package/dist/clis/xiaohongshu/publish.js +78 -42
- package/dist/clis/xiaohongshu/publish.test.js +20 -8
- package/dist/clis/xiaohongshu/search.d.ts +8 -1
- package/dist/clis/xiaohongshu/search.js +20 -1
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
- package/dist/clis/xiaohongshu/search.test.js +32 -1
- package/dist/discovery.js +40 -28
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +2 -2
- package/dist/engine.test.js +42 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/execution.js +45 -7
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/external.js +6 -1
- package/dist/main.js +1 -0
- package/dist/plugin-scaffold.d.ts +28 -0
- package/dist/plugin-scaffold.js +142 -0
- package/dist/plugin-scaffold.test.d.ts +4 -0
- package/dist/plugin-scaffold.test.js +83 -0
- package/dist/plugin.d.ts +55 -17
- package/dist/plugin.js +706 -154
- package/dist/plugin.test.js +836 -38
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1 -1
- package/dist/types.d.ts +2 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/guide/plugins.md +10 -0
- package/package.json +1 -1
- package/src/browser/cdp.ts +6 -0
- package/src/browser/page.ts +24 -1
- package/src/cli.ts +34 -3
- package/src/clis/bluesky/feeds.yaml +29 -0
- package/src/clis/bluesky/followers.yaml +33 -0
- package/src/clis/bluesky/following.yaml +33 -0
- package/src/clis/bluesky/profile.yaml +27 -0
- package/src/clis/bluesky/search.yaml +34 -0
- package/src/clis/bluesky/starter-packs.yaml +34 -0
- package/src/clis/bluesky/thread.yaml +32 -0
- package/src/clis/bluesky/trending.yaml +27 -0
- package/src/clis/bluesky/user.yaml +34 -0
- package/src/clis/twitter/trending.ts +29 -77
- package/src/clis/v2ex/hot.yaml +17 -3
- package/src/clis/xiaohongshu/publish.test.ts +22 -8
- package/src/clis/xiaohongshu/publish.ts +93 -52
- package/src/clis/xiaohongshu/search.test.ts +39 -1
- package/src/clis/xiaohongshu/search.ts +19 -1
- package/src/discovery.ts +41 -33
- package/src/doctor.ts +2 -3
- package/src/engine.test.ts +38 -0
- package/src/errors.ts +6 -2
- package/src/execution.test.ts +47 -0
- package/src/execution.ts +39 -6
- package/src/external.ts +6 -1
- package/src/main.ts +1 -0
- package/src/plugin-scaffold.test.ts +98 -0
- package/src/plugin-scaffold.ts +170 -0
- package/src/plugin.test.ts +881 -38
- package/src/plugin.ts +871 -158
- package/src/runtime.ts +2 -2
- package/src/types.ts +2 -0
- package/tests/e2e/browser-public.test.ts +1 -1
package/dist/runtime.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare const DEFAULT_BROWSER_EXPLORE_TIMEOUT: number;
|
|
|
13
13
|
export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
|
|
14
14
|
timeout: number;
|
|
15
15
|
label?: string;
|
|
16
|
+
hint?: string;
|
|
16
17
|
}): Promise<T>;
|
|
17
18
|
/**
|
|
18
19
|
* Timeout with milliseconds unit. Used for low-level internal timeouts.
|
package/dist/runtime.js
CHANGED
|
@@ -26,7 +26,7 @@ export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_
|
|
|
26
26
|
*/
|
|
27
27
|
export async function runWithTimeout(promise, opts) {
|
|
28
28
|
const label = opts.label ?? 'Operation';
|
|
29
|
-
return withTimeoutMs(promise, opts.timeout * 1000, () => new TimeoutError(label, opts.timeout));
|
|
29
|
+
return withTimeoutMs(promise, opts.timeout * 1000, () => new TimeoutError(label, opts.timeout, opts.hint));
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Timeout with milliseconds unit. Used for low-level internal timeouts.
|
package/dist/types.d.ts
CHANGED
|
@@ -69,4 +69,6 @@ export interface IPage {
|
|
|
69
69
|
getInterceptedRequests(): Promise<any[]>;
|
|
70
70
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
71
71
|
closeWindow?(): Promise<void>;
|
|
72
|
+
/** Returns the current page URL, or null if unavailable. */
|
|
73
|
+
getCurrentUrl?(): Promise<string | null>;
|
|
72
74
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Bluesky
|
|
2
|
+
|
|
3
|
+
**Mode**: 🌐 Public · **Domain**: `bsky.app`
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | Description |
|
|
8
|
+
|---------|-------------|
|
|
9
|
+
| `opencli bluesky profile` | User profile info |
|
|
10
|
+
| `opencli bluesky user` | Recent posts from a user |
|
|
11
|
+
| `opencli bluesky trending` | Trending topics |
|
|
12
|
+
| `opencli bluesky search` | Search users |
|
|
13
|
+
| `opencli bluesky feeds` | Popular feed generators |
|
|
14
|
+
| `opencli bluesky followers` | User's followers |
|
|
15
|
+
| `opencli bluesky following` | Accounts a user follows |
|
|
16
|
+
| `opencli bluesky thread` | Post thread with replies |
|
|
17
|
+
| `opencli bluesky starter-packs` | User's starter packs |
|
|
18
|
+
|
|
19
|
+
## Usage Examples
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# User profile
|
|
23
|
+
opencli bluesky profile --handle bsky.app
|
|
24
|
+
|
|
25
|
+
# Recent posts
|
|
26
|
+
opencli bluesky user --handle bsky.app --limit 10
|
|
27
|
+
|
|
28
|
+
# Trending topics
|
|
29
|
+
opencli bluesky trending --limit 10
|
|
30
|
+
|
|
31
|
+
# Search users
|
|
32
|
+
opencli bluesky search --query "AI" --limit 10
|
|
33
|
+
|
|
34
|
+
# Popular feeds
|
|
35
|
+
opencli bluesky feeds --limit 10
|
|
36
|
+
|
|
37
|
+
# Followers / following
|
|
38
|
+
opencli bluesky followers --handle bsky.app --limit 10
|
|
39
|
+
opencli bluesky following --handle bsky.app
|
|
40
|
+
|
|
41
|
+
# Post thread with replies
|
|
42
|
+
opencli bluesky thread --uri "at://did:.../app.bsky.feed.post/..."
|
|
43
|
+
|
|
44
|
+
# Starter packs
|
|
45
|
+
opencli bluesky starter-packs --handle bsky.app
|
|
46
|
+
|
|
47
|
+
# JSON output
|
|
48
|
+
opencli bluesky profile --handle bsky.app -f json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Prerequisites
|
|
52
|
+
|
|
53
|
+
None — all commands use the public Bluesky AT Protocol API, no browser or login required.
|
package/docs/guide/plugins.md
CHANGED
|
@@ -31,9 +31,19 @@ Plugins live in `~/.opencli/plugins/<name>/`. Each subdirectory is scanned at st
|
|
|
31
31
|
### Supported Source Formats
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
+
# GitHub shorthand
|
|
34
35
|
opencli plugin install github:user/repo
|
|
35
36
|
opencli plugin install github:user/repo/subplugin # install specific sub-plugin from monorepo
|
|
36
37
|
opencli plugin install https://github.com/user/repo
|
|
38
|
+
|
|
39
|
+
# Any git-cloneable URL
|
|
40
|
+
opencli plugin install https://gitlab.example.com/team/repo.git
|
|
41
|
+
opencli plugin install ssh://git@gitlab.example.com/team/repo.git
|
|
42
|
+
opencli plugin install git@gitlab.example.com:team/repo.git
|
|
43
|
+
|
|
44
|
+
# Local plugin (for development)
|
|
45
|
+
opencli plugin install file:///path/to/plugin
|
|
46
|
+
opencli plugin install /path/to/plugin
|
|
37
47
|
```
|
|
38
48
|
|
|
39
49
|
The repo name prefix `opencli-plugin-` is automatically stripped for the local directory name. For example, `opencli-plugin-hot-digest` becomes `hot-digest`.
|
package/package.json
CHANGED
package/src/browser/cdp.ts
CHANGED
|
@@ -173,6 +173,7 @@ export class CDPBridge implements IBrowserFactory {
|
|
|
173
173
|
|
|
174
174
|
class CDPPage implements IPage {
|
|
175
175
|
private _pageEnabled = false;
|
|
176
|
+
private _lastUrl: string | null = null;
|
|
176
177
|
constructor(private bridge: CDPBridge) {}
|
|
177
178
|
|
|
178
179
|
async goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void> {
|
|
@@ -183,6 +184,7 @@ class CDPPage implements IPage {
|
|
|
183
184
|
const loadPromise = this.bridge.waitForEvent('Page.loadEventFired', 30_000).catch(() => {});
|
|
184
185
|
await this.bridge.send('Page.navigate', { url });
|
|
185
186
|
await loadPromise;
|
|
187
|
+
this._lastUrl = url;
|
|
186
188
|
if (options?.waitUntil !== 'none') {
|
|
187
189
|
const maxMs = options?.settleMs ?? 1000;
|
|
188
190
|
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
@@ -307,6 +309,10 @@ class CDPPage implements IPage {
|
|
|
307
309
|
return [];
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
async getCurrentUrl(): Promise<string | null> {
|
|
313
|
+
return this._lastUrl;
|
|
314
|
+
}
|
|
315
|
+
|
|
310
316
|
async installInterceptor(pattern: string): Promise<void> {
|
|
311
317
|
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
312
318
|
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
package/src/browser/page.ts
CHANGED
|
@@ -36,6 +36,8 @@ export class Page implements IPage {
|
|
|
36
36
|
|
|
37
37
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
38
38
|
private _tabId: number | undefined;
|
|
39
|
+
/** Last navigated URL, tracked in-memory to avoid extra round-trips */
|
|
40
|
+
private _lastUrl: string | null = null;
|
|
39
41
|
|
|
40
42
|
/** Helper: spread workspace into command params */
|
|
41
43
|
private _wsOpt(): { workspace: string } {
|
|
@@ -55,10 +57,11 @@ export class Page implements IPage {
|
|
|
55
57
|
url,
|
|
56
58
|
...this._cmdOpts(),
|
|
57
59
|
}) as { tabId?: number };
|
|
58
|
-
// Remember the tabId for subsequent
|
|
60
|
+
// Remember the tabId and URL for subsequent calls
|
|
59
61
|
if (result?.tabId) {
|
|
60
62
|
this._tabId = result.tabId;
|
|
61
63
|
}
|
|
64
|
+
this._lastUrl = url;
|
|
62
65
|
// Inject stealth anti-detection patches (guard flag prevents double-injection).
|
|
63
66
|
try {
|
|
64
67
|
await sendCommand('exec', {
|
|
@@ -79,6 +82,10 @@ export class Page implements IPage {
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
async getCurrentUrl(): Promise<string | null> {
|
|
86
|
+
return this._lastUrl;
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
/** Close the automation window in the extension */
|
|
83
90
|
async closeWindow(): Promise<void> {
|
|
84
91
|
try {
|
|
@@ -183,6 +190,22 @@ export class Page implements IPage {
|
|
|
183
190
|
|
|
184
191
|
async wait(options: number | WaitOptions): Promise<void> {
|
|
185
192
|
if (typeof options === 'number') {
|
|
193
|
+
if (options >= 1) {
|
|
194
|
+
// For waits >= 1s, use DOM-stable check: return early when the page
|
|
195
|
+
// stops mutating, with the original wait time as the hard cap.
|
|
196
|
+
// This turns e.g. `page.wait(5)` from a fixed 5s sleep into
|
|
197
|
+
// "wait until DOM is stable, max 5s" — often completing in <1s.
|
|
198
|
+
try {
|
|
199
|
+
const maxMs = options * 1000;
|
|
200
|
+
await sendCommand('exec', {
|
|
201
|
+
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
|
|
202
|
+
...this._cmdOpts(),
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
} catch {
|
|
206
|
+
// Fallback: fixed sleep (e.g. if page has no DOM yet)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
186
209
|
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
187
210
|
return;
|
|
188
211
|
}
|
package/src/cli.ts
CHANGED
|
@@ -76,9 +76,10 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
76
76
|
for (const [site, cmds] of sites) {
|
|
77
77
|
console.log(chalk.bold.cyan(` ${site}`));
|
|
78
78
|
for (const cmd of cmds) {
|
|
79
|
-
const
|
|
79
|
+
const label = strategyLabel(cmd);
|
|
80
|
+
const tag = label === 'public'
|
|
80
81
|
? chalk.green('[public]')
|
|
81
|
-
: chalk.yellow(`[${
|
|
82
|
+
: chalk.yellow(`[${label}]`);
|
|
82
83
|
console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
|
|
83
84
|
}
|
|
84
85
|
console.log();
|
|
@@ -252,7 +253,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
252
253
|
|
|
253
254
|
pluginCmd
|
|
254
255
|
.command('install')
|
|
255
|
-
.description('Install a plugin from
|
|
256
|
+
.description('Install a plugin from a git repository')
|
|
256
257
|
.argument('<source>', 'Plugin source (e.g. github:user/repo)')
|
|
257
258
|
.action(async (source: string) => {
|
|
258
259
|
const { installPlugin } = await import('./plugin.js');
|
|
@@ -411,6 +412,36 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
411
412
|
console.log();
|
|
412
413
|
});
|
|
413
414
|
|
|
415
|
+
pluginCmd
|
|
416
|
+
.command('create')
|
|
417
|
+
.description('Create a new plugin scaffold')
|
|
418
|
+
.argument('<name>', 'Plugin name (lowercase, hyphens allowed)')
|
|
419
|
+
.option('-d, --dir <path>', 'Output directory (default: ./<name>)')
|
|
420
|
+
.option('--description <text>', 'Plugin description')
|
|
421
|
+
.action(async (name: string, opts: { dir?: string; description?: string }) => {
|
|
422
|
+
const { createPluginScaffold } = await import('./plugin-scaffold.js');
|
|
423
|
+
try {
|
|
424
|
+
const result = createPluginScaffold(name, {
|
|
425
|
+
dir: opts.dir,
|
|
426
|
+
description: opts.description,
|
|
427
|
+
});
|
|
428
|
+
console.log(chalk.green(`✅ Plugin scaffold created at ${result.dir}`));
|
|
429
|
+
console.log();
|
|
430
|
+
console.log(chalk.bold(' Files created:'));
|
|
431
|
+
for (const f of result.files) {
|
|
432
|
+
console.log(` ${chalk.cyan(f)}`);
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
console.log(chalk.dim(' Next steps:'));
|
|
436
|
+
console.log(chalk.dim(` cd ${result.dir}`));
|
|
437
|
+
console.log(chalk.dim(` opencli plugin install file://${result.dir}`));
|
|
438
|
+
console.log(chalk.dim(` opencli ${name} hello`));
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
414
445
|
// ── External CLIs ─────────────────────────────────────────────────────────
|
|
415
446
|
|
|
416
447
|
const externalClis = loadExternalClis();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: feeds
|
|
3
|
+
description: Popular Bluesky feed generators
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 20
|
|
12
|
+
description: Number of feeds
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.unspecced.getPopularFeedGenerators?limit=${{ args.limit }}
|
|
17
|
+
|
|
18
|
+
- select: feeds
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
rank: ${{ index + 1 }}
|
|
22
|
+
name: ${{ item.displayName }}
|
|
23
|
+
likes: ${{ item.likeCount }}
|
|
24
|
+
creator: ${{ item.creator.handle }}
|
|
25
|
+
description: ${{ item.description }}
|
|
26
|
+
|
|
27
|
+
- limit: ${{ args.limit }}
|
|
28
|
+
|
|
29
|
+
columns: [rank, name, likes, creator, description]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: followers
|
|
3
|
+
description: List followers of a Bluesky user
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
handle:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: "Bluesky handle"
|
|
14
|
+
limit:
|
|
15
|
+
type: int
|
|
16
|
+
default: 20
|
|
17
|
+
description: Number of followers
|
|
18
|
+
|
|
19
|
+
pipeline:
|
|
20
|
+
- fetch:
|
|
21
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.graph.getFollowers?actor=${{ args.handle }}&limit=${{ args.limit }}
|
|
22
|
+
|
|
23
|
+
- select: followers
|
|
24
|
+
|
|
25
|
+
- map:
|
|
26
|
+
rank: ${{ index + 1 }}
|
|
27
|
+
handle: ${{ item.handle }}
|
|
28
|
+
name: ${{ item.displayName }}
|
|
29
|
+
description: ${{ item.description }}
|
|
30
|
+
|
|
31
|
+
- limit: ${{ args.limit }}
|
|
32
|
+
|
|
33
|
+
columns: [rank, handle, name, description]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: following
|
|
3
|
+
description: List accounts a Bluesky user is following
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
handle:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: "Bluesky handle"
|
|
14
|
+
limit:
|
|
15
|
+
type: int
|
|
16
|
+
default: 20
|
|
17
|
+
description: Number of accounts
|
|
18
|
+
|
|
19
|
+
pipeline:
|
|
20
|
+
- fetch:
|
|
21
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=${{ args.handle }}&limit=${{ args.limit }}
|
|
22
|
+
|
|
23
|
+
- select: follows
|
|
24
|
+
|
|
25
|
+
- map:
|
|
26
|
+
rank: ${{ index + 1 }}
|
|
27
|
+
handle: ${{ item.handle }}
|
|
28
|
+
name: ${{ item.displayName }}
|
|
29
|
+
description: ${{ item.description }}
|
|
30
|
+
|
|
31
|
+
- limit: ${{ args.limit }}
|
|
32
|
+
|
|
33
|
+
columns: [rank, handle, name, description]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: profile
|
|
3
|
+
description: Get Bluesky user profile info
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
handle:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: "Bluesky handle (e.g. bsky.app, jay.bsky.team)"
|
|
14
|
+
|
|
15
|
+
pipeline:
|
|
16
|
+
- fetch:
|
|
17
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${{ args.handle }}
|
|
18
|
+
|
|
19
|
+
- map:
|
|
20
|
+
handle: ${{ item.handle }}
|
|
21
|
+
name: ${{ item.displayName }}
|
|
22
|
+
followers: ${{ item.followersCount }}
|
|
23
|
+
following: ${{ item.followsCount }}
|
|
24
|
+
posts: ${{ item.postsCount }}
|
|
25
|
+
description: ${{ item.description }}
|
|
26
|
+
|
|
27
|
+
columns: [handle, name, followers, following, posts, description]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: search
|
|
3
|
+
description: Search Bluesky users
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
query:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: Search query
|
|
14
|
+
limit:
|
|
15
|
+
type: int
|
|
16
|
+
default: 10
|
|
17
|
+
description: Number of results
|
|
18
|
+
|
|
19
|
+
pipeline:
|
|
20
|
+
- fetch:
|
|
21
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?q=${{ args.query }}&limit=${{ args.limit }}
|
|
22
|
+
|
|
23
|
+
- select: actors
|
|
24
|
+
|
|
25
|
+
- map:
|
|
26
|
+
rank: ${{ index + 1 }}
|
|
27
|
+
handle: ${{ item.handle }}
|
|
28
|
+
name: ${{ item.displayName }}
|
|
29
|
+
followers: ${{ item.followersCount }}
|
|
30
|
+
description: ${{ item.description }}
|
|
31
|
+
|
|
32
|
+
- limit: ${{ args.limit }}
|
|
33
|
+
|
|
34
|
+
columns: [rank, handle, name, followers, description]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: starter-packs
|
|
3
|
+
description: Get starter packs created by a Bluesky user
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
handle:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: "Bluesky handle"
|
|
14
|
+
limit:
|
|
15
|
+
type: int
|
|
16
|
+
default: 10
|
|
17
|
+
description: Number of starter packs
|
|
18
|
+
|
|
19
|
+
pipeline:
|
|
20
|
+
- fetch:
|
|
21
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.graph.getActorStarterPacks?actor=${{ args.handle }}&limit=${{ args.limit }}
|
|
22
|
+
|
|
23
|
+
- select: starterPacks
|
|
24
|
+
|
|
25
|
+
- map:
|
|
26
|
+
rank: ${{ index + 1 }}
|
|
27
|
+
name: ${{ item.record.name }}
|
|
28
|
+
description: ${{ item.record.description }}
|
|
29
|
+
members: ${{ item.listItemCount }}
|
|
30
|
+
joins: ${{ item.joinedAllTimeCount }}
|
|
31
|
+
|
|
32
|
+
- limit: ${{ args.limit }}
|
|
33
|
+
|
|
34
|
+
columns: [rank, name, description, members, joins]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: thread
|
|
3
|
+
description: Get a Bluesky post thread with replies
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
uri:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: "Post AT URI (at://did:.../app.bsky.feed.post/...) or bsky.app URL"
|
|
14
|
+
limit:
|
|
15
|
+
type: int
|
|
16
|
+
default: 20
|
|
17
|
+
description: Number of replies
|
|
18
|
+
|
|
19
|
+
pipeline:
|
|
20
|
+
- fetch:
|
|
21
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${{ args.uri }}&depth=2
|
|
22
|
+
|
|
23
|
+
- select: thread
|
|
24
|
+
|
|
25
|
+
- map:
|
|
26
|
+
author: ${{ item.post.author.handle }}
|
|
27
|
+
text: ${{ item.post.record.text }}
|
|
28
|
+
likes: ${{ item.post.likeCount }}
|
|
29
|
+
reposts: ${{ item.post.repostCount }}
|
|
30
|
+
replies_count: ${{ item.post.replyCount }}
|
|
31
|
+
|
|
32
|
+
columns: [author, text, likes, reposts, replies_count]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: trending
|
|
3
|
+
description: Trending topics on Bluesky
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 20
|
|
12
|
+
description: Number of topics
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.unspecced.getTrendingTopics
|
|
17
|
+
|
|
18
|
+
- select: topics
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
rank: ${{ index + 1 }}
|
|
22
|
+
topic: ${{ item.topic }}
|
|
23
|
+
link: ${{ item.link }}
|
|
24
|
+
|
|
25
|
+
- limit: ${{ args.limit }}
|
|
26
|
+
|
|
27
|
+
columns: [rank, topic, link]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
site: bluesky
|
|
2
|
+
name: user
|
|
3
|
+
description: Get recent posts from a Bluesky user
|
|
4
|
+
domain: public.api.bsky.app
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
handle:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
positional: true
|
|
13
|
+
description: "Bluesky handle (e.g. bsky.app)"
|
|
14
|
+
limit:
|
|
15
|
+
type: int
|
|
16
|
+
default: 20
|
|
17
|
+
description: Number of posts
|
|
18
|
+
|
|
19
|
+
pipeline:
|
|
20
|
+
- fetch:
|
|
21
|
+
url: https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${{ args.handle }}&limit=${{ args.limit }}
|
|
22
|
+
|
|
23
|
+
- select: feed
|
|
24
|
+
|
|
25
|
+
- map:
|
|
26
|
+
rank: ${{ index + 1 }}
|
|
27
|
+
text: ${{ item.post.record.text }}
|
|
28
|
+
likes: ${{ item.post.likeCount }}
|
|
29
|
+
reposts: ${{ item.post.repostCount }}
|
|
30
|
+
replies: ${{ item.post.replyCount }}
|
|
31
|
+
|
|
32
|
+
- limit: ${{ args.limit }}
|
|
33
|
+
|
|
34
|
+
columns: [rank, text, likes, reposts, replies]
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
3
3
|
|
|
4
|
-
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
7
|
-
|
|
8
|
-
// ── Types ──────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
interface TrendItem {
|
|
11
|
-
rank: number;
|
|
12
|
-
topic: string;
|
|
13
|
-
tweets: string;
|
|
14
|
-
category: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
4
|
// ── CLI definition ────────────────────────────────────────────────────
|
|
18
5
|
|
|
19
6
|
cli({
|
|
@@ -34,79 +21,44 @@ cli({
|
|
|
34
21
|
await page.goto('https://x.com/explore/tabs/trending');
|
|
35
22
|
await page.wait(3);
|
|
36
23
|
|
|
37
|
-
//
|
|
24
|
+
// Verify login via CSRF cookie
|
|
38
25
|
const ct0 = await page.evaluate(`(() => {
|
|
39
26
|
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
40
27
|
})()`);
|
|
41
28
|
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
42
29
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
// Scrape trends from DOM (consistent with what the user sees on the page)
|
|
31
|
+
// DOM children: [0] rank + category, [1] topic, optional post count,
|
|
32
|
+
// and a caret menu button identified by [data-testid="caret"].
|
|
33
|
+
await page.wait(2);
|
|
34
|
+
const trends = await page.evaluate(`(() => {
|
|
35
|
+
const items = [];
|
|
36
|
+
const cells = document.querySelectorAll('[data-testid="trend"]');
|
|
37
|
+
cells.forEach((cell) => {
|
|
38
|
+
const text = cell.textContent || '';
|
|
39
|
+
if (text.includes('Promoted')) return;
|
|
40
|
+
const container = cell.querySelector(':scope > div');
|
|
41
|
+
if (!container) return;
|
|
42
|
+
const divs = container.children;
|
|
43
|
+
if (divs.length < 2) return;
|
|
44
|
+
const topic = divs[1].textContent.trim();
|
|
45
|
+
if (!topic) return;
|
|
46
|
+
const catText = divs[0].textContent.trim();
|
|
47
|
+
const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
|
|
48
|
+
// Find post count: skip rank, topic, and the caret menu button
|
|
49
|
+
let tweets = 'N/A';
|
|
50
|
+
for (let j = 2; j < divs.length; j++) {
|
|
51
|
+
if (divs[j].matches('[data-testid="caret"]') || divs[j].querySelector('[data-testid="caret"]')) continue;
|
|
52
|
+
const t = divs[j].textContent.trim();
|
|
53
|
+
if (t && /\\d/.test(t)) { tweets = t; break; }
|
|
54
54
|
}
|
|
55
|
+
items.push({ rank: items.length + 1, topic, tweets, category });
|
|
55
56
|
});
|
|
56
|
-
return
|
|
57
|
+
return items;
|
|
57
58
|
})()`);
|
|
58
59
|
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
const entries = instructions.flatMap((inst: any) => inst?.addEntries?.entries || inst?.entries || []);
|
|
62
|
-
const apiTrends = entries
|
|
63
|
-
.filter((e: any) => e.content?.timelineModule)
|
|
64
|
-
.flatMap((e: any) => e.content.timelineModule.items || [])
|
|
65
|
-
.map((t: any) => t?.item?.content?.trend)
|
|
66
|
-
.filter(Boolean);
|
|
67
|
-
|
|
68
|
-
trends = apiTrends.map((t: any, i: number) => ({
|
|
69
|
-
rank: i + 1,
|
|
70
|
-
topic: t.name,
|
|
71
|
-
tweets: t.tweetCount ? String(t.tweetCount) : 'N/A',
|
|
72
|
-
category: t.trendMetadata?.domainContext || '',
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Fallback: scrape from the loaded DOM
|
|
77
|
-
if (trends.length === 0) {
|
|
78
|
-
await page.wait(2);
|
|
79
|
-
const domTrends = await page.evaluate(`(() => {
|
|
80
|
-
const items = [];
|
|
81
|
-
const cells = document.querySelectorAll('[data-testid="trend"]');
|
|
82
|
-
cells.forEach((cell) => {
|
|
83
|
-
const text = cell.textContent || '';
|
|
84
|
-
if (text.includes('Promoted')) return;
|
|
85
|
-
const container = cell.querySelector(':scope > div');
|
|
86
|
-
if (!container) return;
|
|
87
|
-
const divs = container.children;
|
|
88
|
-
// Structure: divs[0] = rank + category, divs[1] = topic name, divs[2] = extra
|
|
89
|
-
const topicEl = divs.length >= 2 ? divs[1] : null;
|
|
90
|
-
const topic = topicEl ? topicEl.textContent.trim() : '';
|
|
91
|
-
const catEl = divs.length >= 1 ? divs[0] : null;
|
|
92
|
-
const catText = catEl ? catEl.textContent.trim() : '';
|
|
93
|
-
const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
|
|
94
|
-
const extraEl = divs.length >= 3 ? divs[2] : null;
|
|
95
|
-
const extra = extraEl ? extraEl.textContent.trim() : '';
|
|
96
|
-
if (topic) {
|
|
97
|
-
items.push({ rank: items.length + 1, topic, tweets: extra || 'N/A', category });
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
return items;
|
|
101
|
-
})()`);
|
|
102
|
-
|
|
103
|
-
if (Array.isArray(domTrends) && domTrends.length > 0) {
|
|
104
|
-
trends = domTrends;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (trends.length === 0) {
|
|
109
|
-
throw new EmptyResultError('twitter trending', 'API may have changed or login may be required.');
|
|
60
|
+
if (!Array.isArray(trends) || trends.length === 0) {
|
|
61
|
+
throw new EmptyResultError('twitter trending', 'No trends found. The page structure may have changed.');
|
|
110
62
|
}
|
|
111
63
|
|
|
112
64
|
return trends.slice(0, limit);
|