A "focus" is a topic-aggregated content stream in SupSub: it spans multiple sources (WeChat Official Accounts MP, websites WEBSITE, Twitter X) and gathers articles on the same topic together. This skill handlesviewingthe focus list,browsingthe content in a focus (distinguishing read/unread), anddeletinga focus.
Difference from
supsub-sub:submanages "subscription sources" (a specific Official Account/website, and the articles under it);focusmanages the "topic aggregation view" (a content stream across multiple sources). Do not confuse the two. Difference fromsupsub-search:searchdoes one-off full-text search by keyword;focusbrowses an existing, continuously updated topic stream.
Prerequisites
- Install:
curl -fsSL https://raw.githubusercontent.com/SupSub-AI/supsub-cli/master/scripts/install.sh | bash(native install, installed to~/.local, supports automatic background updates); 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, so use a sufficiently long timeout, e.g. 10 minutes; the user only needs to click confirm in the browser and does not need to type anything in the terminal), and retry the original command after authorization succeeds. In a browserless / headless environment, fall back to prompting the userSUPSUB_NO_BROWSER=1 supsub auth login。
Commands
List focuses (focus list)
supsub focus list
No parameters. Fields per line:id, icon(emoji), title, unreadCount。
# 列出全部关注点
supsub focus list
# JSON 输出
supsub focus list -o json
JSON shape: {"success":true,"data":[{"id":42,"icon":"🤖","title":"...","unreadCount":3}, ...]}
Read/unread: each focus's
unreadCountis the focus's current unread item count. When the user asks "which of my focuses still have unread items / how many are left unread in each focus", read this field directly; no need to go into contents.
Browse contents in a focus (view focus content)
supsub focus contents --id <focusId> [--unread | --all] [--brief] [--page <n>] [--page-size <n>]
| Flag | Default | Description |
|---|---|---|
--id |
required | Focus ID (positive integer), from supsub focus list 's id |
--unread |
(default) | Return only unread content |
--all |
— | Return all content (read + unread) |
--brief |
— | Condensed output: keep only contentId / title / sourceType / sourceName / publishedAt(+Text) / isRead |
--page |
1 | Page number (positive integer) |
--page-size |
20 | Items per page, 1-100 |
⚠️
--unreadand--allare mutually exclusive: specifying both throwsINVALID_ARGS. When neither is passed, the default behavior is equivalent to--unread。
⚠️ "Recent" is not the same as "unread": requests like "show me the recent content of X" or "what's recent in focus X" thatbrowse by timeshould use --all,
treat isRead asan annotation("N of these you haven't seen yet") rather than a filter condition; only when the user explicitly says "unread / not yet seen / how many are left unread"
should the default be used. Otherwise, once the focus has been read, it will return an empty array and you will reply "no content" — but the user asked about "recent", not "unread".
The focus aggregates multiple sources, so every line carries sourceType / sourceName, making it easy to see which account/website/Twitter this content comes from.
Fields per line:contentId, url, title, coverImage, keywords[], tags[], summary, sourceType, sourceName, publishedAt, isRead。
# 默认(仅未读)—— 等价于 --unread
supsub focus contents --id 42
# 全部内容(含已读)
supsub focus contents --id 42 --all
# 翻到第 2 页 / 一次多拿一些(上限 100)
supsub focus contents --id 42 --page 2
supsub focus contents --id 42 --page-size 100 -o json
# 全部 + JSON
supsub focus contents --id 42 --all -o json
JSON shape: {"success":true,"data":[{"contentId":"...","url":"...","title":"...","coverImage":"...","keywords":[...],"tags":[...],"summary":"...","sourceType":"MP","sourceName":"...","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 the CLI-populatedpublishedAtText(always"YYYY-MM-DD HH:mm")。sourceTypemay beMP/WEBSITE/X(Twitter).For batch enumeration always add
--brief: complete output has every entry withsummary/keywords/tags, about 90KB for 100 entries, which will exceed the tool output limit and be forced to disk;--brieflikewise about 14KB for 100 entries. Only use full output when you need to explain the content to the user.Pagination strategy: for browsing-type requests only fetch page 1 and report the total; for "all / statistics / export"-type requests use
--page-size 100to automatically loop through pages, do not ask the user page by page. Seesupsub-unread。
Mark a focus as read (mark the entire focus as read)
supsub focus mark-read --id <focusId> --all
| Flag | Required | Description |
|---|---|---|
--id |
yes | Focus ID (positive integer) |
--all |
yes | Confirm that the entire focus is marked as read (irreversible) |
⚠️ Only entire-focus read is supported; there is no single-article read. articles have no "detail / read" step, so a natural single-article read event cannot be produced, therefore the CLI only retains the user's explicit "I've finished reading this focus". Passing
--content-id/--type/--source-typewill throwINVALID_ARGS. When the user says "mark this article as read", truthfully explain that it is not supported.⚠️
--allis required and irreversible(no mark-as-unread). Before executing you mustdouble-confirmwith the user: restate the focus title + currentunreadCount, and only execute after obtaining explicit consent.The CLI will not ask again: this command (as well as
focus remove) has no interactive confirmation prompt; it takes effect as soon as it is executed. If you hand the command to the user to run in their own terminal, do not say "the CLI will prompt for confirmation after execution"—that safety net does not exist.
# 整个关注点已读(需用户确认后执行)
supsub focus mark-read --id 42 --all -o json
JSON shape: {"success":true,"data":{"message":"已标记该关注点全部内容为已读(不可逆)"}}
Remove a focus (delete a focus)
supsub focus remove --id <focusId>
| Flag | Required | Description |
|---|---|---|
--id |
yes | Focus ID (positive integer) |
supsub focus remove --id 42
supsub focus remove --id 42 -o json
JSON shape: {"success":true,"data":{"message":"..."}}
⚠️ Destructive and irreversible: after deletion the CLI has no command for adding a new focus(no
focus add), once deleted it cannot be recreated. Be sure to confirm with the user before executing.
Read / unread (important)
Read status is maintained by theserver side, and the CLI can both read and mark it (can only mark as read,cannot mark back to unread):
focus listeach item'sunreadCount= the number of unread entries for that focus. This is the fastest entry point for determining "which focus still has unread items".focus contentsdefault (or explicitly--unread) only returns unread entries;--allreturns all (read + unread).focus contentseach item'sisRead(boolean) = whether that entry is read: rendered in the table as thereadcolumn (read shows✓, unread is empty).- To mark as read use
focus mark-read --all: only "entire focus read" is available, there is no single-article read. - There is no command to unmark as read: if marked by mistake it cannot be restored in the CLI.
--alllevel operations are irreversible, and you must double-confirm with the user before executing. - For the cross-source + focus unread overview and the "clear unread" workflow, see
supsub-unread(supsub unread— see everything with one command).
Agent Usage Notes
- When parsing data, always use
-o json; table output has truncation and column width limits, and is not suitable for downstream processing. - All JSON responses are of the
{"success":true,"data":<payload>}structure (fromsrc/ui/output.ts)。 - Getting
focusIdthe only source ofsupsub focus listisidof--id; ifINVALID_ARGSis not a positive integer, it reports - When a user asks "what content/articles are in this focus", it usually means
--all(including read); when asking "what else haven't I read / unread", use the default--unread。 focus contentsdefaults to 20 items per page; use--page/--page-size(maximum 100) to page through and get earlier content.focus mark-readonly marks the entire focus as read (marking a single article as read is not supported: articles do not have a detail/read step);--allis required and irreversible, and you must confirm with the user a second time before executing。- This skill is not responsible for: adding, deleting, or querying subscription sources (→
supsub-sub), subscription groups (→supsub-group), cross-source unread overview and the clear unread workflow (→supsub-unread), keyword full-text search (→supsub-search), discovering new official accounts (→supsub-mp). focus has no add. - Exit codes:
0OK,2UNAUTHORIZED,64INVALID_ARGS(--id/--page/--page-sizevalidation failure,--alland--unreadare mutually exclusive, mark-read did not provide--allor a single-article parameter was mistakenly passed),1business error (backend 4xx, such as focus does not exist),10network error,11server error (5xx).