@302ai/media-studio-cli
CLI for AI Media Studio — drive media-studio chat from any terminal or external agent.
Install
npm install -g @302ai/media-studio-cli
# requires Node.js >= 22
Staging and Local are available only in prerelease versions. Region visibility follows the package version, not the npm dist-tag: a published prerelease exposes them, while a stable version installed from
@betadoes not.
If you installed via bun and get
command not found: bun's global bin dir is not on PATH by default. Run once:echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
Usage
msc login # authenticate in a browser
msc guide # the full agent-facing manual
msc whoami # show current auth status
msc chat "hi" # one chat turn (multi-turn by default)
msc sessions # list sessions from the server
msc skills # list the skill catalog you can reference
msc logout # clear stored credentials
media-studio-cli is the long alias for msc — the same program, both
installed. The docs, the help output, and every error message use the short
name:
msc chat "画一只猫" -r # -r/--show-reasoning = 展示模型推理过程
msc chat "hi" --json # machine-readable output
msc chat "hi" --session <id> # 切换到已有会话(先校验存在)并设为默认
msc chat "hi" --new # 新建会话并设为默认(id 不复用)
msc sessions # 列出所有会话(标记当前默认 `(current)`)
msc sessions --json # 机器可读的原始 JSON 数组
msc sessions use <id> # 校验存在后切换默认会话
msc sessions use --new # 新建会话并设为默认
msc skills # 列出技能库(--plaza 浏览技能广场)
msc skills add <slug> # 从广场安装技能(如 brainstorming,见 --plaza --json 的 slug)
msc skills rm <command> # 从技能库移除(-y/--yes 跳过确认)
msc skills enable <command> # 启用 / disable 停用
msc --help # all flags
Session deletion is not available in the CLI. To delete a session, open the corresponding session page in the Media Studio web app and delete it manually.
msc --help carries an agent-oriented quickstart — the auth prerequisite, the
three canonical calls, and the --json contract. msc guide prints the full
manual: every command, both JSON schemas, the nextStep values, exit codes,
stream discipline, and skill semantics. Point an agent at those two instead of
duplicating them here.
CLI compatibility and updates
Online CLI requests carry X-Agent-Cli-Version: <package version>. The BFF applies CMS CliVersionPolicy (cli-version-policy) to whoami, chat, sessions, skills and attachments. enabled defaults true; enabled=false is the emergency disable. Blank minSupportedVersion uses the CLI package version bundled in the BFF build (0.1.4 for this release); a custom stable SemVer can raise or lower the floor. Clearing the minimum restores the release baseline rather than disabling enforcement. Headerless Bearer requests are legacy CLI and normally receive 426 (prewarm exception below); headerless browser-cookie/anonymous requests are unaffected. This is not an authentication boundary. Local help, version, guide, logout and browser login remain available.
A below-floor request receives HTTP 426, CLI_UPGRADE_REQUIRED (10010) and nextStep: "upgrade_cli" before its upstream work or stream begins. This is an application convention, not an HTTP transport upgrade. CMS read failure uses a valid available cached policy or returns 503, not an upgrade refusal; do not interpret every whoami exit 1 as expired credentials. Policy caching uses save-triggered tag invalidation and periodic revalidation, not simultaneous cross-instance activation.
Older clients may not decode upgrade_cli; the server message still supplies both installation commands and requires permission first. Publishing a CLI does not deploy the BFF or change its bundled baseline: publish 0.1.4 and sync the matching public Agent Skill before deploying/activating the BFF build containing that manifest. See the operator activation procedure below.
The sole transport exception is an upgrade refusal for headerless Bearer POST /api/sessions/prewarm: HTTP 200 SSE event:error carries flat { err_code: 10010, message: <full permission-first commands> }, with no session_ready or upstream work. The old CLI exits 1 and exposes errCode: 10010 and the message, but no httpStatus. Stamped clients and all other interfaces retain 426; CMS failure remains 503 and is not converted to SSE.
Optional npm discovery uses a best-effort 24-hour cache and short timeout; discovery failures never fail a media task or change its exit status. Chat success/incomplete JSON may contain a stable updateAvailable: { current, latest, upgradeCommand, skillUpgradeCommand }; successful whoami may append a stable notice. Stable emissions are deduplicated per target version across both commands after output, not acknowledgment. Interactive terminal commands can also report beta updates for prerelease builds; beta notices are not deduplicated. A stable version installed via @beta cannot be identified as beta and receives no beta notice. Skills/sessions arrays and single-document JSON stdout stay unchanged. Agents finish the task before telling the user about an optional update and never silently install it.
Local notification deduplication is best-effort, not exactly-once: simultaneous processes or an unwritable cache can repeat notices.
Required upgrade recovery needs permission for both global installs, in order:
npm install -g @302ai/media-studio-cli@latest
npx -y skills add 302ai/msc-skill --skill msc-skill -g -y
msc --version
msc guide
Stop if either install fails and report completed steps; do not claim success or automatically roll back the other installation. Skill installation may not reload a running Agent's cached context, so read the guide before resuming. Retry only definitely unaccepted generation. A refusal covers only that request: earlier stages may already have effects. Reuse an error's sessionId with --session, not --new; inspect the existing session/artifacts when acceptance is uncertain. Policy changes must not terminate accepted work or established SSE streams.
Operators: see CLI minimum-version activation before changing the floor.
Headless environments cannot complete browser sign-in, so msc login refuses
immediately instead of waiting out the callback deadline: CI (CI set), and
Linux with neither DISPLAY nor WAYLAND_DISPLAY. Sign in from a machine
whose browser can reach the loopback callback.
Multi-turn conversation
chat is multi-turn by default — no flag needed. The CLI keeps a single
lastSessionId pointer in config.json (under the config dir):
- First-ever chat provisions a session via the same prewarm flow the web
app uses (source
cli), printing stage progress (creating / sandbox / skills / finishing), then chats. The pointer is written the moment the session is durably persisted upstream — an interrupted provision never loses a live session. - Later chats continue that session — upstream remembers the conversation by session id, so no history is resent.
- If the current session is gone before a turn produces any output, the CLI transparently provisions a fresh one and re-runs the turn — a first-turn failure never strands you.
- Once a turn has started producing output, a session failure is reported instead: the partial result stands, nothing is re-run, and no replacement session is created (a retry would replay output and re-dispatch tool calls or generation tasks the first attempt may already have started).
--session <id>switches to an existing session (including web-created ones) and makes it the default. The id is verified upstream first: a missing id fails with a hint, it never provisions a different session behind your back.--newcreates a fresh session with a newly generated id and makes it the default for later runs. Ids are never reused or adopted: the CLI either uses the id it generated or reports the refusal.
Manual proof of continuity:
msc chat "记住数字42"
msc chat "刚才的数字是几?" # answers 42
Point at a different server via msc login --region default|cn|local (the
server you log into is saved and reused; local — like staging — is a
prerelease-only region, see the note at the top).
Configuration and storage
The CLI stores credentials and local settings in:
- Default:
~/.config/media-studio-cli/ - XDG standard:
$XDG_CONFIG_HOME/media-studio-cli/ - Override:
MEDIA_STUDIO_CONFIG_DIR=/path/to/dir
Key files:
auth.json: stores active platform credentials and user session metadata (permissions0600on POSIX)config.json: stores CLI client settings, includinglastSessionIdpointer
auth.json's apiUrl is only accepted when its host is on the CLI allowlist:
the official hosts (which the rejection message lists) plus the local-dev
hosts localhost / 127.0.0.1 / ::1. A hand-edited URL outside that list is
refused before any request is sent, and the CLI reports it as a rejected
credential naming the URL — not as "not logged in", since the token itself may
be fine. msc login rewrites the file with a server-supplied URL and is the
fix; the refused URL is never used, and base-URL resolution falls back to the
official default host.
In --json mode the two auth states are distinguishable without reading the
message: a rejected credential carries "nextStep": "relogin" (and its
guidance), while no credential at all reports status and message only.
Skills
Discover what your account can reference, then reference it — the reference must name a skill that exists in your catalog:
msc skills # list your library
msc skills --json # raw catalog array (see fields below)
msc skills --plaza # browse the Skill Plaza with your library
# state merged in (built-ins excluded)
msc skills --plaza --json
msc skills add <slug> # install a plaza entry by slug
msc skills rm <command> # remove a library skill by command
msc skills enable <command> # flip a library skill's enabled flag
msc skills disable <command>
add takes the entry's plaza slug (brainstorming, shown in
msc skills --plaza --json as slug); the library mutations take the
command string (brainstorming and /brainstorming are the same entry) —
the same string the prompt uses to reference the skill.
--json is the intended output for agents: the catalog is the input to a
decision the calling program makes itself. Both descriptionEn/descriptionZh
are passed through untouched — upstream does not guarantee the En slot is
English, so the CLI picks nothing. The human rendering lists names only (the
command string is available in --json) with the full descriptionEn on a
second line, and marks [on] enabled / [off] disabled / [-] not installed
in the plaza view.
Shared API endpoints: mutations use the exact same endpoints as the web app
(POST /api/skills for installs, DELETE /api/skills?skillId= for removal,
POST /api/skills/enabled for enable/disable). The CLI resolves the slug/command
to the entry/id before calling the endpoint. rm requires --yes outside a
terminal. Built-in entries are rejected (422): they sync into your library from
the backend. Managing skills (upload, cover) stays in the Media Studio web app.
Then reference a skill in the prompt and the CLI installs it into the session sandbox on demand:
msc chat "/always-hello 不管我说什么" # leading slash command
msc chat "%%/always-hello%%不管我说什么" # web composer marker, paste-compatible
Both spellings resolve to the same thing. The upstream installs a skill only
from the turn's skill_urls field, so the CLI looks the reference up in your
catalog (GET /api/skills) and sends the matching package URL — prompt text
alone triggers nothing.
- A bare
/commandcounts only at the start of the prompt, so mid-sentence slashes stay prose (check /etc/hostsis not a skill reference). The%%…%%marker is also recognized anywhere — a web prompt pasted verbatim keeps its token — but the skill activates upstream only when the marker leads the message, so lead the prompt with it. The leading token can be any bare slug, single-segment included —/tmp what is thisresolves/tmp— and a reference that matches nothing fails the turn with the catalog listed, so a typo never silently degrades to a skill-less answer. - The catalog is fetched only when the prompt references a skill; skill-free turns cost no extra request.
- Unknown skills fail the turn with a non-zero exit before any session is
provisioned. A disabled skill is auto-enabled first — the same ADR-0001
behavior as the web's "Use in Chat" — because the upstream treats
enabledas a hard precondition for invocation and would otherwise download the package and silently never run it. The enable flips the skill'senabledflag on your account (a persistent change), is reported asAuto-enabled for this turn: …, and a failed enable aborts the turn.
Agent Skill (302ai/msc-skill)
To teach an external AI agent (Claude Code, Cursor, Codex, OpenCode, Gemini CLI, etc.) how and when to invoke msc, install the public Agent Skill:
npx -y skills add 302ai/msc-skill --skill msc-skill -g -y
For opt-in beta, pair the @beta CLI with the beta Skill installation. Installing another channel replaces the same global CLI or Skill; ask permission before switching. The public beta branch is created by the first successful beta sync.
- Source of truth: The canonical skill source lives in this repository under
msc-skill/. A verified CLI Release synchronizes its matching channel to 302ai/msc-skill; compatible Skill-only changes and retries use manual dispatch. - Contract co-evolution: When modifying CLI commands,
--jsonpayloads, exit codes, error structures (nextStep), authentication behavior, or file attachment syntax inmedia-studio-cli, always updatemsc-skill/skills/msc-skill/SKILL.mdin the same commit.
Attachments
Attach local files to a chat turn using @path anywhere in the prompt:
msc chat "看看 @cover.png 这张图" # bare path (ends at whitespace)
msc chat "总结 @/abs/path/notes.pdf" # absolute path
msc chat '对比 @"my docs/report.docx" 内容' # quoted path with spaces
The CLI mirrors the web composer's upload flow:
- Validates each referenced file locally (exists, readable, supported extension, under 50 MB) before any network call.
- Uploads each file via the web app's
POST /api/chat/attachmentsendpoint with a terminal spinner (Uploading attachment 1/2: cover.png). - Rewrites the prompt to inline the remote URL using the web's
&&<url>&&marker, which the upstream model and web session history natively understand. - Up to 10 unique attachments per turn (same as the web composer); referencing the same file twice uploads it once.
- An unattachable reference (missing file, unsupported format) fails the turn before any session is provisioned.
- Prose mentions (
@team 辛苦了, email addresses) stay literal text — only tokens with a file extension or path slash are treated as attachments. @pathis local-filesystem-only:@https://example.com/a.pngfails withno such file. There is no@urlform — remote URLs are not attachments.
In --json mode the uploaded attachments are surfaced under the attachments array:
[{ name, type, size, remoteUrl }].
Staging server (expired TLS certificate)
The staging server's certificate is expired (ops declines renewal), so the
CLI skips TLS certificate verification only while pointed at the staging
host; the server choice itself is the opt-in, and the default / CN hosts
always verify. The interactive msc login region list shows each server's
URL and marks Staging with the auto-off behavior; msc login --region staging opts in.
Stable (production) builds hide Staging and Local from the msc login region
menu and --region, but a persisted auth.json from a prerelease login keeps
pointing at that server — and a staging one still auto-bypasses TLS
verification. This is intentional (the login persists across upgrades), not an
oversight.
The whitelist is the only way the bypass turns on — no environment variable can extend it to another server, so production can never skip verification. To force verification back on while pointed at staging (once ops renews the certificate, or to debug its chain):
MEDIA_STUDIO_INSECURE_TLS=0 msc chat "hi" # also accepts "false"
Plain-http:// servers skip the bypass entirely (nothing to verify). The
bypass prints nothing: the staging choice is already stated in the msc login
menu and here, so no warning repeats on every command (--help and --version
included, whose output scripts and agents parse).
TLS-intercepting proxies
If browser SSO login works but every API call fails with fetch failed, a
TLS-intercepting proxy (Clash/FlClash TUN mode, corporate MITM) is breaking
certificate verification (unable to verify the first certificate).
Node/Bun do not use the OS keychain, so a proxy CA trusted by your browser
is not trusted by the CLI. Either trust the proxy CA explicitly:
export NODE_EXTRA_CA_CERTS=/path/to/proxy-ca.crt
or skip verification for the CLI only (no certificate checks). The staging host
already does this automatically, so this is only needed for other servers — and
it is now the only way to bypass a non-staging host, since no msc flag or env
var does so. Note it is process-global: every host the CLI talks to stops being
verified.
NODE_TLS_REJECT_UNAUTHORIZED=0 msc sessions
Proxies
msc follows the usual CLI convention (git, gh, curl): the environment
owns the proxy, the CLI only reads it. No OS-level "system proxy" detection, no
--proxy flag, and nothing printed.
export HTTPS_PROXY=http://127.0.0.1:7890
export NO_PROXY=localhost,127.0.0.1,::1 # appended automatically when absent
HTTP_PROXY / HTTPS_PROXY / NO_PROXY and their lowercase spellings are
accepted. ALL_PROXY is not, matching gh. Loopback is never proxied, so the
msc login callback and any future local helper keep working even when the
proxy list is empty.
Node reads proxy settings only while it builds the global dispatcher, before
any CLI code runs, so a set proxy variable makes msc re-run itself once with
--use-env-proxy (one extra process start; nothing at all when no proxy
variable is set). Setting NODE_USE_ENV_PROXY=1 in the environment skips that
restart. NO_PROXY entries are host names, .suffix or host:port; CIDR
ranges are not matched. The Bun dev run (bun run dev) and the standalone
binary ignore the proxy variables — Node's flag does not exist there.
A proxied browser is not evidence that the CLI can reach the host: if SSO login
works but every command fails with fetch failed, check what the hostname
resolves to before blaming certificates.
Development
bun run dev # run from TS source (bun), no build step
bun run build # bundled JS entry -> dist/ (the npm artifact)
bun run build:binary # standalone single-file binary -> bin/
Testing (before shipping any change)
Test layers from fast to faithful. Minimum bar: layer 2 passes; before an actual publish also run layer 4.
1. Dev iteration (fastest)
bun run dev -- chat "hi" # runs TS source directly, no packaging
2. Automated external-consumer check (required)
# from repo root
bun run package:cli:check
Builds, type-checks, packs the real tarball, installs it into a temp
directory outside the repo, and executes the installed binaries
(--version / --help assertions). Catches packaging-only breakage
(missing shebang, lost exec bit, stale bin target) that in-repo runs never see.
3. Manual user-install simulation
cd packages/media-studio-cli
bun pm pack --destination /tmp/cli-test
# option A: clean local consumer
mkdir /tmp/cli-consumer && cd /tmp/cli-consumer
bun add /tmp/cli-test/302ai-media-studio-cli-0.1.0.tgz
./node_modules/.bin/msc --version
# option B: global install (also verifies the short name doesn't collide)
bun add -g /tmp/cli-test/302ai-media-studio-cli-0.1.0.tgz
msc --version
# cleanup afterwards — one command removes BOTH bin names
bun remove -g @302ai/media-studio-cli
4. End-to-end against a real server
# terminal 1: local gateway (CLI defaults to localhost:3000)
bun run dev
# terminal 2:
msc login # browser sign-in
msc whoami # verify auth state
msc chat "画一只猫" -r # real streaming turn
msc chat "hi" --json
# Multi-turn continuity acceptance recipe:
msc chat "记住数字42"
msc chat "刚才的数字是几?" # answers 42
# Session management verification:
msc sessions # shows session list with (current) marker
msc chat "hi" --session <id> # switch to an existing session
msc chat "hi" --new # start a new session
msc sessions use <id> # switch default pointer to an existing session
msc sessions use --new # create a new session and switch to it
Without the local server, point at production with
msc login --region default.
Notes learned the hard way
bun pm packdoes NOT run publish lifecycle scripts; the smoke script builds explicitly first. Don't assume pack produces a fresh dist.- Two
binentries pointing at the SAME file makebun pm packemit duplicate tarball entries — that's whymsctargets the tinydist/msc.jsshim instead ofdist/index.js. Keep bin targets unique. bun publishrequires a modern bun (lifecycle scripts); the repo standard publish gate isbun run package:cli:checkfirst.
License
Apache-2.0