@jackwener/opencli 1.7.19 → 1.7.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/README.zh-CN.md +9 -10
- package/cli-manifest.json +6 -177
- package/clis/twitter/bookmark-folder.js +5 -3
- package/clis/twitter/bookmark-folder.test.js +59 -1
- package/clis/twitter/bookmarks.js +9 -3
- package/clis/twitter/bookmarks.test.js +205 -0
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +3 -0
- package/dist/src/browser/daemon-client.test.js +20 -0
- package/dist/src/cli.js +8 -3
- package/dist/src/cli.test.js +1 -0
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -1
- package/dist/src/daemon.js +44 -13
- package/dist/src/daemon.test.js +42 -1
- package/dist/src/electron-apps.js +0 -1
- package/dist/src/electron-apps.test.js +1 -0
- package/dist/src/external-clis.yaml +12 -3
- package/dist/src/external.d.ts +4 -0
- package/dist/src/external.js +3 -0
- package/dist/src/external.test.js +24 -1
- package/dist/src/help.d.ts +5 -1
- package/dist/src/help.js +4 -3
- package/dist/src/help.test.js +5 -1
- package/package.json +1 -1
- package/clis/notion/export.js +0 -32
- package/clis/notion/favorites.js +0 -85
- package/clis/notion/new.js +0 -35
- package/clis/notion/read.js +0 -31
- package/clis/notion/search.js +0 -47
- package/clis/notion/sidebar.js +0 -42
- package/clis/notion/status.js +0 -17
- package/clis/notion/write.js +0 -41
package/README.md
CHANGED
|
@@ -14,17 +14,17 @@ OpenCLI gives you one surface for three different kinds of automation:
|
|
|
14
14
|
- **Let AI Agents operate any website** — install the `opencli-adapter-author` skill in your AI agent (Claude Code, Cursor, etc.), and it can navigate, click, type/fill, extract, and inspect any page through your logged-in browser via `opencli browser` primitives.
|
|
15
15
|
- **Write new adapters** end-to-end with `opencli browser` + the `opencli-adapter-author` skill, which guides from first recon through field decoding, code, and `opencli browser verify`.
|
|
16
16
|
|
|
17
|
-
It also works as a **CLI hub** for local tools such as `gh`, `docker`, `tg
|
|
17
|
+
It also works as a **CLI hub** for local tools such as `gh`, `docker`, `tg`, `discord`, `wx`, `ntn` (Notion), and other binaries you register yourself, plus **desktop app adapters** for Electron apps like Cursor, Codex, Antigravity, and ChatGPT.
|
|
18
18
|
|
|
19
19
|
## Highlights
|
|
20
20
|
|
|
21
|
-
- **Desktop App Control** — Drive Electron apps (Cursor, Codex, ChatGPT,
|
|
21
|
+
- **Desktop App Control** — Drive Electron apps (Cursor, Codex, ChatGPT, etc.) directly from the terminal via CDP.
|
|
22
22
|
- **Browser Automation for AI Agents** — Install the `opencli-adapter-author` skill, and your AI agent can operate any website: navigate, click, type/fill, extract, screenshot — all through your logged-in Chrome session.
|
|
23
23
|
- **Multi-profile Browser Bridge** — Install the extension in each Chrome profile you want to use, then route commands with `--profile`, `OPENCLI_PROFILE`, or `opencli profile use`.
|
|
24
24
|
- **Website → CLI** — Turn any website into a deterministic CLI: 100+ site surfaces are already registered, or write your own with the `opencli-adapter-author` skill + `opencli browser verify`.
|
|
25
25
|
- **Account-safe** — Reuses Chrome/Chromium logged-in state; your credentials never leave the browser.
|
|
26
26
|
- **AI Agent ready** — One skill takes you from site recon through API discovery, field decoding, adapter writing, and verification.
|
|
27
|
-
- **CLI Hub** — Discover, auto-install, and passthrough commands to any external CLI (gh, docker, obsidian, tg
|
|
27
|
+
- **CLI Hub** — Discover, auto-install, and passthrough commands to any external CLI (gh, docker, obsidian, tg, discord, wx, etc).
|
|
28
28
|
- **Zero LLM cost** — No tokens consumed at runtime. Run 10,000 times and pay nothing.
|
|
29
29
|
- **Deterministic** — Same command, same output schema, every time. Pipeable, scriptable, CI-friendly.
|
|
30
30
|
|
|
@@ -181,7 +181,7 @@ When the site you need is not yet covered, use the `opencli-adapter-author` skil
|
|
|
181
181
|
|
|
182
182
|
OpenCLI is not only for websites. It can also:
|
|
183
183
|
|
|
184
|
-
- expose local binaries like `gh`, `docker`, `obsidian`, `tg
|
|
184
|
+
- expose local binaries like `gh`, `docker`, `obsidian`, `tg`, `discord`, `wx`, or custom tools through `opencli <tool> ...`
|
|
185
185
|
- control Electron desktop apps through dedicated adapters and CDP-backed integrations
|
|
186
186
|
|
|
187
187
|
## Prerequisites
|
|
@@ -283,19 +283,20 @@ To load the source Browser Bridge extension:
|
|
|
283
283
|
|
|
284
284
|
## CLI Hub
|
|
285
285
|
|
|
286
|
-
OpenCLI acts as a universal hub for your existing command-line tools — unified discovery, pure passthrough execution, and auto-install
|
|
286
|
+
OpenCLI acts as a universal hub for your existing command-line tools — unified discovery, pure passthrough execution, and auto-install when a safe package-manager command is configured.
|
|
287
287
|
|
|
288
288
|
| External CLI | Description | Example |
|
|
289
289
|
|--------------|-------------|---------|
|
|
290
290
|
| **gh** | GitHub CLI | `opencli gh pr list --limit 5` |
|
|
291
291
|
| **obsidian** | Obsidian vault management | `opencli obsidian search query="AI"` |
|
|
292
292
|
| **docker** | Docker | `opencli docker ps` |
|
|
293
|
+
| **ntn** | Notion CLI — official Notion API CLI for pages, databases, blocks, search, comments | `opencli ntn pages list` |
|
|
293
294
|
| **lark-cli** | Lark/Feishu — messages, docs, calendar, tasks, 200+ commands | `opencli lark-cli calendar +agenda` |
|
|
294
295
|
| **dws** | DingTalk — cross-platform CLI for DingTalk's full suite, designed for humans and AI agents | `opencli dws msg send --to user "hello"` |
|
|
295
296
|
| **wecom-cli** | WeCom/企业微信 — CLI for WeCom open platform, for humans and AI agents | `opencli wecom-cli msg send --to user "hello"` |
|
|
296
|
-
| **tg-cli** | Telegram — local-first sync, search, and export via MTProto for AI agents | `opencli tg search "AI news" -f json` |
|
|
297
|
-
| **discord-cli** | Discord — local-first sync, search, and export via SQLite for AI agents | `opencli discord recent --channel general` |
|
|
298
|
-
| **wx-cli** | WeChat — query local WeChat data: sessions, messages, search, contacts, export | `opencli wx search "OpenCLI"` |
|
|
297
|
+
| **tg(tg-cli)** | Telegram — local-first sync, search, and export via MTProto for AI agents | `opencli tg search "AI news" -f json` |
|
|
298
|
+
| **discord(discord-cli)** | Discord — local-first sync, search, and export via SQLite for AI agents | `opencli discord recent --channel general` |
|
|
299
|
+
| **wx(wx-cli)** | WeChat — query local WeChat data: sessions, messages, search, contacts, export | `opencli wx search "OpenCLI"` |
|
|
299
300
|
| **vercel** | Vercel — deploy projects, manage domains, env vars, logs | `opencli vercel deploy --prod` |
|
|
300
301
|
|
|
301
302
|
**Register your own** — add any local CLI so AI agents can discover it via `opencli list`:
|
|
@@ -304,6 +305,8 @@ OpenCLI acts as a universal hub for your existing command-line tools — unified
|
|
|
304
305
|
opencli external register mycli
|
|
305
306
|
```
|
|
306
307
|
|
|
308
|
+
**Manual install** — some external CLIs use official shell-script installers rather than shell-free package-manager commands. For `ntn`, install from <https://ntn.dev> first, then run `opencli ntn ...`.
|
|
309
|
+
|
|
307
310
|
### Desktop App Adapters
|
|
308
311
|
|
|
309
312
|
Control Electron desktop apps directly from the terminal. Each adapter has its own detailed documentation:
|
|
@@ -315,7 +318,6 @@ Control Electron desktop apps directly from the terminal. Each adapter has its o
|
|
|
315
318
|
| **Antigravity** | Control Antigravity Ultra from terminal | [Doc](./docs/adapters/desktop/antigravity.md) |
|
|
316
319
|
| **ChatGPT App** | Automate ChatGPT macOS desktop app | [Doc](./docs/adapters/desktop/chatgpt-app.md) |
|
|
317
320
|
| **ChatWise** | Multi-LLM client (GPT-4, Claude, Gemini) | [Doc](./docs/adapters/desktop/chatwise.md) |
|
|
318
|
-
| **Notion** | Search, read, write Notion pages | [Doc](./docs/adapters/desktop/notion.md) |
|
|
319
321
|
| **Discord** | Discord Desktop — messages, channels, servers | [Doc](./docs/adapters/desktop/discord.md) |
|
|
320
322
|
| **Doubao** | Control Doubao AI desktop app via CDP | [Doc](./docs/adapters/desktop/doubao-app.md) |
|
|
321
323
|
|
package/README.zh-CN.md
CHANGED
|
@@ -14,16 +14,16 @@ OpenCLI 可以用同一套 CLI 做三类事情:
|
|
|
14
14
|
- **让 AI Agent 操作任意网站**:在你的 AI Agent(Claude Code、Cursor 等)中安装 `opencli-adapter-author` skill,Agent 就能用你的已登录浏览器导航、点击、输入/填充、提取任意网页内容。
|
|
15
15
|
- **把新网站写成 CLI**:用 `opencli browser` 原语 + `opencli-adapter-author` skill,从站点侦察、API 发现、字段解码到 `opencli browser verify` 一条龙。
|
|
16
16
|
|
|
17
|
-
除了网站能力,OpenCLI 还是一个 **CLI 枢纽**:你可以把 `gh`、`docker`、`tg
|
|
17
|
+
除了网站能力,OpenCLI 还是一个 **CLI 枢纽**:你可以把 `gh`、`docker`、`tg`、`discord`、`wx`、`ntn`(Notion)等本地工具统一注册到 `opencli` 下,也可以通过桌面端适配器控制 Cursor、Codex、Antigravity、ChatGPT 等 Electron 应用。
|
|
18
18
|
|
|
19
19
|
## 亮点
|
|
20
20
|
|
|
21
|
-
- **桌面应用控制** — 通过 CDP 直接在终端驱动 Electron 应用(Cursor、Codex、ChatGPT
|
|
21
|
+
- **桌面应用控制** — 通过 CDP 直接在终端驱动 Electron 应用(Cursor、Codex、ChatGPT 等)。
|
|
22
22
|
- **AI Agent 浏览器自动化** — 安装 `opencli-adapter-author` skill,你的 AI Agent 就能操作任意网站:导航、点击、输入/填充、提取、截图——全部通过你的已登录 Chrome 会话完成。
|
|
23
23
|
- **网站 → CLI** — 把任何网站变成确定性 CLI:100+ 站点能力已注册,或用 `opencli-adapter-author` skill + `opencli browser verify` 自己写。
|
|
24
24
|
- **账号安全** — 复用 Chrome/Chromium 登录态,凭证永远不会离开浏览器。
|
|
25
25
|
- **面向 AI Agent** — 一个 skill 带你走完站点侦察、API 发现、字段解码、适配器编写、验证的全流程。
|
|
26
|
-
- **CLI 枢纽** — 统一发现、自动安装、纯透传任何外部 CLI(gh、docker、obsidian、tg
|
|
26
|
+
- **CLI 枢纽** — 统一发现、自动安装、纯透传任何外部 CLI(gh、docker、obsidian、tg、discord、wx 等)。
|
|
27
27
|
- **零 LLM 成本** — 运行时不消耗模型 token,跑 10,000 次也不花一分钱。
|
|
28
28
|
- **确定性输出** — 相同命令,相同输出结构,每次一致。可管道、可脚本、CI 友好。
|
|
29
29
|
|
|
@@ -165,7 +165,7 @@ Agent 在内部自动处理所有 `opencli browser` 命令——你只需用自
|
|
|
165
165
|
|
|
166
166
|
OpenCLI 不只是网站 CLI,还可以:
|
|
167
167
|
|
|
168
|
-
- 统一代理本地二进制工具,例如 `gh`、`docker`、`obsidian`、`tg
|
|
168
|
+
- 统一代理本地二进制工具,例如 `gh`、`docker`、`obsidian`、`tg`、`discord`、`wx`
|
|
169
169
|
- 通过专门适配器和 CDP 集成控制 Electron 桌面应用
|
|
170
170
|
|
|
171
171
|
## 前置要求
|
|
@@ -241,7 +241,6 @@ npm link
|
|
|
241
241
|
| **chatwise** | `status` `new` `send` `read` `ask` `model` `history` `export` `screenshot` | 桌面端 |
|
|
242
242
|
| **doubao** | `status` `new` `send` `read` `ask` `history` `detail` `meeting-summary` `meeting-transcript` | 浏览器 |
|
|
243
243
|
| **doubao-app** | `status` `new` `send` `read` `ask` `screenshot` `dump` | 桌面端 |
|
|
244
|
-
| **notion** | `status` `search` `read` `new` `write` `sidebar` `favorites` `export` | 桌面端 |
|
|
245
244
|
| **discord-app** | `status` `send` `read` `channels` `servers` `search` `members` | 桌面端 |
|
|
246
245
|
| **v2ex** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 公开 / 浏览器 |
|
|
247
246
|
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `comments` `watchlist` `earnings-date` `fund-holdings` `fund-snapshot` | 浏览器 |
|
|
@@ -333,17 +332,18 @@ OpenCLI 也可以作为你现有命令行工具的统一入口,负责发现、
|
|
|
333
332
|
| **gh** | GitHub CLI | `opencli gh pr list --limit 5` |
|
|
334
333
|
| **obsidian** | Obsidian 仓库管理 | `opencli obsidian search query="AI"` |
|
|
335
334
|
| **docker** | Docker 命令行工具 | `opencli docker ps` |
|
|
335
|
+
| **ntn** | Notion CLI — 基于官方 Notion API 的页面、数据库、块、搜索、评论命令 | `opencli ntn pages list` |
|
|
336
336
|
| **lark-cli** | 飞书 CLI — 消息、文档、日历、任务,200+ 命令 | `opencli lark-cli calendar +agenda` |
|
|
337
337
|
| **dws** | 钉钉 CLI — 钉钉全套产品能力的跨平台命令行工具,支持人类和 AI Agent 使用 | `opencli dws msg send --to user "hello"` |
|
|
338
338
|
| **wecom-cli** | 企业微信 CLI — 企业微信开放平台命令行工具,支持人类和 AI Agent 使用 | `opencli wecom-cli msg send --to user "hello"` |
|
|
339
|
-
| **tg-cli** | Telegram CLI — 基于 MTProto 的本地优先同步、搜索、导出,面向 AI Agent | `opencli tg search "AI news" -f json` |
|
|
340
|
-
| **discord-cli** | Discord CLI — 基于 SQLite 的本地优先同步、搜索、导出,面向 AI Agent | `opencli discord recent --channel general` |
|
|
341
|
-
| **wx-cli** | 微信本地数据 CLI — 会话、聊天记录、搜索、联系人、导出 | `opencli wx search "OpenCLI"` |
|
|
339
|
+
| **tg(tg-cli)** | Telegram CLI — 基于 MTProto 的本地优先同步、搜索、导出,面向 AI Agent | `opencli tg search "AI news" -f json` |
|
|
340
|
+
| **discord(discord-cli)** | Discord CLI — 基于 SQLite 的本地优先同步、搜索、导出,面向 AI Agent | `opencli discord recent --channel general` |
|
|
341
|
+
| **wx(wx-cli)** | 微信本地数据 CLI — 会话、聊天记录、搜索、联系人、导出 | `opencli wx search "OpenCLI"` |
|
|
342
342
|
| **vercel** | Vercel — 部署项目、管理域名、环境变量、日志 | `opencli vercel deploy --prod` |
|
|
343
343
|
|
|
344
344
|
**零配置透传**:OpenCLI 会把你的输入原样转发给底层二进制,保留原生 stdout / stderr 行为。
|
|
345
345
|
|
|
346
|
-
|
|
346
|
+
**自动安装**:如果某个外部 CLI 配置了安全的包管理器安装命令,OpenCLI 会优先尝试安装后再执行;`ntn` 的官方安装方式是 shell 脚本,请先按 <https://ntn.dev> 手动安装。
|
|
347
347
|
|
|
348
348
|
**注册自定义本地 CLI**:
|
|
349
349
|
|
|
@@ -362,7 +362,6 @@ opencli register mycli
|
|
|
362
362
|
| **Antigravity** | 在终端直接控制 Antigravity Ultra | [Doc](./docs/adapters/desktop/antigravity.md) |
|
|
363
363
|
| **ChatGPT App** | 自动化操作 ChatGPT macOS 桌面客户端 | [Doc](./docs/adapters/desktop/chatgpt-app.md) |
|
|
364
364
|
| **ChatWise** | 多 LLM 客户端(GPT-4、Claude、Gemini) | [Doc](./docs/adapters/desktop/chatwise.md) |
|
|
365
|
-
| **Notion** | 搜索、读取、写入 Notion 页面 | [Doc](./docs/adapters/desktop/notion.md) |
|
|
366
365
|
| **Discord** | Discord 桌面版 — 消息、频道、服务器 | [Doc](./docs/adapters/desktop/discord.md) |
|
|
367
366
|
| **Doubao** | 通过 CDP 控制豆包桌面应用 | [Doc](./docs/adapters/desktop/doubao-app.md) |
|
|
368
367
|
|
package/cli-manifest.json
CHANGED
|
@@ -15753,181 +15753,6 @@
|
|
|
15753
15753
|
"sourceFile": "notebooklm/summary.js",
|
|
15754
15754
|
"navigateBefore": false
|
|
15755
15755
|
},
|
|
15756
|
-
{
|
|
15757
|
-
"site": "notion",
|
|
15758
|
-
"name": "export",
|
|
15759
|
-
"description": "Export the current Notion page as Markdown",
|
|
15760
|
-
"access": "read",
|
|
15761
|
-
"domain": "localhost",
|
|
15762
|
-
"strategy": "ui",
|
|
15763
|
-
"browser": true,
|
|
15764
|
-
"args": [
|
|
15765
|
-
{
|
|
15766
|
-
"name": "output",
|
|
15767
|
-
"type": "str",
|
|
15768
|
-
"required": false,
|
|
15769
|
-
"help": "Output file (default: /tmp/notion-export.md)"
|
|
15770
|
-
}
|
|
15771
|
-
],
|
|
15772
|
-
"columns": [
|
|
15773
|
-
"Status",
|
|
15774
|
-
"File"
|
|
15775
|
-
],
|
|
15776
|
-
"type": "js",
|
|
15777
|
-
"modulePath": "notion/export.js",
|
|
15778
|
-
"sourceFile": "notion/export.js",
|
|
15779
|
-
"navigateBefore": true
|
|
15780
|
-
},
|
|
15781
|
-
{
|
|
15782
|
-
"site": "notion",
|
|
15783
|
-
"name": "favorites",
|
|
15784
|
-
"description": "List pages from the Notion Favorites section in the sidebar",
|
|
15785
|
-
"access": "read",
|
|
15786
|
-
"domain": "localhost",
|
|
15787
|
-
"strategy": "ui",
|
|
15788
|
-
"browser": true,
|
|
15789
|
-
"args": [],
|
|
15790
|
-
"columns": [
|
|
15791
|
-
"Index",
|
|
15792
|
-
"Title",
|
|
15793
|
-
"Icon"
|
|
15794
|
-
],
|
|
15795
|
-
"type": "js",
|
|
15796
|
-
"modulePath": "notion/favorites.js",
|
|
15797
|
-
"sourceFile": "notion/favorites.js",
|
|
15798
|
-
"navigateBefore": true
|
|
15799
|
-
},
|
|
15800
|
-
{
|
|
15801
|
-
"site": "notion",
|
|
15802
|
-
"name": "new",
|
|
15803
|
-
"description": "Create a new page in Notion",
|
|
15804
|
-
"access": "write",
|
|
15805
|
-
"domain": "localhost",
|
|
15806
|
-
"strategy": "ui",
|
|
15807
|
-
"browser": true,
|
|
15808
|
-
"args": [
|
|
15809
|
-
{
|
|
15810
|
-
"name": "title",
|
|
15811
|
-
"type": "str",
|
|
15812
|
-
"required": false,
|
|
15813
|
-
"positional": true,
|
|
15814
|
-
"help": "Page title (optional)"
|
|
15815
|
-
}
|
|
15816
|
-
],
|
|
15817
|
-
"columns": [
|
|
15818
|
-
"Status"
|
|
15819
|
-
],
|
|
15820
|
-
"type": "js",
|
|
15821
|
-
"modulePath": "notion/new.js",
|
|
15822
|
-
"sourceFile": "notion/new.js",
|
|
15823
|
-
"navigateBefore": true
|
|
15824
|
-
},
|
|
15825
|
-
{
|
|
15826
|
-
"site": "notion",
|
|
15827
|
-
"name": "read",
|
|
15828
|
-
"description": "Read the content of the currently open Notion page",
|
|
15829
|
-
"access": "read",
|
|
15830
|
-
"domain": "localhost",
|
|
15831
|
-
"strategy": "ui",
|
|
15832
|
-
"browser": true,
|
|
15833
|
-
"args": [],
|
|
15834
|
-
"columns": [
|
|
15835
|
-
"Title",
|
|
15836
|
-
"Content"
|
|
15837
|
-
],
|
|
15838
|
-
"type": "js",
|
|
15839
|
-
"modulePath": "notion/read.js",
|
|
15840
|
-
"sourceFile": "notion/read.js",
|
|
15841
|
-
"navigateBefore": true
|
|
15842
|
-
},
|
|
15843
|
-
{
|
|
15844
|
-
"site": "notion",
|
|
15845
|
-
"name": "search",
|
|
15846
|
-
"description": "Search pages and databases in Notion via Quick Find (Cmd+P)",
|
|
15847
|
-
"access": "read",
|
|
15848
|
-
"domain": "localhost",
|
|
15849
|
-
"strategy": "ui",
|
|
15850
|
-
"browser": true,
|
|
15851
|
-
"args": [
|
|
15852
|
-
{
|
|
15853
|
-
"name": "query",
|
|
15854
|
-
"type": "str",
|
|
15855
|
-
"required": true,
|
|
15856
|
-
"positional": true,
|
|
15857
|
-
"help": "Search query"
|
|
15858
|
-
}
|
|
15859
|
-
],
|
|
15860
|
-
"columns": [
|
|
15861
|
-
"Index",
|
|
15862
|
-
"Title"
|
|
15863
|
-
],
|
|
15864
|
-
"type": "js",
|
|
15865
|
-
"modulePath": "notion/search.js",
|
|
15866
|
-
"sourceFile": "notion/search.js",
|
|
15867
|
-
"navigateBefore": true
|
|
15868
|
-
},
|
|
15869
|
-
{
|
|
15870
|
-
"site": "notion",
|
|
15871
|
-
"name": "sidebar",
|
|
15872
|
-
"description": "List pages and databases from the Notion sidebar",
|
|
15873
|
-
"access": "read",
|
|
15874
|
-
"domain": "localhost",
|
|
15875
|
-
"strategy": "ui",
|
|
15876
|
-
"browser": true,
|
|
15877
|
-
"args": [],
|
|
15878
|
-
"columns": [
|
|
15879
|
-
"Index",
|
|
15880
|
-
"Title"
|
|
15881
|
-
],
|
|
15882
|
-
"type": "js",
|
|
15883
|
-
"modulePath": "notion/sidebar.js",
|
|
15884
|
-
"sourceFile": "notion/sidebar.js",
|
|
15885
|
-
"navigateBefore": true
|
|
15886
|
-
},
|
|
15887
|
-
{
|
|
15888
|
-
"site": "notion",
|
|
15889
|
-
"name": "status",
|
|
15890
|
-
"description": "Check active CDP connection to Notion Desktop",
|
|
15891
|
-
"access": "read",
|
|
15892
|
-
"domain": "localhost",
|
|
15893
|
-
"strategy": "ui",
|
|
15894
|
-
"browser": true,
|
|
15895
|
-
"args": [],
|
|
15896
|
-
"columns": [
|
|
15897
|
-
"Status",
|
|
15898
|
-
"Url",
|
|
15899
|
-
"Title"
|
|
15900
|
-
],
|
|
15901
|
-
"type": "js",
|
|
15902
|
-
"modulePath": "notion/status.js",
|
|
15903
|
-
"sourceFile": "notion/status.js",
|
|
15904
|
-
"navigateBefore": true
|
|
15905
|
-
},
|
|
15906
|
-
{
|
|
15907
|
-
"site": "notion",
|
|
15908
|
-
"name": "write",
|
|
15909
|
-
"description": "Append text content to the currently open Notion page",
|
|
15910
|
-
"access": "write",
|
|
15911
|
-
"domain": "localhost",
|
|
15912
|
-
"strategy": "ui",
|
|
15913
|
-
"browser": true,
|
|
15914
|
-
"args": [
|
|
15915
|
-
{
|
|
15916
|
-
"name": "text",
|
|
15917
|
-
"type": "str",
|
|
15918
|
-
"required": true,
|
|
15919
|
-
"positional": true,
|
|
15920
|
-
"help": "Text to append to the page"
|
|
15921
|
-
}
|
|
15922
|
-
],
|
|
15923
|
-
"columns": [
|
|
15924
|
-
"Status"
|
|
15925
|
-
],
|
|
15926
|
-
"type": "js",
|
|
15927
|
-
"modulePath": "notion/write.js",
|
|
15928
|
-
"sourceFile": "notion/write.js",
|
|
15929
|
-
"navigateBefore": true
|
|
15930
|
-
},
|
|
15931
15756
|
{
|
|
15932
15757
|
"site": "nowcoder",
|
|
15933
15758
|
"name": "companies",
|
|
@@ -22711,7 +22536,9 @@
|
|
|
22711
22536
|
"retweets",
|
|
22712
22537
|
"bookmarks",
|
|
22713
22538
|
"created_at",
|
|
22714
|
-
"url"
|
|
22539
|
+
"url",
|
|
22540
|
+
"has_media",
|
|
22541
|
+
"media_urls"
|
|
22715
22542
|
],
|
|
22716
22543
|
"type": "js",
|
|
22717
22544
|
"modulePath": "twitter/bookmark-folder.js",
|
|
@@ -22772,7 +22599,9 @@
|
|
|
22772
22599
|
"retweets",
|
|
22773
22600
|
"bookmarks",
|
|
22774
22601
|
"created_at",
|
|
22775
|
-
"url"
|
|
22602
|
+
"url",
|
|
22603
|
+
"has_media",
|
|
22604
|
+
"media_urls"
|
|
22776
22605
|
],
|
|
22777
22606
|
"type": "js",
|
|
22778
22607
|
"modulePath": "twitter/bookmarks.js",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
2
|
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { TWITTER_BEARER_TOKEN, applyTopByEngagement } from './utils.js';
|
|
4
|
-
import { resolveTwitterQueryId } from './shared.js';
|
|
4
|
+
import { extractMedia, resolveTwitterQueryId } from './shared.js';
|
|
5
5
|
|
|
6
6
|
// Companion to bookmark-folders.js: reads tweets inside a single folder.
|
|
7
7
|
// X exposes folder contents through a separate timeline operation
|
|
@@ -54,7 +54,7 @@ function buildFolderTimelineUrl(queryId, folderId, count, cursor) {
|
|
|
54
54
|
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function extractFolderTweet(result, seen) {
|
|
57
|
+
export function extractFolderTweet(result, seen) {
|
|
58
58
|
if (!result) return null;
|
|
59
59
|
const tw = result.tweet || result;
|
|
60
60
|
const legacy = tw.legacy || {};
|
|
@@ -72,6 +72,7 @@ function extractFolderTweet(result, seen) {
|
|
|
72
72
|
bookmarks: legacy.bookmark_count || 0,
|
|
73
73
|
created_at: legacy.created_at || '',
|
|
74
74
|
url: screenName ? `https://x.com/${screenName}/status/${tw.rest_id}` : `https://x.com/i/status/${tw.rest_id}`,
|
|
75
|
+
...extractMedia(legacy),
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -129,7 +130,7 @@ cli({
|
|
|
129
130
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of bookmarks to return (default 20).' },
|
|
130
131
|
{ name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the folder by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API\'s native (saved-time) ordering.' },
|
|
131
132
|
],
|
|
132
|
-
columns: ['id', 'author', 'text', 'likes', 'retweets', 'bookmarks', 'created_at', 'url'],
|
|
133
|
+
columns: ['id', 'author', 'text', 'likes', 'retweets', 'bookmarks', 'created_at', 'url', 'has_media', 'media_urls'],
|
|
133
134
|
func: async (page, kwargs) => {
|
|
134
135
|
const folderId = String(kwargs['folder-id'] || '').trim();
|
|
135
136
|
if (!folderId || !FOLDER_ID_PATTERN.test(folderId)) {
|
|
@@ -184,6 +185,7 @@ cli({
|
|
|
184
185
|
|
|
185
186
|
export const __test__ = {
|
|
186
187
|
parseBookmarkFolderTimeline,
|
|
188
|
+
extractFolderTweet,
|
|
187
189
|
buildFolderTimelineUrl,
|
|
188
190
|
FOLDER_ID_PATTERN,
|
|
189
191
|
};
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
3
|
import { __test__ } from './bookmark-folder.js';
|
|
4
4
|
|
|
5
|
-
const { parseBookmarkFolderTimeline, buildFolderTimelineUrl, FOLDER_ID_PATTERN } = __test__;
|
|
5
|
+
const { parseBookmarkFolderTimeline, extractFolderTweet, buildFolderTimelineUrl, FOLDER_ID_PATTERN } = __test__;
|
|
6
6
|
|
|
7
7
|
describe('twitter bookmark-folder URL builder', () => {
|
|
8
8
|
it('embeds the folder id and count in the variables payload', () => {
|
|
@@ -97,6 +97,8 @@ describe('twitter bookmark-folder timeline parser', () => {
|
|
|
97
97
|
bookmarks: 3,
|
|
98
98
|
created_at: 'Tue Mar 17 09:00:00 +0000 2026',
|
|
99
99
|
url: 'https://x.com/alice/status/1',
|
|
100
|
+
has_media: false,
|
|
101
|
+
media_urls: [],
|
|
100
102
|
},
|
|
101
103
|
]);
|
|
102
104
|
expect(nextCursor).toBe('NEXT_CURSOR');
|
|
@@ -247,6 +249,62 @@ describe('twitter bookmark-folder timeline parser', () => {
|
|
|
247
249
|
it('returns empty array + null cursor for unknown envelope', () => {
|
|
248
250
|
expect(parseBookmarkFolderTimeline({}, new Set())).toEqual({ tweets: [], nextCursor: null });
|
|
249
251
|
});
|
|
252
|
+
|
|
253
|
+
it('includes photo media URLs from extended_entities', () => {
|
|
254
|
+
const tweet = extractFolderTweet({
|
|
255
|
+
rest_id: '101',
|
|
256
|
+
legacy: {
|
|
257
|
+
full_text: 'pic folder tweet',
|
|
258
|
+
extended_entities: {
|
|
259
|
+
media: [
|
|
260
|
+
{ type: 'photo', media_url_https: 'https://pbs.twimg.com/media/abc.jpg' },
|
|
261
|
+
{ type: 'photo', media_url_https: 'https://pbs.twimg.com/media/def.jpg' },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
core: { user_results: { result: { legacy: { screen_name: 'eve' } } } },
|
|
266
|
+
}, new Set());
|
|
267
|
+
expect(tweet?.has_media).toBe(true);
|
|
268
|
+
expect(tweet?.media_urls).toEqual([
|
|
269
|
+
'https://pbs.twimg.com/media/abc.jpg',
|
|
270
|
+
'https://pbs.twimg.com/media/def.jpg',
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('extracts mp4 variant URL for video media', () => {
|
|
275
|
+
const tweet = extractFolderTweet({
|
|
276
|
+
rest_id: '102',
|
|
277
|
+
legacy: {
|
|
278
|
+
full_text: 'video folder tweet',
|
|
279
|
+
extended_entities: {
|
|
280
|
+
media: [{
|
|
281
|
+
type: 'video',
|
|
282
|
+
media_url_https: 'https://pbs.twimg.com/amplify_video_thumb/thumb.jpg',
|
|
283
|
+
video_info: {
|
|
284
|
+
variants: [
|
|
285
|
+
{ content_type: 'application/x-mpegURL', url: 'https://video.twimg.com/playlist.m3u8' },
|
|
286
|
+
{ content_type: 'video/mp4', bitrate: 832000, url: 'https://video.twimg.com/low.mp4' },
|
|
287
|
+
{ content_type: 'video/mp4', bitrate: 2176000, url: 'https://video.twimg.com/high.mp4' },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
}],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
core: { user_results: { result: { legacy: { screen_name: 'frank' } } } },
|
|
294
|
+
}, new Set());
|
|
295
|
+
expect(tweet?.has_media).toBe(true);
|
|
296
|
+
expect(tweet?.media_urls?.[0]).toMatch(/\.mp4$/);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('returns has_media false / media_urls empty when no media present', () => {
|
|
300
|
+
const tweet = extractFolderTweet({
|
|
301
|
+
rest_id: '103',
|
|
302
|
+
legacy: { full_text: 'text only', favorite_count: 0, retweet_count: 0, bookmark_count: 0 },
|
|
303
|
+
core: { user_results: { result: { legacy: { screen_name: 'gail' } } } },
|
|
304
|
+
}, new Set());
|
|
305
|
+
expect(tweet?.has_media).toBe(false);
|
|
306
|
+
expect(tweet?.media_urls).toEqual([]);
|
|
307
|
+
});
|
|
250
308
|
});
|
|
251
309
|
|
|
252
310
|
describe('twitter bookmark-folder id validation', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
2
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { extractMedia } from './shared.js';
|
|
3
4
|
import { TWITTER_BEARER_TOKEN, applyTopByEngagement } from './utils.js';
|
|
4
5
|
const BOOKMARKS_QUERY_ID = 'Fy0QMy4q_aZCpkO0PnyLYw';
|
|
5
6
|
const MAX_PAGINATION_PAGES = 100;
|
|
@@ -42,7 +43,7 @@ function buildBookmarksUrl(count, cursor) {
|
|
|
42
43
|
+ `?variables=${encodeURIComponent(JSON.stringify(vars))}`
|
|
43
44
|
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
44
45
|
}
|
|
45
|
-
function extractBookmarkTweet(result, seen) {
|
|
46
|
+
export function extractBookmarkTweet(result, seen) {
|
|
46
47
|
if (!result)
|
|
47
48
|
return null;
|
|
48
49
|
const tw = result.tweet || result;
|
|
@@ -64,9 +65,10 @@ function extractBookmarkTweet(result, seen) {
|
|
|
64
65
|
bookmarks: legacy.bookmark_count || 0,
|
|
65
66
|
created_at: legacy.created_at || '',
|
|
66
67
|
url: `https://x.com/${screenName}/status/${tw.rest_id}`,
|
|
68
|
+
...extractMedia(legacy),
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
|
-
function parseBookmarks(data, seen) {
|
|
71
|
+
export function parseBookmarks(data, seen) {
|
|
70
72
|
const tweets = [];
|
|
71
73
|
let nextCursor = null;
|
|
72
74
|
const instructions = data?.data?.bookmark_timeline_v2?.timeline?.instructions
|
|
@@ -111,7 +113,7 @@ cli({
|
|
|
111
113
|
{ name: 'limit', type: 'int', default: 20, help: 'Maximum number of bookmarks to return (default 20).' },
|
|
112
114
|
{ name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the bookmarks by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API\'s native (saved-time) ordering.' },
|
|
113
115
|
],
|
|
114
|
-
columns: ['id', 'author', 'text', 'likes', 'retweets', 'bookmarks', 'created_at', 'url'],
|
|
116
|
+
columns: ['id', 'author', 'text', 'likes', 'retweets', 'bookmarks', 'created_at', 'url', 'has_media', 'media_urls'],
|
|
115
117
|
func: async (page, kwargs) => {
|
|
116
118
|
const limit = kwargs.limit || 20;
|
|
117
119
|
const cookies = await page.getCookies({ url: 'https://x.com' });
|
|
@@ -174,3 +176,7 @@ cli({
|
|
|
174
176
|
return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);
|
|
175
177
|
},
|
|
176
178
|
});
|
|
179
|
+
export const __test__ = {
|
|
180
|
+
parseBookmarks,
|
|
181
|
+
extractBookmarkTweet,
|
|
182
|
+
};
|