| name | lightning-artifacts |
| description | Publish a local file (HTML report, PDF, image, dataset sample, build output) to Lightning AI and get a durable, public lightning.ai/artifacts/<id> link that never expires and renders inline in the browser - plus list what's in the artifacts drive, unpublish (revoke) links, and delete the files behind them - entirely through the `lightning` CLI (uvx lightning-sdk) with regular auth (`lightning login` or an API key), no code. Use when the user wants to share a file, a generated one-pager, or an agent-made artifact as a permanent URL, hand a file to a teammate or CI job, see or revoke existing shared links, or asks to "get a public / shareable link for this file". |
Lightning AI Artifacts (durable shareable file links)
Publish any file to a teamspace and get back a durable public URL —
https://lightning.ai/artifacts/<id> — that anyone can open with no Lightning
login, renders inline in the browser (HTML/PDF/images), and never expires.
The control plane streams the bytes from storage on every request, so unlike a
presigned S3 URL there is no ~1h cap. Great for agent-generated one-pagers,
reports, dashboards, dataset samples, or build artifacts.
This whole flow runs through the lightning CLI (uvx lightning-sdk):
lightning cp / ls / rm handle the files, and lightning api — a
gh api-style raw HTTP client — makes the two publish calls around them. No
Python, no SDK code, not even a curl.
Setup & auth
uvx lightning-sdk --version
lightning login
Either credential works for every call in this skill — no token minting or
extra auth steps. Get a key from lightning.ai → user/org settings, or
lightning api-key create --org <org> --name artifacts. Never hardcode the
key — read it from the environment. To target a non-prod control plane, set
LIGHTNING_CLOUD_URL (default https://lightning.ai).
If lightning cp / ls / rm fails with "No such command", a cached older
CLI is running — refresh with uvx --refresh lightning-sdk (or
pip install -U lightning-sdk for a persistent install).
lightning api flags: -X method, -f key=val string field, -F key=val
typed field, -H header, --input <file> request body (--input /dev/stdin
to pipe one), -q jq filter (needs the jq binary for -q), -i include
response headers. Fields are JSON body for POST/PUT-with-body and query
params when the request also has --input or is a GET.
Resolve the teamspace (do this first)
Artifacts live in a teamspace (a "project" in the REST API). You need three
values: the owner name and teamspace name (for lit:// upload URLs)
and the project id (for the REST calls). Never guess. List memberships
and, if more than one fits and none is configured, ask the user which to
use:
lightning api /v1/memberships -q '.memberships[] | [.name, .projectId, .ownerType, .ownerId] | @tsv'
Capture the row's projectId and resolve the owner's name (teamspaces are
org-owned; the membership only carries the id):
PID=<projectId-from-above>
OWNER=$(lightning api "/v1/orgs/<ownerId-from-above>" -q .name | tr -d '"')
TSNAME=<name-from-above>
Teamspaces you can access through org-level permissions (rather than direct
membership) don't appear in /v1/memberships — if the user names one you
can't find, ask them for the <owner>/<teamspace> pair and get the project id
from lightning api "/v1/projects?name=..." or from them directly.
Publish a durable link (the CLI flow)
Three calls: lightning cp the file into the artifacts/ drive, read back
which storage cluster it landed on, then register the object as a shared
artifact. Copy-paste function:
share() {
local FILE="$1" NAME="${2:-$(basename "$1")}" CT="${3:-$(file -b --mime-type "$1")}"
local KEY="artifacts/${NAME#artifacts/}"
lightning cp "$FILE" "lit://$OWNER/$TSNAME/$KEY" >&2 || return 1
local CLUSTER; CLUSTER=$(lightning ls --json "lit://$OWNER/$TSNAME/$KEY" | jq -r '.[0].clusterId')
local LINK; LINK=$(lightning api "/v1/projects/$PID/shared-artifacts" -X POST \
-f clusterId="$CLUSTER" -f filename="" -f contentType= -F private= -q .url | -d )
>&2
>&2
}
share report.html
share dashboard.html reports/dash.html text/html
When you publish something for the user, always show them the full URL on its
own line (terminals make it clickable) — never just say "done".
The upload path and the publish filename must point at the same object — the
KEY variable keeps them identical. The server confines shares to the
artifacts/ folder; if you pass a bare filename (no artifacts/ prefix) to
publish it prepends one, but matching the two explicitly is clearest.
Set -F private=true to require an authorized project reader to open the link
(good for internal-only shares); the default false is genuinely public.
Content types that render inline
file -b --mime-type guesses most cases; pass an explicit type when the
extension is ambiguous. The publish call's contentType is what the browser
sees on every request (it overrides the stored object's type), so getting it
right there is what makes HTML/PDF render instead of download.
| File | Content-Type |
|---|
.html | text/html; charset=utf-8 |
.pdf | application/pdf |
.svg | image/svg+xml |
.png / .jpg | image/png / image/jpeg |
.json / .txt / .csv | application/json / text/plain / text/csv |
| anything to force-download | application/octet-stream |
List, see what's published, unpublish, delete
List what's in the drive with lightning ls — one level by default,
-r for every file underneath, --json for entries with size and the
clusterId the publish call needs. Listings follow the server's pages
automatically, so folders with many thousands of files come back complete:
lightning ls "lit://$OWNER/$TSNAME/artifacts/"
lightning ls -r "lit://$OWNER/$TSNAME/artifacts/reports"
lightning ls --json "lit://$OWNER/$TSNAME/artifacts/reports"
List every published link in the project (GET /v1/projects/{pid}/shared-artifacts,
newest first — id, filename, content type, public/private, download count, URL):
shares() {
lightning api "/v1/projects/$PID/shared-artifacts" | jq -r '.artifacts // [] | .[] |
"\(if .private then "🔒" else "🌐" end) \(.filename) · ⬇ \(.downloads // 0) · \(.createdAt // "")
id: \(.id)
🔗 \(.url)"'
}
Unpublish (revoke the link; the file itself stays in the drive):
unshare() {
lightning api "/v1/projects/$PID/shared-artifacts/$1" -X DELETE --silent \
&& echo "🗑️ Unpublished $1 — link is dead, file kept in the drive" >&2
}
unshare art_01kxgaep54zzs84arfns1j21wd
Delete the file itself (after unpublishing, or to clean up an abandoned
upload) — no cluster id needed; the server removes it wherever it is stored:
lightning rm "lit://$OWNER/$TSNAME/artifacts/report.html"
lightning rm -r "lit://$OWNER/$TSNAME/artifacts/reports"
rm fails on a path that doesn't exist (pass -f to ignore) and refuses a
folder without -r.
Re-publish the same object later (new id, new URL) by repeating the publish call
with the same filename. Update the contents behind an existing link by
re-running lightning cp to the same artifacts/<name> — published ids keep
serving the new bytes. To change the served Content-Type, publish again.
Example workflows
Prompts this skill handles: "share this HTML report as a permanent link",
"give me a public URL for output.pdf", "publish this one-pager so I can send
it", "drop this file somewhere CI can curl it".
Share an agent-generated HTML one-pager (renders in the browser, no login):
share summary.html
Publish a folder of reports, one link each (share prints each name + URL):
for f in out/*.html; do share "$f" "reports/$(basename "$f")" "text/html; charset=utf-8"; done
Hand a file to another service / CI job:
URL=$(share model-metrics.json)
Gotchas
- Publish wants the blob's storage cluster, not the cluster the upload
went through. A teamspace can be bound to compute clusters that store
their files under a parent cluster's bucket; publishing with such a compute
cluster's id fails with HTTP 500. The artifacts tree listing reports each
blob's real
clusterId — always read it from there (the share function
does). Deletes don't take a cluster at all.
lightning cp needs no cluster flag — it resolves the teamspace's
default cloud account and prints which it chose. Pass
--cloud-account <id> only to steer placement deliberately, and pick a
cluster whose status.phase is CLUSTER_STATE_RUNNING
(/v1/projects/$PID/clusters) — bound-but-unusable clusters make the
upload fail with a drive error that says nothing about cluster health.
- Unpublish reports success for links that don't exist.
DELETE /v1/projects/{pid}/shared-artifacts/{id} returns HTTP 200 with {} for an
id that was already revoked or simply mistyped — do not treat exit code 0 as
proof; verify by checking the public URL 404s. (lightning rm, by contrast,
fails on a missing path unless you pass -f.)
- The served HTML is not byte-identical to what you uploaded. Cloudflare injects a
Browser-Insights RUM beacon (
static.cloudflareinsights.com/beacon.min.js) into HTML
responses — a ~350-byte delta. Harmless for viewing, but don't promise a
bit-exact document, and don't checksum the response against the source file.
- Shares are confined to the
artifacts/ folder. Publish only finds objects
under projects/{pid}/artifacts/..., so the upload must target
lit://<owner>/<teamspace>/artifacts/.... Files under uploads/ or
lightning_storage/ are reachable with lightning cp too, but publish
won't find them there.
- Content-Type is set at publish time, not upload time — the serving handler
uses the shared-artifact record's
contentType, overriding the stored object.
Set it on the publish call so HTML/PDF render inline.
- The public link is genuinely open — anyone with it can fetch the file with no
auth. Use
-F private=true for anything you don't want world-readable.
-q (jq filtering) and the / helpers need the binary
installed; without it, parse the JSON yourself.