deckflow-core
One CLI over the Deckflow tools core actually brokers, with the provider acquired on demand.
deckflow env check / setup / clean the environment ✅
deckflow auth the cloud credential, via deckflow-extract ✅
deckflow parse -> deckflow-extract (PyPI, ~4MB) ✅
deckflow update install a newer core beside the running one ✅
Core is a capability broker: it owns the CLI contract, the result envelope, the version pin and the safety policy. The provider owns the actual work, stays independently versioned, and is only fetched when a command needs it.
What core deliberately does not broker
@deckflow/deckhtml (PPTX export) and @deckflow/html-editor (visual editing)
are called directly by the Skill, not through core. Wrapping them here meant
restating two contracts core does not own — slide ordering, stage geometry,
element identity, session lifecycle — and then keeping that restatement correct
across three release cadences. The tools already publish those contracts; a
second copy in core could only drift from them.
deckflow env check does report whether Node and npx exist, because the Skill
needs the fact and would otherwise write a second probe. That is the line:
facts, never verdicts. There is no pptx_available field, because that also
depends on registry reachability and the deck's stage size, and core knows
neither.
Install
Pure standard library, zero third-party dependencies. That is a hard constraint rather than a preference: it is what lets core be installed without a virtualenv, or vendored into a Skill with no install step at all.
If you are integrating a Skill, do not write an install step at all. Copy
launcher/deckflow into the Skill's scripts/ and make
the prerequisite one line:
python3 scripts/deckflow env check
The launcher finds a suitable interpreter, locates core (vendored → managed →
importable), installs it into ~/.deckflow/core/<version>/ if it is missing,
and declares the Skill's root. It exists because the obvious alternative does
not work:
| What breaks | Where |
|---|---|
pip install deckflow-core → error: externally-managed-environment |
any PEP 668 interpreter: Homebrew macOS, Debian 12+ |
Requires-Python >=3.10 unsatisfied |
macOS /usr/bin/python3 is 3.9 |
deckflow: command not found right after a successful install |
--user installs land outside PATH |
An agent that hits any of those improvises, and the improvisation is usually
--break-system-packages on someone's system Python.
For a human managing their own environment:
pipx install deckflow-core # or: uv tool install deckflow-core
deckflow env check
# Or a managed install by hand — no venv, works on a PEP 668 interpreter
python3 -m pip install --target ~/.deckflow/core/0.3.0 deckflow-core==0.3.0
PYTHONPATH=~/.deckflow/core/0.3.0 python3 -m deckflow_core env check
Requires Python 3.10+. Node.js is not involved anywhere in core.
deckflow env
deckflow env check # report; no side effects, no downloads, always exit 0
deckflow env setup # acquire the pinned provider (~4MB) — the only one that downloads
deckflow env clean # remove the managed provider install
env check is designed to be the first line of a Skill's prerequisites, which
fixes three of its properties: it never writes anything, it exits 0 whenever
the check itself ran, and it reports facts rather than verdicts. A non-zero
exit there would tell an agent the Skill is broken and send it off to repair a
machine that is fine.
JSON is the default output. --human is the opt-in, for people.
{
"schema_version": 2, "command": "env check", "core_version": "0.3.0",
"status": "succeeded",
"extract": { "status": "not-acquired", "pinned_version": "0.3.0",
"resolution": "missing", "acquired": false, "download_mb": 4 },
"env": {
"skill": { "name": "gezhe-ppt", "version": "0.4.0-beta.2",
"root": "/…/gezhe-ppt", "version_source": "frontmatter" },
"runtime": { "version": "0.3.0", "installation": "managed", "location": "…" },
"python": { "version": "3.14.5", "executable": "/opt/homebrew/bin/python3",
"satisfies_requires_python": true, "externally_managed": true },
"cloud": { "available": false, "reason": "extract-not-acquired",
"configured": null, "shared_with": "deckhtml" },
"host": { "node": { "present": true, "version": "22.3.0" },
"npx": { "present": true } },
"home": "/Users/you/.deckflow"
}
}
python.executable is there because a bare version is not actionable: an agent
host commonly has three python3 binaries on PATH, and two machines both
reporting 3.12 differ in whether an install will be refused.
Core never goes looking for a Skill. Scanning upward for a SKILL.md would
find the user's project, not the Skill. The caller declares one with
--skill-root, or DECKFLOW_SKILL_ROOT, or by invoking the launcher, which
knows its own location. No declaration reports "skill": null and is not an
error. The version is read from deckflow-skill.json if present, else from
SKILL.md frontmatter metadata.version.
deckflow auth
deckflow auth status # read-only; never downloads to answer
deckflow auth login # browser login; refused without a TTY
deckflow auth set-key --stdin # a space worker secret; no browser needed
The credential lives in ~/.deckflow/credentials and is shared with
DeckHTML. Core owns none of it: every action here forwards to
deckflow-extract, which owns that file's merge rules. This is core's one
exception to "core writes only --out and --report", and it is delegated
rather than reimplemented.
Two consequences worth knowing:
- branch on
configured, not onDECKFLOW_API_KEY. A user who logged into DeckHTML for a PPTX export has also configured cloud parsing, with no variable visible anywhere in the environment. configured: nullmeans "not asked", not "no".auth statusrefuses to trigger a 4MB download to answer a question, so an unacquired provider reportsavailable: falseand leavesconfigurednull. A confident "no" for a question never asked is how a logged-in user's material gets uploaded.
There is no logout. Clear a stored credential with
deckflow-extract auth logout.
deckflow parse
deckflow parse <file> --out <dir> [--report r.json] [--overwrite]
Extracts one local file into a Parse Bundle (parse-manifest.json +
document.md + assets/) through the deckflow-extract provider.
Deliberately thin — the provider already has a well-shaped contract, so core adds boundaries and gets out of the way:
- the bundle passes through untouched. Core does not rewrite
document.md, recompute fidelity, or invent a second artifact vocabulary alongside it. recommendations[]reaches the caller verbatim. When the provider says a heavier engine would extract 65 images instead of 5, that surfaces as aninfodiagnostic — choosing is the caller's job, never core's.- engine upgrades default to
never. Provider acquisition happens automatically, but the provider's own optional engines (56MB PDF, 107MB OCR) change what the extraction produces, so they are opt-in via--upgrade. --mode localis forced and cloud credentials are withheld, and--fetch-remote-images offis passed explicitly so a change in the provider's defaults cannot put the content plane on the network. Withholding means both halves: the credential variables are removed from the child environment andDECKFLOW_NO_STORED_CREDENTIALS=1is set, because the provider also reads~/.deckflow/credentials— the file it shares with DeckHTML — where a logged-in machine would otherwise hand back exactly what was just removed.- URLs are refused. The provider can fetch them; core does not, because "the content plane never reaches the network" is not worth stating with an exception in it. The refusal names the direct provider command.
How the provider is resolved
| # | Source | Notes |
|---|---|---|
| 1 | --extract-bin <path>, or DECKFLOW_EXTRACT_BIN |
wins over everything; for developing core and extract together |
| 2 | already in the environment | used only if its version satisfies the pinned range |
| 3 | core's managed home | $DECKFLOW_HOME/extract/<version>/ |
| 4 | on-demand acquisition | unless --offline |
| 5 | structured failure | EXTRACT_MISSING with a runnable recovery command |
An ambient install outside the pinned range is not an error: core records an
EXTRACT_VERSION_MISMATCH warning and uses its own copy, so a global install
can never quietly change what a pinned run executes.
Acquisition is narrowly bounded. It writes only into
$DECKFLOW_HOME/extract/<version>/ — never a global install, never your Python
environment; installs only the exact pinned version; passes the index
explicitly, so local pip configuration cannot redirect the pin; verifies
before the install counts, and removes the directory if it cannot; and reports
itself in the envelope as acquired: true.
--offline (env DECKFLOW_OFFLINE) is the CI setting: a missing provider
becomes an error instead of a download.
deckflow update
Installs a newer core into ~/.deckflow/core/<version>/ and takes effect on the
next run — never in place. Upgrading the package that is currently executing
is the kind of operation that half-works; installing beside it means an
interrupted update leaves the working copy untouched and rollback is removing
one directory. Older managed versions are pruned after a successful install.
There is deliberately no deckflow update extract: the provider pin moves with
core, and an independently upgradable provider is not a pinned one.
deckflow update skill reports and never writes. Core does not update a
Skill directory: the distribution channel is not declared anywhere, ownership
would become a cycle (the Skill installs core, core rewrites the Skill), a
rewrite would clobber files the user edited, and a self-updating Skill is remote
code execution on the next agent run. A Skill that wants this machine-readable
declares update.command in deckflow-skill.json, and core hands that command
back.
The managed home
~/.deckflow/
├── core/<version>/ core itself; the launcher runs the newest
├── extract/<version>/ core's managed copy of deckflow-extract
├── parse/ deckflow-extract's OWN engine sidecars — not ours
└── credentials shared with DeckHTML; only extract may write it
Network and content
Two separate planes:
| Plane | Policy |
|---|---|
| Providers (fetching code) | network allowed, for the pinned package from declared indexes only, written only to the managed home, always reported |
| Content (sources, extracted text, assets) | never uploaded. The provider's cloud mode is used only when you explicitly ask for it; the presence of an API key is not authorization |
Output contract
stdout and --report carry the same envelope. Diagnostics are sorted
deterministically so two isolated runs over the same inputs produce the same
report bytes.
{"schema_version": 2, "command": "env check", "core_version": "0.3.0",
"status": "succeeded", "started_at": "...", "finished_at": "...",
"extract": null, "inputs": [], "outputs": [], "diagnostics": []}
extract is a single object — schema 1 had a providers[] array, which made
every caller index into a list to find the only element it could contain. It is
always present, and null when the command never resolved the provider.
status is one of succeeded / partial / failed. Read it — the exit code
only classifies why a run ended:
| Code | Meaning |
|---|---|
| 0 | succeeded or partial |
| 2 | usage |
| 3 | input missing/invalid, or a precondition not met |
| 5 | a provider failed to run, is missing, or is incompatible |
| 6 | output conflict, permission, or atomic write failure |
| 130 | interrupted |
A failure still prints a parseable envelope on stdout; prose goes to stderr. That holds for the launcher too: a bootstrap that never reached core emits the same shape rather than a traceback.
Scope of this release
v0.3.0 registers env, auth, parse and update, and that is the whole
surface. editor, export, validate and providers are not registered at
all: an unregistered name is an argparse invalid choice and exit 2, never a stub
or a "not implemented" response, because either would put the name in --help
and let a caller believe core owns the capability.
providers is on that list because it was core's own word for one package. The
resolution ladder, the pin and the managed install all survive — the noun does
not. See deckflow-core-refactor.md.
Tests
PYTHONPATH=src:tests python3 -m unittest discover -s tests
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deckflow_core-0.3.0.tar.gz.
File metadata
- Download URL: deckflow_core-0.3.0.tar.gz
- Upload date:
- Size: 59.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57c1adfea7260bd553d9170a63e80ec921361fd326a9c58625575dbaae3849c1
|
|
| MD5 |
c90e62ec9f1de87531052570f419761f
|
|
| BLAKE2b-256 |
92e8694451747aeacb2721a4afb1b943f62de09ac152f5a65cecc614da08d146
|
Provenance
The following attestation bundles were made for deckflow_core-0.3.0.tar.gz:
Publisher:
release.yml on deckflow/deckflow-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deckflow_core-0.3.0.tar.gz -
Subject digest:
57c1adfea7260bd553d9170a63e80ec921361fd326a9c58625575dbaae3849c1 - Sigstore transparency entry: 2281464940
- Sigstore integration time:
-
Permalink:
deckflow/deckflow-core@a48eab477a6f6a9c322d31f16cb5a48d86b092d1 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/deckflow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a48eab477a6f6a9c322d31f16cb5a48d86b092d1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file deckflow_core-0.3.0-py3-none-any.whl.
File metadata
- Download URL: deckflow_core-0.3.0-py3-none-any.whl
- Upload date:
- Size: 55.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66530aeda17e538f016aeb310468df8590efde732b4aff54b17627db49860dd3
|
|
| MD5 |
3ae86c614fbd6491aa344bea0050ff82
|
|
| BLAKE2b-256 |
c816396e534322375d78477583ea47a163bbab0f3a008081901a2ca051998e47
|
Provenance
The following attestation bundles were made for deckflow_core-0.3.0-py3-none-any.whl:
Publisher:
release.yml on deckflow/deckflow-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deckflow_core-0.3.0-py3-none-any.whl -
Subject digest:
66530aeda17e538f016aeb310468df8590efde732b4aff54b17627db49860dd3 - Sigstore transparency entry: 2281465160
- Sigstore integration time:
-
Permalink:
deckflow/deckflow-core@a48eab477a6f6a9c322d31f16cb5a48d86b092d1 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/deckflow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a48eab477a6f6a9c322d31f16cb5a48d86b092d1 -
Trigger Event:
push
-
Statement type: