Skip to main content

Install

npm i -g @antasphere/slideless exposes the slideless binary (the package publishes under the Antasphere org’s npm scope; the binary name is unchanged). The published package is a single self-contained bundle (dist/bin.js, built with esbuild): the workspace-internal @slideless/contract, @slideless/sdk, @antasphere/cli-core, and commander are inlined at build time, so the published manifest carries zero runtime dependencies — nothing internal or git-pinned leaks into a public install. From a checkout:

Configuration: profiles, flags, environment

Config lives in the shared Antasphere CLI config home (provided by @antasphere/cli-core — one home for the whole tool family), under the slideless namespace: $XDG_CONFIG_HOME/antasphere/tools/slideless.json (default ~/.config/antasphere/tools/slideless.json), directories 0700, file 0600:
Migrating from older builds: the config used to live at ~/.config/slideless/config.json. On first run, if that file still exists, holds at least one profile, and the new home has no slideless profiles yet, it is imported automatically and non-destructively — the old file stays in place (older CLI builds keep working) but stops being read. Note the corollary: after slideless config clear, a still-present legacy file is imported again on the next run; delete ~/.config/slideless/config.json too if you want a truly clean slate. Every command accepts --api-url (alias --url), --api-key, --profile, and --json. Resolution order: Keeping secrets out of argv: a value passed as --api-key slk_… or --password … is visible to every process on the machine (ps) and lands in the shell history. Both have alternatives that do not touch the command line: --api-key-stdin reads the key from the first line of stdin (pass show slideless | slideless --api-key-stdin files list), SLIDELESS_API_KEY carries it in the environment, and share / share-email take --password-stdin or SLIDELESS_SHARE_PASSWORD. stdin can only be spent once per invocation — asking twice is a usage error rather than two commands silently sharing one secret. There is deliberately no default URL: a self-hosted CLI must name its instance explicitly (flag, env, or saved profile) rather than silently talking to the wrong host.

Cloud instances: connect through antasphere login

On an Antasphere-cloud instance you never run a Slideless-specific login. When no direct key resolves, the CLI asks discovery (GET /api/v1/instance) whether the instance signs in through the hub (auth.methods contains antasphere); if so, it exchanges the stored antasphere login credential for a user-scoped tool-local slk_ key (hub → tool; see Your Antasphere account) and caches it in the profile per hub profile (connectKeys — ONE key per hub account, valid for every org; the org is a per-request selection, never part of the credential):
  • The exchange names no organization (the hub credential identifies the USER, never one org); a single cached key serves whatever org context is active. Second and later runs make zero hub calls and mint nothing.
  • The hub key is sent to the hub only; the instance sees a short-lived user-scoped JWT (plus its own one-time offline grant, relayed once and never stored by the CLI) and answers with an ordinary local key.
  • A cached key is only ever replayed against the instance it was minted on (the profile’s baseUrl scopes the cache).
  • slideless logout on a hub-connected profile self-revokes the cached key(s) server-side (DELETE /cli/auth/key — the presenting key revokes exactly itself), then evicts them. An OLDER instance whose machine allowlist predates the self-revoke refuses (403) and keeps the key valid server-side — the CLI says so; revoke it from the dashboard. A classic single-key profile logs out exactly as before.
Self-hosted (oss) instances never take this branch: the flows above (auth login-request, login, SLIDELESS_API_KEY, --api-key) resolve exactly as documented, and the hub is never contacted.

Sign in

The OTP flow is the self-host entrance. On an Antasphere-cloud instance it refuses — the CLI detects cloud via discovery and steers you to antasphere login (see “Cloud instances” above); server-side the endpoints answer 403 cli_otp_disabled (cloud instances are hub-login-only). The flow needs the instance to have a delivering email driver (EMAIL_DRIVER=smtp|resend); it signs in existing accounts only — sign-up stays closed (accounts enter via setup, workspace invitations, or collaborator claims):
login-complete mints an slk_ API key server-side (scopes presentations:read + presentations:write, never data:export) and stores it as the active profile; --key-name <name> names the key as the dashboard lists it, and --expires-in-days <n> gives it a TTL (it never expires otherwise). Accounts with 2FA enabled are refused (two_factor_required), and an instance with no email driver has no OTP at all — mint a key in the dashboard instead (API keys, Create key; tick presentations:write, which the dialog leaves unchecked, for push and share) and paste it:
Profile management:

Author: push / pull / dev

push implements the content-addressed 3-step protocol: scan → hash every file (sha256) → precheck (the server answers which blobs it is missing) → upload exactly those → commit the manifest. Unchanged files are never re-uploaded.
  • New deck vs new version: the first push writes .slideless.json into the deck folder ({ presentationId, baseUrl }). Later pushes find it and commit a new version of that deck with optimistic concurrency (expectedBaseVersion; a concurrent push answers a clean retry error). --id <deckId> targets a deck explicitly; --new forces a fresh deck. A link pointing at a different instance errors loudly instead of silently targeting a foreign id.
  • Ignores: node_modules, Thumbs.db, .slideless.json, .slidelessignore, every dot-prefixed file or directory (.git, .DS_Store, .env, .github, …) and package.json / the common lockfiles are always skipped. That is not a taste call: a manifest path is what slideless pull writes onto someone’s disk, so those names are refused by the wire contract at commit and by the CLI again at pull — .git/hooks/pre-commit, .env and package.json are all “write this and something else runs it later” paths. Pushing a single dotfile (slideless push .env) errors instead of uploading it. A .slidelessignore in the deck root adds gitignore-style rules (a pragmatic subset: # comments, *, ?, **; trailing / = directories only; patterns with / anchor to the deck root, without / they match any path segment; no ! negation). Patterns are capped at 256 characters and 8 ** wildcards, and a longer or heavier one errors — an unbounded pattern used to hang the scan. Symlinks are never followed.
  • Entry detection: --entry wins, else index.html, else the only .html file, else an error listing candidates. A single-file push (slideless push deck.html) uses that file as the entry.
  • The deck’s page: every push answers with the deck’s own page on the instance, <instance>/decks/<id>/present (the master page: the owner’s full view, where share links are minted from — no link is created by a push; The deck is the artifact describes the page and its bar). The human summary prints it as url:; --json carries it as url next to presentation and version. The first push of a folder (the one that creates the deck) also opens that page in your default browser; later pushes only print it. --open opens it on any push, --no-open never does. A run whose stdout is not a terminal, or any --json run, never opens a browser — whatever the flags say — so a push in CI or under an agent stays silent. The opener is the platform’s own (open, xdg-open, the Windows URL handler) with the URL as an argument, never a shell string.
  • Attachments: a downloads/ folder at the root of the deck is the version’s attachment set (Attachments, and the Downloads page for the URLs and the API): files handed to a link’s recipient as downloads, never rendered. The bar over the share link offers them (a Download button listing each file, and Download all for the zip); on a link made with --no-bar, or inside an embed, the bar is absent and your deck must link its own files for a recipient to find them: <a href="downloads/figures.csv"> for one, <a href="downloads.zip"> for the set, relative to the deck’s own URL. The push classifies them by the same rule the server uses and prints an Attachments: N files, X MB (downloads/) line under the summary when the folder carries some; --json carries attachments: { count, sizeBytes }. They ride the same content-addressed protocol as every file: an unchanged attachment is never re-uploaded when you iterate on the HTML (the precheck reports it present), and only what changed travels.
  • The per-file cap, refused before anything is uploaded: every file of the deck is checked against the instance’s per-file cap before the upload session, the precheck or any upload. A file over it names itself and the cap, and no byte has left the machine:
    The cap is limits.maxFileSizeMb from GET /api/v1/instance when the instance exposes it, else the documented default of 100 MB. Without this check the instance answered 413 to the oversized blob only after its upload, with the smaller files already stored.
  • Flags: --title, --entry, --kind presentation|app|plan, --interactive, --id, --new, --open / --no-open.
open opens the page of the deck a folder is linked to: it reads .slideless.json (deck id + instance) and composes the same URL a push prints, with no key and no network call. --json prints { presentationId, baseUrl, url } instead of opening. An unlinked folder is an error pointing at push, and a link file whose instance is not an http(s) URL is refused before anything reaches the opener (the file can arrive with a cloned folder; the opener would dispatch any scheme). pull downloads a version’s manifest and streams every blob to disk — byte-identical to what was pushed, attachments under downloads/ included (the summary counts them) — then writes/refreshes .slideless.json so a later push in that folder targets the same deck. It treats the instance’s answer as untrusted input: every manifest path is re-validated locally, each blob is capped at the size the manifest declared and must hash to the sha256 the manifest claims before anything is written, writes refuse to follow a symlink (file or directory) and never leave a file executable, and a destination whose .slideless.json names a different instance errors loudly — the same refusal push has always had. dev serves the folder locally with the exact public-viewer posture — Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-modals allow-downloads, nosniff, no-referrer, no-store — so what you preview is exactly what share-link recipients get (same isolation, same relative paths). Live reload is injected into HTML responses; any file change reloads the browser. No backend, no credentials. It serves the deck folder and nothing else: a path is resolved with realpath and re-checked against the root, so a symlink inside the folder cannot serve a file from elsewhere on the disk; dot-prefixed paths 404 outright; and requests whose Host header is not the address the server bound answer 403, which is what stops a DNS-rebinding page from reading your deck (and anything else) through the browser.

Share

A share link is a secret URL made on top of the deck, public to whoever holds it, following the latest version or pinned to one, with its own switches, expiry, password and counts: Share links is the model, Versions the latest-or-pinned story.
Secrets are stored hash-only server-side: the URL printed at creation is never retrievable again (share-email mints and mails a fresh secret per send). Embedding from the CLI: share --json always carries an embed object ({ script, iframe, embedJsUrl }) alongside the token and URL, so an agent that both manages decks and builds websites mints the link and pastes the snippet in one step — no second command, nothing to assemble by hand. In human output, --embed prints the same two snippets. Both come from the same builder as the dashboard’s copy dialog (identical sandbox attributes); --placement <label> bakes a per-spot analytics label into them (it shows up per view in slideless views). Details: the Embedding page under Sharing & review. tokens lists every link of the deck, the dashboard’s own preview links included: rows named Dashboard preview that expire within the hour and count nothing; the dashboard hides them, the CLI and the API do not. The human table has no column for the bar: --json carries showBar on every token, which is how a bare link is told apart from the others. Access stats count entry loads only, de-duplicated per browser within a short window (VIEW_DEDUPE_WINDOW_MINUTES, default 10 min) — so one human open is one count, while cookie-less fetches (CLI, curl) count each time. “Last opened” is the last counted open. The downloads column is the link’s downloadCount (one per attachment taken through the link, one per zip, never a view) when the link allows downloads, and no downloads when it was minted with --no-download (canDownload: false; the deck still opens, the file URLs answer 404). --json carries both fields on every token. Collecting form responses: slideless responses with no filters prints the summary first (responses per form × link × source × placement, with the latest activity), then the most recent rows. Filters narrow the row listing: --form to one named form, --link to one share link (token id or link name, never a share URL: secrets are hash-only server-side and cannot be resolved back to a token), --source link|embed and --placement <label> to one distribution spot, --since <ISO datetime> to a time window (a response created OR edited at or after it — an edit is activity, so an edited answer resurfaces). --all follows pagination, --json prints the wire shape for agents, and --csv writes a spreadsheet-safe CSV (cells are guarded against formula injection) built client-side from the same rows. Links minted with --no-forms refuse submissions (403 forms_disabled); everything else about them works unchanged. Every edit is kept as a revision: slideless response <id> <responseId> prints the current answer and every kept revision (at most 100 per response; the first and the latest 99 always survive), each with the link and the moment it was written through. slideless notify <id> --off silences the owner mails (one on a new response, a different one on an edit, never carrying the answers, at most one per deck per ten minutes) without touching forms. Details: the Forms page under Sharing & review.

Collaborators

Decks, files, export

Paging

Every listing command (list, versions, tokens, views, responses, files list) answers one page at a time, newest first: --limit <n> sets the page size (1 to 100), --cursor <cursor> resumes from the nextCursor a previous page printed, and --all follows the cursors until every page is fetched.

Shell completion

Machine use

Add --json to any command for the wire shape; every error prints to stderr and exits non-zero. Typical agent loop:
A --json push never opens a browser; hand .url (the deck’s page on the instance, behind the owner’s session) to the person, and mint a share link only when a recipient needs one.

Server endpoints behind auth login-*

POST /api/v1/cli/auth/request and POST /api/v1/cli/auth/complete are public pre-auth endpoints (like /setup), riding the better-auth email-OTP plugin with disableSignUp — an unknown email gets a generic success and no mail (no account enumeration, no account creation), codes are attempt-limited (3) and both endpoints sit behind the instance’s OTP/login rate walls. The key is returned exactly once; the flow’s throwaway session is deleted server-side. Without an email driver both answer 400 otp_unavailable; on the cloud edition both answer 403 cli_otp_disabled (hub-only login — mint through antasphere login instead). DELETE /cli/auth/key is the logout counterpart on both editions: an authenticated route where the presenting API key revokes exactly ITSELF (machine-allowed under presentations:write in the fail-closed scope allowlist; sessions are refused — the dashboard is their key surface).