@jackwener/opencli 1.5.0 → 1.5.2
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/discover.js +11 -7
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/page.d.ts +4 -0
- package/dist/browser/page.js +52 -3
- package/dist/browser.test.js +5 -0
- package/dist/cli-manifest.json +460 -1
- package/dist/cli.js +34 -3
- package/dist/clis/apple-podcasts/commands.test.js +26 -3
- package/dist/clis/apple-podcasts/top.js +4 -1
- 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/weread/shelf.js +132 -9
- package/dist/clis/weread/utils.js +5 -1
- 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/daemon.js +1 -0
- package/dist/discovery.js +40 -28
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +9 -5
- 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 -13
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/extension-manifest-regression.test.d.ts +1 -0
- package/dist/extension-manifest-regression.test.js +12 -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/dist/weread-private-api-regression.test.js +185 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/guide/plugins.md +10 -0
- package/extension/dist/background.js +4 -2
- package/extension/manifest.json +4 -1
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +2 -1
- package/package.json +1 -1
- package/src/browser/cdp.ts +6 -0
- package/src/browser/discover.ts +10 -7
- package/src/browser/index.ts +2 -0
- package/src/browser/page.ts +49 -3
- package/src/browser.test.ts +6 -0
- package/src/cli.ts +34 -3
- package/src/clis/apple-podcasts/commands.test.ts +30 -2
- package/src/clis/apple-podcasts/top.ts +4 -1
- 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/weread/shelf.ts +169 -9
- package/src/clis/weread/utils.ts +6 -1
- 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/daemon.ts +1 -0
- package/src/discovery.ts +41 -33
- package/src/doctor.ts +11 -8
- 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 -15
- package/src/extension-manifest-regression.test.ts +17 -0
- 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/src/weread-private-api-regression.test.ts +207 -0
- package/tests/e2e/browser-public.test.ts +1 -1
- package/tests/e2e/output-formats.test.ts +10 -14
- package/tests/e2e/plugin-management.test.ts +4 -1
- package/tests/e2e/public-commands.test.ts +12 -1
- package/vitest.config.ts +1 -15
package/dist/cli.js
CHANGED
|
@@ -69,9 +69,10 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
69
69
|
for (const [site, cmds] of sites) {
|
|
70
70
|
console.log(chalk.bold.cyan(` ${site}`));
|
|
71
71
|
for (const cmd of cmds) {
|
|
72
|
-
const
|
|
72
|
+
const label = strategyLabel(cmd);
|
|
73
|
+
const tag = label === 'public'
|
|
73
74
|
? chalk.green('[public]')
|
|
74
|
-
: chalk.yellow(`[${
|
|
75
|
+
: chalk.yellow(`[${label}]`);
|
|
75
76
|
console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
|
|
76
77
|
}
|
|
77
78
|
console.log();
|
|
@@ -228,7 +229,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
228
229
|
const pluginCmd = program.command('plugin').description('Manage opencli plugins');
|
|
229
230
|
pluginCmd
|
|
230
231
|
.command('install')
|
|
231
|
-
.description('Install a plugin from
|
|
232
|
+
.description('Install a plugin from a git repository')
|
|
232
233
|
.argument('<source>', 'Plugin source (e.g. github:user/repo)')
|
|
233
234
|
.action(async (source) => {
|
|
234
235
|
const { installPlugin } = await import('./plugin.js');
|
|
@@ -380,6 +381,36 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
380
381
|
console.log(chalk.dim(` ${plugins.length} plugin(s) installed`));
|
|
381
382
|
console.log();
|
|
382
383
|
});
|
|
384
|
+
pluginCmd
|
|
385
|
+
.command('create')
|
|
386
|
+
.description('Create a new plugin scaffold')
|
|
387
|
+
.argument('<name>', 'Plugin name (lowercase, hyphens allowed)')
|
|
388
|
+
.option('-d, --dir <path>', 'Output directory (default: ./<name>)')
|
|
389
|
+
.option('--description <text>', 'Plugin description')
|
|
390
|
+
.action(async (name, opts) => {
|
|
391
|
+
const { createPluginScaffold } = await import('./plugin-scaffold.js');
|
|
392
|
+
try {
|
|
393
|
+
const result = createPluginScaffold(name, {
|
|
394
|
+
dir: opts.dir,
|
|
395
|
+
description: opts.description,
|
|
396
|
+
});
|
|
397
|
+
console.log(chalk.green(`✅ Plugin scaffold created at ${result.dir}`));
|
|
398
|
+
console.log();
|
|
399
|
+
console.log(chalk.bold(' Files created:'));
|
|
400
|
+
for (const f of result.files) {
|
|
401
|
+
console.log(` ${chalk.cyan(f)}`);
|
|
402
|
+
}
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(chalk.dim(' Next steps:'));
|
|
405
|
+
console.log(chalk.dim(` cd ${result.dir}`));
|
|
406
|
+
console.log(chalk.dim(` opencli plugin install file://${result.dir}`));
|
|
407
|
+
console.log(chalk.dim(` opencli ${name} hello`));
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
411
|
+
process.exitCode = 1;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
383
414
|
// ── External CLIs ─────────────────────────────────────────────────────────
|
|
384
415
|
const externalClis = loadExternalClis();
|
|
385
416
|
program
|
|
@@ -31,13 +31,14 @@ describe('apple-podcasts search command', () => {
|
|
|
31
31
|
});
|
|
32
32
|
expect(fetchMock).toHaveBeenCalledWith('https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5');
|
|
33
33
|
expect(result).toEqual([
|
|
34
|
-
{
|
|
34
|
+
expect.objectContaining({
|
|
35
35
|
id: 42,
|
|
36
36
|
title: 'Machine Learning Guide',
|
|
37
37
|
author: 'OpenCLI',
|
|
38
38
|
episodes: 12,
|
|
39
39
|
genre: 'Technology',
|
|
40
|
-
|
|
40
|
+
url: '',
|
|
41
|
+
}),
|
|
41
42
|
]);
|
|
42
43
|
});
|
|
43
44
|
});
|
|
@@ -45,6 +46,26 @@ describe('apple-podcasts top command', () => {
|
|
|
45
46
|
beforeEach(() => {
|
|
46
47
|
vi.restoreAllMocks();
|
|
47
48
|
});
|
|
49
|
+
it('adds a timeout signal to chart fetches', async () => {
|
|
50
|
+
const cmd = getRegistry().get('apple-podcasts/top');
|
|
51
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
52
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
53
|
+
ok: true,
|
|
54
|
+
json: () => Promise.resolve({
|
|
55
|
+
feed: {
|
|
56
|
+
results: [
|
|
57
|
+
{ id: '100', name: 'Top Show', artistName: 'Host A' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
63
|
+
await cmd.func(null, { country: 'US', limit: 1 });
|
|
64
|
+
const [, options] = fetchMock.mock.calls[0] ?? [];
|
|
65
|
+
expect(options).toBeDefined();
|
|
66
|
+
expect(options.signal).toBeDefined();
|
|
67
|
+
expect(options.signal).toHaveProperty('aborted', false);
|
|
68
|
+
});
|
|
48
69
|
it('uses the canonical Apple charts host and maps ranked results', async () => {
|
|
49
70
|
const cmd = getRegistry().get('apple-podcasts/top');
|
|
50
71
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -61,7 +82,9 @@ describe('apple-podcasts top command', () => {
|
|
|
61
82
|
});
|
|
62
83
|
vi.stubGlobal('fetch', fetchMock);
|
|
63
84
|
const result = await cmd.func(null, { country: 'US', limit: 2 });
|
|
64
|
-
expect(fetchMock).toHaveBeenCalledWith('https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json'
|
|
85
|
+
expect(fetchMock).toHaveBeenCalledWith('https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json', expect.objectContaining({
|
|
86
|
+
signal: expect.any(Object),
|
|
87
|
+
}));
|
|
65
88
|
expect(result).toEqual([
|
|
66
89
|
{ rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
|
|
67
90
|
{ rank: 2, title: 'Second Show', author: 'Host B', id: '101' },
|
|
@@ -2,6 +2,7 @@ import { cli, Strategy } from '../../registry.js';
|
|
|
2
2
|
import { CliError } from '../../errors.js';
|
|
3
3
|
// Apple Marketing Tools RSS API — public, no key required
|
|
4
4
|
const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
|
|
5
|
+
const CHARTS_TIMEOUT_MS = 15_000;
|
|
5
6
|
cli({
|
|
6
7
|
site: 'apple-podcasts',
|
|
7
8
|
name: 'top',
|
|
@@ -19,7 +20,9 @@ cli({
|
|
|
19
20
|
const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
|
|
20
21
|
let resp;
|
|
21
22
|
try {
|
|
22
|
-
resp = await fetch(url
|
|
23
|
+
resp = await fetch(url, {
|
|
24
|
+
signal: AbortSignal.timeout(CHARTS_TIMEOUT_MS),
|
|
25
|
+
});
|
|
23
26
|
}
|
|
24
27
|
catch (error) {
|
|
25
28
|
const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
|
|
@@ -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,7 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
3
|
-
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
4
|
-
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
5
3
|
// ── CLI definition ────────────────────────────────────────────────────
|
|
6
4
|
cli({
|
|
7
5
|
site: 'twitter',
|
|
@@ -19,73 +17,43 @@ cli({
|
|
|
19
17
|
// Navigate to trending page
|
|
20
18
|
await page.goto('https://x.com/explore/tabs/trending');
|
|
21
19
|
await page.wait(3);
|
|
22
|
-
//
|
|
20
|
+
// Verify login via CSRF cookie
|
|
23
21
|
const ct0 = await page.evaluate(`(() => {
|
|
24
22
|
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
25
23
|
})()`);
|
|
26
24
|
if (!ct0)
|
|
27
25
|
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
// Scrape trends from DOM (consistent with what the user sees on the page)
|
|
27
|
+
// DOM children: [0] rank + category, [1] topic, optional post count,
|
|
28
|
+
// and a caret menu button identified by [data-testid="caret"].
|
|
29
|
+
await page.wait(2);
|
|
30
|
+
const trends = await page.evaluate(`(() => {
|
|
31
|
+
const items = [];
|
|
32
|
+
const cells = document.querySelectorAll('[data-testid="trend"]');
|
|
33
|
+
cells.forEach((cell) => {
|
|
34
|
+
const text = cell.textContent || '';
|
|
35
|
+
if (text.includes('Promoted')) return;
|
|
36
|
+
const container = cell.querySelector(':scope > div');
|
|
37
|
+
if (!container) return;
|
|
38
|
+
const divs = container.children;
|
|
39
|
+
if (divs.length < 2) return;
|
|
40
|
+
const topic = divs[1].textContent.trim();
|
|
41
|
+
if (!topic) return;
|
|
42
|
+
const catText = divs[0].textContent.trim();
|
|
43
|
+
const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
|
|
44
|
+
// Find post count: skip rank, topic, and the caret menu button
|
|
45
|
+
let tweets = 'N/A';
|
|
46
|
+
for (let j = 2; j < divs.length; j++) {
|
|
47
|
+
if (divs[j].matches('[data-testid="caret"]') || divs[j].querySelector('[data-testid="caret"]')) continue;
|
|
48
|
+
const t = divs[j].textContent.trim();
|
|
49
|
+
if (t && /\\d/.test(t)) { tweets = t; break; }
|
|
38
50
|
}
|
|
51
|
+
items.push({ rank: items.length + 1, topic, tweets, category });
|
|
39
52
|
});
|
|
40
|
-
return
|
|
53
|
+
return items;
|
|
41
54
|
})()`);
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
const entries = instructions.flatMap((inst) => inst?.addEntries?.entries || inst?.entries || []);
|
|
45
|
-
const apiTrends = entries
|
|
46
|
-
.filter((e) => e.content?.timelineModule)
|
|
47
|
-
.flatMap((e) => e.content.timelineModule.items || [])
|
|
48
|
-
.map((t) => t?.item?.content?.trend)
|
|
49
|
-
.filter(Boolean);
|
|
50
|
-
trends = apiTrends.map((t, i) => ({
|
|
51
|
-
rank: i + 1,
|
|
52
|
-
topic: t.name,
|
|
53
|
-
tweets: t.tweetCount ? String(t.tweetCount) : 'N/A',
|
|
54
|
-
category: t.trendMetadata?.domainContext || '',
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
57
|
-
// Fallback: scrape from the loaded DOM
|
|
58
|
-
if (trends.length === 0) {
|
|
59
|
-
await page.wait(2);
|
|
60
|
-
const domTrends = await page.evaluate(`(() => {
|
|
61
|
-
const items = [];
|
|
62
|
-
const cells = document.querySelectorAll('[data-testid="trend"]');
|
|
63
|
-
cells.forEach((cell) => {
|
|
64
|
-
const text = cell.textContent || '';
|
|
65
|
-
if (text.includes('Promoted')) return;
|
|
66
|
-
const container = cell.querySelector(':scope > div');
|
|
67
|
-
if (!container) return;
|
|
68
|
-
const divs = container.children;
|
|
69
|
-
// Structure: divs[0] = rank + category, divs[1] = topic name, divs[2] = extra
|
|
70
|
-
const topicEl = divs.length >= 2 ? divs[1] : null;
|
|
71
|
-
const topic = topicEl ? topicEl.textContent.trim() : '';
|
|
72
|
-
const catEl = divs.length >= 1 ? divs[0] : null;
|
|
73
|
-
const catText = catEl ? catEl.textContent.trim() : '';
|
|
74
|
-
const category = catText.replace(/^\\d+\\s*/, '').replace(/^\\xB7\\s*/, '').trim();
|
|
75
|
-
const extraEl = divs.length >= 3 ? divs[2] : null;
|
|
76
|
-
const extra = extraEl ? extraEl.textContent.trim() : '';
|
|
77
|
-
if (topic) {
|
|
78
|
-
items.push({ rank: items.length + 1, topic, tweets: extra || 'N/A', category });
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
return items;
|
|
82
|
-
})()`);
|
|
83
|
-
if (Array.isArray(domTrends) && domTrends.length > 0) {
|
|
84
|
-
trends = domTrends;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (trends.length === 0) {
|
|
88
|
-
throw new EmptyResultError('twitter trending', 'API may have changed or login may be required.');
|
|
55
|
+
if (!Array.isArray(trends) || trends.length === 0) {
|
|
56
|
+
throw new EmptyResultError('twitter trending', 'No trends found. The page structure may have changed.');
|
|
89
57
|
}
|
|
90
58
|
return trends.slice(0, limit);
|
|
91
59
|
},
|