List, add, remove subscription sources, and browse the articles inside each source. Sources are typed MP(WeChat Official Account),WEBSITE(website) or X(Twitter / Twitter platform account).
⚠️ About the ambiguity of the letter "X" (must read first)
In this document, X has twocompletely differentmeanings; do not confuse them:
--type X— this is a real sourceType enum value, specifically referring to Twitter / Twitter / X platform account。- placeholder X — when users colloquially say "subscribe to X", "unsubscribe from X", "recent articles of X official account", the X here is usually just a placeholder code name, referring toa specific account(it could be an official account, a website, or just something with X in its name),unrelated to Twitter。
Determination rule:
- Only when the user **explicitly mentions "Twitter / Twitter / X platform"** should you use
--type X。 - When the user says "subscribe to X", "X official account", "read X's articles" without mentioning the Twitter platform,do notinfer
--type Xfrom this; the X here is a placeholder and should be handled according to its actual type (mostlyMPorWEBSITE).
Prerequisites
- Installation:
curl -fsSL https://raw.githubusercontent.com/SupSub-AI/supsub-cli/master/scripts/install.sh | bash(native installation, installed to~/.local, supports background auto-update); or package managernpm i -g @supsub/cli/pnpm add -g @supsub/cli - Logged in:
supsub auth statusshows Authenticated (on first use, firstsupsub auth login) - When unauthorized (exit 2 / UNAUTHORIZED), do not stop at "you are not logged in": directly run
supsub auth loginto open browser authorization for the user (the command will automatically open the browser and block waiting for authorization; please use a sufficiently long timeout, such as 10 minutes; the user only needs to click confirm in the browser and does not need to enter anything in the terminal), and retry the original command after successful authorization. In a browserless / headless environment, fall back to prompting the userSUPSUB_NO_BROWSER=1 supsub auth login。
Commands
List subscriptions
supsub sub list [--type <MP|WEBSITE|X>]
| Flag | Default | Description |
|---|---|---|
--type |
— (all) | Filter source type:MP(official account) / WEBSITE(website) / X(Twitter) |
Each row includes: sourceId, sourceType, name, img, description, unreadCount. In table mode, the 类型 column displays sourceType in Chinese:MP→official account,WEBSITE→website,X→Twitter.
# 列出全部订阅
supsub sub list
# 仅看公众号
supsub sub list --type MP
# 仅看网站
supsub sub list --type WEBSITE
# 仅看推特(X 平台账号)
supsub sub list --type X
# 导出 JSON
supsub sub list -o json
JSON shape: {"success":true,"data":[{"sourceType":"MP","sourceId":12345,"name":"...","img":"...","description":"...","unreadCount":3}, ...]}
Add a subscription
sub add has two mutually exclusive entry points, corresponding to two kinds of "ID forms obtained":
# A) 全局搜索 / 已收录源 → 用内部正整数 sourceId
supsub sub add --source-id <id> --type <MP|WEBSITE|X> [--group <gid>]...
# B) mp search 发现的微信原生公众号 → 用 base64 字符串 mpId
supsub sub add --mp-id <mpId> [--type MP] [--group <gid>]...
| Flag | Required | Description |
|---|---|---|
--source-id |
choose one of the two | source ID (positive integer). From supsub search / supsub sub list 's sourceId |
--mp-id |
choose one of the two | official account mpId (base64 string). From supsub mp search returned result |
--type |
see description | --source-id required in mode, take MP / WEBSITE / X(Twitter);--mp-id optional in mode, if provided it must be MP |
--group |
no | group ID (number, multiple can be specified repeatedly) |
mutually exclusive:
--source-idand--mp-idmust be exactly one of the two. Providing both / neither will throwINVALID_ARGS。The two paths use different endpoints:
--source-id→POST /api/subscriptions(subscription to an already indexed source);--mp-id→POST /api/mps(Add and subscribe to the new official account by WeChat native ID).
# 路径 A:sub list / search 看到的内部 sourceId(公众号)
supsub sub add --source-id 12345 --type MP
# 路径 A:网站 + 多分组
supsub sub add --source-id 67890 --type WEBSITE --group 1 --group 2
# 路径 A:推特账号(用户明确说要订阅某个 Twitter / X 平台账号时)
supsub sub add --source-id 24680 --type X
# 路径 B:mp search 拿到的 mpId(base64)
supsub sub add --mp-id "MzkyNTYzODk0NQ=="
# 路径 B + 分组
supsub sub add --mp-id "MzkyNTYzODk0NQ==" --group 3
# JSON 输出
supsub sub add --source-id 12345 --type MP -o json
JSON shape: {"success":true,"data":{"message":"..."}}
--source-idMust be a positive integer (non-numeric →INVALID_ARGS);--mp-idis a string, no format validation, adjudicated by the backend;--groupaccepts multiple numeric IDs, non-numeric will reportINVALID_ARGS(exit 64)。
Remove a subscription
supsub sub remove --source-id <id> --type <MP|WEBSITE|X>
| Flag | Required | Description |
|---|---|---|
--source-id |
yes | Source ID (positive integer) |
--type |
yes | MP(official account) / WEBSITE(website) / X(Twitter) |
⚠️ Unsubscribing is a destructive operation; you must confirm with the user a second time before executing: restate the source name (taken from
sub list), and only execute after obtaining explicit consent. Like mark-read,the CLI will not ask again—when handing the command to the user to run in their own terminal, do not say "it will prompt for confirmation after execution."⚠️ Unsubscribing will also remove the source from all groups it belongs to(verified: after subscribing and joining a group, then unsubscribing,
group subsreturns empty). The groups themselves are unaffected. Re-subscribingwill notautomatically restore the original group membership; you need to rungroup add-subagain. Therefore, when the user says "unsubscribe first, then subscribe back later," remind them that group membership will be lost.
# 取消订阅某个公众号(需用户确认后执行)
supsub sub remove --source-id 12345 --type MP
# 取消订阅某个网站
supsub sub remove --source-id 67890 --type WEBSITE -o json
# 取消订阅某个推特账号
supsub sub remove --source-id 24680 --type X
JSON shape: {"success":true,"data":{"message":"..."}}
Browse articles in a subscription
supsub sub contents --source-id <id> --type <MP|WEBSITE|X> [--unread | --all] [--brief] [--page <n>] [--page-size <n>]
| Flag | Default | Description |
|---|---|---|
--source-id |
required | Source ID (positive integer).Alias --id(aligned with focus contents --id , the two forms are equivalent) |
--type |
required | MP(official account) / WEBSITE(website) / X(Twitter) |
--unread |
(default) | Return only unread articles |
--all |
— | Return all articles (read + unread) |
--brief |
— | Condensed output: keep only contentId / title / publishedAt(+Text) / isRead |
--page |
1 | Page number (positive integer) |
--page-size |
20 | Number of items per page, 1-100 |
⚠️
--unreadand--allare mutually exclusive: specifying both will throwINVALID_ARGS. When neither is passed, the default behavior is equivalent to--unread。
⚠️ "Recent" does not equal "unread"—choose the flag according to the user's intent; don't let the default make the decision for the user:
| User's wording | Intent | Which to use |
|---|---|---|
| "Take a look at the recent content of X" "What's new with X recently" "What has X been talking about" "Has X updated" | Look at what this source is talking about by time | --all,isRead used only toannotate("3 of them you haven't seen yet"),do not use it to filter |
| "What else of X haven't I seen" "X's unread" "How many of X are left unread" | Explicitly want unread | Default (--unread) |
| Unclear | —— | --all + annotate which are unread |
Using the default to handle "take a look at the recent content of X" will hit this pitfall:When the source has already been fully read, it returns an empty array, so the reply is "no content"— but what the user asked about was "recent," not "unread." Only when the user's words contain "unread / haven't seen / haven't read / clear unread" should you filter.
For batch enumeration always add --brief: by default every output item includes summary / coverImage / tags, 100 items is about 90KB,
enough to blow past the tool output limit and be forced to write to disk;--brief for the same 100 items is only about 14KB. Use the full output only when you need to explain the content to the user.
Fields per line:contentId, url, title, coverImage, tags[], summary, publishedAt, publishedAtText, isRead。
--brief when there is only contentId, title, publishedAt, publishedAtText, isRead。
publishedAtis a Unix second-level timestamp (in a few backend versions it is a string),publishedAtTextis already formatted by the CLI"YYYY-MM-DD HH:mm", just use it directly, don't write your owndate -r/ timestamp conversion. For sorting, still usepublishedAt。
# 默认(未读)—— 看某个公众号里有哪些文章
supsub sub contents --source-id 12345 --type MP
# 全部文章(含已读)
supsub sub contents --source-id 12345 --type MP --all
# 看更早的文章:翻到第 2 页
supsub sub contents --source-id 12345 --type MP --page 2
# 一次多拿一些(上限 100)
supsub sub contents --source-id 12345 --type MP --page-size 100 -o json
# 全部文章 + JSON
supsub sub contents --source-id 12345 --type MP --all -o json
JSON shape: {"success":true,"data":[{"contentId":"...","url":"...","title":"...","coverImage":"...","tags":[...],"summary":"...","publishedAt":<timestamp 或字符串>,"isRead":false}, ...]}
publishedAt's raw form varies by backend version (Unix seconds number /"YYYY-MM-DD HH:mm:ss"string), no need to handle compatibility yourself—just read thepublishedAtTextfilled in by the CLI (always"YYYY-MM-DD HH:mm")。Pagination strategy: for browsing-type requests, only fetch page 1 and report the total; for "all / stats / export"-type requests, use
--page-size 100to automatically loop through pages (returned count < page-size means last page), don't ask the user page by page. Seesupsub-unread。
Mark a source as read (mark entire source as read)
supsub sub mark-read --source-id <id> --type <MP|WEBSITE|X> --all
| Flag | Required | Description |
|---|---|---|
--source-id |
yes | Source ID (positive integer) |
--type |
yes | MP / WEBSITE / X(Twitter) |
--all |
yes | Confirm marking the entire source as read (irreversible) |
⚠️ Only entire-source read is supported, there is no single-article read. articles have no "detail / read" step, so no natural single-article read event can be produced, therefore the CLI only keeps the user's explicit "I've finished reading this account". Passing
--content-idwill throwINVALID_ARGS. When the user says "mark this article as read", truthfully explain that it's not supported, and ask whether they want to mark the entire source as read.⚠️
--allis required and irreversible(the CLI has no mark-as-unread). Before executing, you mustdouble-confirmwith the user: restate the source name + current unread count (taken fromsub list/supsub unread), and only execute after obtaining explicit consent.The CLI will not ask again: this command has no interactive confirmation prompt, it takes effect as soon as it's executed. If you hand the command to the user to run themselves in the terminal, don't say "the CLI will prompt for confirmation after execution"—that safety net doesn't exist.
# 整源已读(需用户确认后执行)
supsub sub mark-read --source-id 12345 --type MP --all -o json
JSON shape: {"success":true,"data":{"message":"已标记该订阅源全部内容为已读(不可逆)"}}
Agent Usage Notes
- When parsing data, always use
-o json; table output has truncation and column-width limits, so it's not suitable for downstream processing. - All JSON responses are in the
{"success":true,"data":<payload>}structure (fromsrc/ui/output.ts)。 --typevalue isMP/WEBSITE/X(the CLI internallytoUpperCase, case-insensitive); a common mistake is writingmp_account/wechat/rss/TWITTERwhich will reportINVALID_ARGS——Twitter only acceptsX, notTWITTER。- Once again emphasizing the ambiguity of the letter "X":
--type X= Twitter platform account. In users' colloquial speech, the X in "subscribe to X", "X official account", "read X's articles" is mostly a placeholder referring to some specific account,do notpass--type Xjust because the letter X appears; only use--type X。 - Before adding a subscription, think clearly about the ID source:
- from
supsub search/sub list's internal positive integersourceId→sub add --source-id <id> --type ... - from
supsub mp search's base64 stringmpId→sub add --mp-id <mpId>(type defaults to MP, can be omitted) - Don't cast
mp search'smpIdinto a number and stuff it into--source-id— that's a different ID space and will be rejected by the backend.
- from
sub contentsby default only shows unread — for time-based browsing requests like "take a look at X's recent content", "what articles does this account have", "what did X talk about", always add--all, setisReadTreat as an annotation rather than a filter condition; only use the default when the user explicitly says "unread / not viewed".sub contentsDefault is 20 per page, use--page/--page-size(max 100) to page through to earlier history.- Marking as read only has a whole-source level(
mark-read --all): there is no per-article read status, because articles do not have a "details / read" step.Whole-source read is irreversible, and you must confirm with the user a second time before executing. For a cross-source unread overview and the clear-unread workflow, seesupsub-unread。 - For group management (adding this source to a group, and which subscriptions are in a group), see
supsub-group; when subscribing, you can directly usesub add --group <gid>to add to a group in one step. - Duplicate subscription is not an error: in practice, running
sub addagain on the same source returns{"success":true,"data":{"message":"订阅成功"}},exit0(idempotent). Do not use this to determine "whether already subscribed"—to determine that, seesub listor theisSubscribed。Only duplicate unsubscription will report an error(订阅不存在,400 → exit1), the two are asymmetric. - Exit codes:
0OK,2UNAUTHORIZED,64INVALID_ARGS(--type/--source-id/--group/--page/--page-sizevalidation failure,--alland--unreadare mutually exclusive, mark-read did not provide--allor mistakenly passed--content-id),1business error (backend 4xx, such as source does not exist / not subscribed / duplicate unsubscription),10network error,11server error (5xx).