splicecraft --agent (alias --agent-api) exposes a localhost JSON
HTTP API with bearer-token auth, covering every GUI action external
AI agents need. The server binds 127.0.0.1 only and rejects any
request whose Host header isn't loopback (127.0.0.1 / localhost
/ ::1) — a DNS-rebinding defense, so a browser page can't reach the
API even to enumerate endpoints.
Local AI coding agents (Claude Code, Cursor, aider, hand-rolled scripts) work best when they can do things in the user's existing environment, not just generate text. SpliceCraft's agent API lets an agent drive the running GUI session through the side-door without leaving its terminal.
splicecraft --agent # default port 6701
splicecraft --agent --agent-port 6800 # alternative port
splicecraft --headless # agent API only, NO terminal UI (CI / no-pty)--headless (alias --agent-headless, or env SPLICECRAFT_HEADLESS=1)
runs the JSON API under Textual's headless driver — no pty required, so
it backgrounds cleanly in CI / agent contexts (no script -qfc wrapper).
It implies --agent, prints a SpliceCraft agent API ready on … line to
stdout once the socket is accepting connections, and serves the same
endpoints as the UI launch. For readiness, poll the unauthenticated
GET /healthz (returns {ok, status:"ready", version, headless})
instead of racing the token-file write + first real call.
A long-running daemon keeps serving the code it launched with, so after a
splicecraft update in another terminal it silently runs stale. status
surfaces this: it reports installed_version (what's on disk now) next to
the running version, plus a stale boolean (installed_version is null
when it can't be read, e.g. running from a source checkout). Every
authenticated response also carries a _stale warning when the running
version is behind, so you notice without polling status (the pre-auth
/healthz and /tools probes are served ahead of that stamp, so they stay
bare). A headless daemon can POST /restart
itself — it re-execs and the API returns on the same port (poll /healthz);
a GUI --agent session refuses (it would lose its live view — restart it
manually).
To stop a headless daemon, POST /shutdown rather than killing it. The app
quits through its normal teardown, so pending collection-sync and settings
writes are flushed, in-flight workers are drained, and the data-dir lock is
released; a SIGKILL skips all of that. The reply returns immediately and
the process exits about half a second later, so poll the returned pid (or
the port) to confirm — don't expect the socket to outlive the response. It
refuses with 409 when the canvas has unsaved edits (pass {"force": true}
to quit anyway), and, like restart, refuses on a GUI --agent session,
which has a keyboard and its own quit flow. Note that --agent already
auto-headlesses when it has no controlling TTY, so a daemon launched from
an agent context is eligible.
The server writes a token file at
<DATA_DIR>/agent_token containing the port + bearer token on the
first two lines. Hand the token to any client that needs to call the
API; the CLI sidecar reads this file automatically.
# manual cURL
TOKEN=$(tail -1 ~/.local/share/splicecraft/agent_token)
PORT=$(head -1 ~/.local/share/splicecraft/agent_token)
curl -s -H "Authorization: Bearer $TOKEN" \
-X POST -H "Content-Type: application/json" \
-d '{"accession":"L09137"}' \
http://127.0.0.1:$PORT/fetch~220 endpoints across:
-
Records —
new-plasmid(create from a raw sequence, the Ctrl+N flow), get / set sequence, add / update / delete features (with the full arrow type — forward ▶ / reverse ◀ / arrowless / double-stranded ◀▶ — plus colour and arbitrary GenBank qualifiers, matching the Insert/Edit Feature dialog;add-featuresinserts MANY at once under one lock + one dirty-check), list features (features, aliaslist-features), find ORFs (length cutoff in AMINO ACIDS —min_aa;min_length/min_bpare rejected so a bp-vs-aa mix-up can't silently return a default-length result. Each ORF carriesstart,end,strand,length_aa,aa_seq, plusnt_len— the exact coding length in bases INCLUDING the stop codon — andexceeds_one_lap. Take length fromnt_len/length_aa, never fromend - start: on a circular plasmid a frame can run a full lap or more without an in-frame stop, and no start/end pair on an n-bp circle can express that. Such an ORF setsexceeds_one_lap: trueand has its span pinned to the near-full circle),undo/redothe last edit,discard-changes(revert the canvas to its library-stored copy / clear a stuck-dirty flag so the next load / new-plasmid proceeds withoutforce), transfer annotations, apply GFF3 features to the loaded record (apply-gff3). -
Files — load (chromosome-scale safe via the path-based loader; supports
.gb/.gbk/.genbank/.dna/.embl/ FASTA /.ab1/ single-record.fastq/.gff3), export GenBank / GFF3 / FASTA / EMBL / CommercialSaaS.dna(symlink-guarded), bulk import a folder, bulk export a collection — to any of those formats or a circular-map image (png/svg) — viabulk-export-collection, or export the LOADED plasmid's circular map alone (png/svg) viaexport-map-image. -
Library + collections — list, search across collections, load an entry by name or id (
load-entryresolves cross-collection: active collection first, then the others; passcollectionto disambiguate), rename a plasmid's display name (rename-plasmid— fixes a name that got slugged to underscores), delete entries, copy-plasmid (copy an entry into another collection, name + features intact, in one call), move-plasmid (RELOCATE an entry between collections atomically — no copy-then-delete data-loss round-trip; handles the active collection on either side, re-staging the live library mirror), copy-plasmids / move-plasmids (the BULK form — a whole{names:[…]}list into one collection in a single locked save, the machine-friendly path that sidesteps the rate limiter; per-item results report copied/moved, not_found, ambiguous, and conflict, never silently renaming or dropping), create / rename / delete collections, get / set the active collection, list / set plasmid statuses. -
Parts — list-parts, get-part, create-part / update-part (full parity with the Part editor — name / grammar / type / level / position / overhangs / sequence plus
backbone, selectionmarker, andfwd_primer/rev_primerdomestication primers, Tms derived), delete-part, move-part (reassign a part to another bin in one atomic call), classify-part (overhang-pair lookup against every grammar), make-l0-part-from-fragment (turn a saved synthetic FRAG into a Level-0 part and the L0 plasmid it lives on, by SIMULATING the clone into the grammar's entry vector — body{fragment, part_type, [grammar], [name], [bin], [collection], [product_name], [save_plasmid]}; the part goes to the bin and the circular plasmid is saved to the library as<part> (L0)with its lineage, viadomesticate-partso there is one definition of an L0 clone (save_plasmid: falsefiles only the part); the part's overhangs come from that grammar's position table, so a two-tier nested fragment files its CATEGORY pair rather than the entry pair a digest would report; 400 names the cause for an unwrapped fragment, a part type the grammar doesn't define — the error lists the ones it does — a part type whose overhangs aren't on the fragment, or a grammar with no entry vector assigned; 409 on a duplicate(name, grammar). The filedsequenceis the body BETWEEN the overhangs, exactly ascreate-partexpects, so the part re-clones into the plasmid the call simulated; the reply carriespart_len,insert_len,plasmid_len,entry_vector,nested, andsaved_name/saved_id/collectionfor the plasmid — plusplasmid_errorin the rare case the part filed but the plasmid didn't, which is reported rather than swallowed).create-part/list-parts/delete-partaccept a{bin}to file into / read / prune just that named bin's own parts (a bin is a real partition, not only for writes). Parts-bin containers: list-parts-bins, create-parts-bin (the parts-side parallel to create-collection), set-active-parts-bin (switch the active named bin; mirrors the bin into the live parts file), rename-parts-bin, delete-parts-bin (the deleted bin's parts go with it; the last bin can't be removed; the active bin auto-promotes + re-mirrors). -
Design — gibson-assemble, simulate-gibson, traditional-clone / simulate-traditional-cloning (restriction digest + ligation: excise the insert, digest the vector, try every vector-fragment × insert-fragment × orientation, and save the product — refuses to guess when more than one ligation is possible, pick with
vector_frag_idx/insert_frag_idx. Passinsert_circular:trueto cut a cassette OUT of a plasmid insert — e.g. an Ω multigene into a binary vector — so its two digest fragments are both offered instead of treating the insert as a linear PCR product. Passcarry_annotations:trueto lift thevector_name/insert_nameentries' own features onto the ligated product — split at the cuts and shifted to product coordinates — so the clone isn't feature-bare; a side whose passed sequence doesn't exactly match its named entry is reported incarry_warningsrather than mis-placed), golden-gate-assemble / simulate-golden-gate (Type IIS — BsaI / BsmBI / BbsI / SapI / Esp3I — overhang-directed N-part assembly: parts in any order, chained by their 4-nt overhangs into a circle, with a unique-overhang + no-residual-site fidelity check), lint-synthesis (an "is it safe to order + assemble?" pre-flight over a baresequenceor a libraryid/name: internal Type IIS sites, extreme overall + windowed GC, long homopolymer runs, tandem repeats, degenerate bases, and — withexpect_cds— a full-length ORF check, rolled into a 0-100score+ line-itemwarnings; read-only), design-mutagenesis, scrub-plasmid (clone-free restriction-site removal: silent / synonymous cures inside CDSes + minimal swaps elsewhere; scrubs the loaded record or an explicitseq+features, optionalcodon_taxidbiases coding cures to a host's frequent codons, never mutates the canvas.methodpicks the route:"quikchange"(default — cured sequence + an improved-QuikChange primer pair per locus) or"golden_braid"(split into BsaI-tailed fragments that Golden-Gate back together; force-cures every BsaI site, returns per-fragment primers + native junction overhangs + a digest+ligateverifiedflag)), design-gb-part (Golden Braid / MoClo domestication primers; passcheck_entry_vector:true— plus optionalentry_vector_rolefor a Golden-Braid role — to validate at DESIGN time that the part's overhangs actually match the configured entry vector's acceptor, so a mismatch surfaces here instead of at the clone step; verdict rides underresult.entry_vector_check. Two-tier aware: if the configured L0 entry vector exposes EXTERNAL acceptor overhangs that differ from the position's category overhangs — a UPD/pUPD vector, e.g. externalCTCG/TGAGvs categoryAATG/GCTT— the category pair is auto-NESTED inside the external pair so the fragment enters on Esp3I (external) and later releases on BsaI (category); actual cut overhangs return asresult.entry_oh5/entry_oh3), design-synthesis-fragment (the direct-synthesis counterpart: wrap asequencein the SAME nested overhangs so the SYNTHESISED fragment — ordered as a gBlock, no PCR — clones into the UPD vector as an L0 part. Pass{sequence, part_type?, oh5?, oh3?, grammar?}— a grammar POSITION type OR a custom 4-nt overhang pair; auto-nests from the configured entry vector like design-gb-part. Returnsresult.fragment(+length, categoryoh5/oh3, externalentry_oh5/entry_oh3,nested,entry_vector, andinternal_sitesflagging any grammar-forbidden site in the insert that would self-cut)), domesticate-part (the real Synthesis-tab L0 clone: digest the grammar's CONFIGURED entry vector + the part's primed amplicon at the Type IIS enzyme, ligate the insert into the backbone, and save the circular L0 plasmid with its insert + entry-vector lineage — pass{sequence, oh5, oh3, name, grammar?, type?, fwd_primer?, rev_primer?, collection?}(saves to the active collection, or the non-activecollectionif given). Fails loud (422) when no compatible entry vector is configured rather than silently emitting a pUPD2-stub backbone, so an agent never files a wrong construct;domesticate-partsruns the same engine over a{parts:[…]}batch in ONE call — dirty-guard checked once, per-item results so one part that can't clone doesn't abort the rest), assemble-into-entry-vector (the multi-source level-up clone: chain L0 parts into an α/L1 TU, or TUs into an Ω/L2 module, by their fusion overhangs and ligate into the configuredroleacceptor at the level-up Type IIS enzyme — pass{sources, grammar?, source_level?, role, name, collection?}(active collection by default, or the named one). Also fails loud (422) on a missing acceptor or a chain that can't close; the L2→binary hop is a known engine gap), design-primers (Primer3 primer-pair design over a region:detectionpicks a diagnostic amplicon,cloningappends RE-site tails,genericreturns binding-only primers — no tails / overhangs), check-primer (a SINGLE oligo vs a template: Tm, GC%, and every 3'-anchored binding site — both strands, wrap-aware on a circular template, the tail scored as mismatches — to confirm a designed primer binds exactly once before saving), optimize-protein (codon-optimise an AA sequence to a chosen table; optionalstops0–3 appends that many stop codons, and a trailing*run in the protein is honored as-is and overrides it; optionaltransl_tableis an NCBI genetic-code id — default 1 = standard — so a host with a reassigned codon, e.g. 4 Mycoplasma or 6 ciliate, optimises and terminates against the code it actually reads;modeisfrequency(default, matches the host's codon distribution) ormax_cai(every position gets its best synonym); optional scrub passes —hazard_hosts(built-in plant/mammalian/bacterial expression hazards),forbidden_motifs(your own IUPAC patterns),min_gc/max_gcover agc_window, andavoid_repeats_with(break shared runs against constructs you've already built) — each opt-in, with the response reportingfixes,remaining_motifs,remaining_repeats, and the achievedgc_window_min/gc_window_maxso an unreachable target is visible, not implied met. The response also carriesgc3(third-position "wobble" GC) and awarningslist:max_caion an AT- or GC-biased host stacks that host's single top codon everywhere, collapsing GC3 — a silencing / mRNA-instability risk — while overallgcstill looks healthy, so it warns; on such a host preferfrequencymode (matches the native GC3) or setmin_gc). Only E. coli K12 ships as a table —add-codon-tablefetches your host from Kazusa by taxid, builds one from an NCBI genome, or imports a local CDS FASTA. -
Simulate — simulate-pcr (exact-match in-silico amplification, wrap-aware on circular templates) and simulate-gel (per-lane band positions + optional rendered ASCII gel image; ladder / plasmid / digest / PCR-amplicon sources).
-
Digest — digest (cut a RAW sequence with named enzymes and report the cuts + resulting fragments with their overhangs — overhang-aware QC for a Golden-Braid / restriction junction without loading the sequence onto the canvas;
circulardefaults true, a singularenzymeis accepted, and names the catalog doesn't know are reported underunknown_enzymesrather than silently dropped). -
Alignment — diff-plasmid (one target, circular rotation auto-detected), multi-align (batch: the loaded plasmid or a given sequence vs many targets at once — the Alt+A overlay; rotation-aware per target, circular auto-detected from each target's topology or forced with a
circularflag, withpicked_rotationreported per row), list-plasmidsaurus-members, align-plasmidsaurus-zip, verify-against-reads (the "I built X, I got reads back — do they match?" check: a bare or library-resolvedreferencevs a list of rawreads(Nanopore / Sanger / a consensus), each aligned rotation + RC-aware; returns per-read identity% + amatch/mismatchverdictagainstmin_identity). -
History — get-history returns the parsed
<HistoryTree>lineage as nested JSON. Agent assemblies (gibson-assemble,traditional-clone,golden-gate-assemble) attach real parent lineage — each input fragment / part / vector becomes a parent node, so the product reads as a genuineinsertFragmentassembly instead of a flatcreateDocumentleaf. Name your inputs (a{name}on a gibson fragment / golden-gate part, orvector_name/insert_nameon a traditional clone) and a parent that matches a saved library entry nests that entry's own sub-lineage. -
Codon tables — list, add (Kazusa fetch or raw dict), delete.
-
Search — blast (in-process BLASTN / BLASTP against your collections; pass
collectionsto scope the DB to a subset and stay under the build cap on a large library), hmmscan. Online (off by default): blast-online (remote NCBI BLAST vsnt/nr/…) and hmmer-web (remote hmmscan vs Pfam at EBI) SHIP the query sequence to an external server, so they are refused403unless the SpliceCraft user has armed Settings → "Allow agent online BLAST/HMMER". That setting is the HUMAN half of a two-key gate — it is deliberately NOT in the agent settings allowlist, so an autonomous agent can never flip it on itself; the in-process blast / hmmscan stay local and never consult it. Both block for minutes on the remote job (502 on a network / server error) and are concurrency-capped. -
Online reference-database lookups (off by default) — fpbase-search (FPbase fluorescent proteins: spectra, oligomerization, xrefs, sequence), uniprot-search (UniProtKB proteins: function, organism, keywords), literature-search (Europe PMC papers: title/authors/journal/DOI/abstract), genbank-search (NCBI
nucleotide/proteinterm search → accessions you canfetch; returnstotal_matches), wikipedia-search (lead-section summaries), web-search (general web), patent-search, and read-url (open ONE page by URL and return its readable text — HTML stripped to text, no JavaScript run; the companion to web-search, so Babs can open a result and actually read it, and it refuses binary / PDF pages with a502). All read-only. The*-searchendpoints send only a QUERY STRING to the public database — read-url sends only the URL you give it — never your sequence (distinct from the BLAST/HMMER egress above), but egress still requires the SpliceCraft user to arm Settings → "Allow Babs online database lookups" (403otherwise). That setting is the HUMAN half of the gate: deliberately NOT in the agent settings allowlist, so an agent can't self-arm. web-search uses the Brave Search API when abrave_search_api_keyis set (else keyless DuckDuckGo, best-effort); patent-search uses PatentsView when apatentsview_api_keyis set (else keyless Google Patents, best-effort). Both keys are secret — redacted in logs and unreadable via the agent settings surface. A keyless provider that is rate-limited returns502with a hint to add a key. Upstream/network failures →502. Every fetch routes through the SSRF-hardened opener with the fail-closed DEMO_MODE egress gate. -
Background Learning (
learn-start,learn-status,learn-results,learn-list) — grow Babs' corpus about a TOPIC with a focused, drift-resistant crawl of open databases + open-licensed web pages.learn-start {topic, max_docs?, max_minutes?}is the only write (it launches a crawl): refused403unless the user has armed the same "Allow Babs online database lookups" setting, and write-flagged so the in-app Babs prompts before an autonomous agent starts one. It returns immediately with aslug; polllearn-status {slug}for live counters (kept / fetched / dropped / frontier / status / drift), readlearn-results {slug, limit?}for the scored kept papers, andlearn-listfor all sessions on the box. Only topic query strings leave the machine — never your sequences. The crawl engine lives in the separate Babs repo (bb_learn.py); each topic is isolated in its own corpus + vector collection. -
Corpus, memory + reference (
recall-knowledge,index-library,ingest-url,remember-fact,memory-list,memory-forget,reference-lookup,learn-merge) — what Babs KNOWS, as opposed to what she can look up.recall-knowledge {query, max_passages?}retrieves scored passages (with sources) from the local corpus.reference-lookup {query, registry?, max_hits?}(or{list_registries: true}) reads Babs' CURATED registries — antibiotics/selection agents, promoters, terminators, UTRs, origins, Type IIS enzymes, assembly standards, markers, E. coli/Agrobacterium strains, binary and CRISPR vectors, base/prime editors, localization signals, chassis, media, growth regulators, bench recipes, troubleshooting — returning WHOLE structured records (working concentrations, cut notation, host ranges, genotypes) rather than retrieved prose. Prefer it over corpus recall for a named reagent, enzyme, strain or part: deterministic, instant, no embedding step. Both read-only and fully LOCAL — they send nothing anywhere.The writes all grow or prune what Babs believes, so each is write-flagged and the in-app Babs asks first.
index-library {}indexes the user's OWN work (plasmids, primers, parts, lab notebook) into a private pack so recall can answer "which of my plasmids carries a KanR and a T7 promoter?" — it READS the SpliceCraft data dir and writes only into the Babs repo, rebuilding the pack each run so a renamed or deleted plasmid can't linger as a ghost. That pack is private by construction: every record flagged private, a.no-egressmarker in its directory, excluded from the corpus-sync script, and never used to derive a web query.ingest-url {url, title?}is the only one with EGRESS — it persists ONE page into the corpus (SSRF-gated, redirects checked, open-licence pages only so the corpus stays redistributable) and is refused403unless the user armed Settings → "Allow Babs online database lookups".remember-fact {fact, title?}saves a durable fact;memory-listreturns[{slug, title}](check it before saving a duplicate, and to get slugs);memory-forget {slug}deletes one — forgetting changes what Babs believes in every future session, so it goes through the same approval as any other write.learn-merge {slug}folds a finished Background Learning session into the MAIN corpus. -
RNA structure + RBS — fold-rna (minimum-free-energy secondary structure: dot-bracket fold + ΔG in kcal/mol, pure-Python Turner-2004, no external dependency); cofold-rna (bound-state heterodimer ΔG of two strands, e.g. a 16S anti-SD : mRNA hybrid); rbs-strength (relative E. coli translation-initiation strength of a ribosome binding site — weighs SD:anti-SD match, 5'UTR occlusion, SD-to-start spacing, and the start codon; returns a RELATIVE ranking score, not an absolute rate); design-rbs (reverse-design a 5'UTR / Shine-Dalgarno + spacer to a target relative strength, with the achievable range + an on-target flag); assemble-operon (assemble a contiguous operon from a list of CDSs each with a target strength — context-aware RBS design + an element layout + per-gene achieved-vs-target report). DNA
Tread asU; ambiguous / over-length / bad input → 400. -
HMM databases — list / get / set-active / delete / add / download-hmm-database (the registry that backs hmmscan).
addregisters a custom.hmm.gzURL (mirrors the GUI "Add" form);downloadstreams → decompresses → hmmpresses a catalog entry (builtin likepfam-a/ncbifam, or a custom one) into<DATA_DIR>/hmm_databases/<id>/exactly as a GUI download would, soset-active+hmmscancan then use it.downloadruns synchronously, so a large builtin (Pfam-A ~300 MB, NCBIfam ~600 MB) blocks the request for minutes; a 409 means a download for that id is already in flight.deleteun-downloads the files but keeps the catalog entry sodownloadcan re-fetch it. -
Plasmidsaurus — list-plasmidsaurus-items (list your sequencing orders, most-recent first;
plasmidsaurus-itemsis a back-compat alias) / download-plasmidsaurus (fetch a run's results zip by its 6-character item code over Plasmidsaurus's official OAuth2 REST API and import the run's.gbkassemblies into the library as new entries, taggedsource: plasmidsaurus:<code>:<sample>, never overwriting existing entries). Credentials resolve env-first (PLASMIDSAURUS_CLIENT_ID/_SECRET) then the Settings values, and are deliberately NOT exposed through get-settings / set-setting.downloadruns synchronously and imports onlykind=results(the archive that carries assemblies); no credentials → 400, a download / parse failure → 502, an archive with no samples → 422, and a 429 means the account's 10-requests-per-minute budget is spent. Each listed item is{code, status, order_name, product_name, quantity, done_date, gross, has_results}. Choose a run byhas_results, not bystatus— roughly 40% of a real listing is shipping-label orders that are markedstatus: "complete"with a nulldone_dateand sort to the top, but have no results zip (feeding one todownload-plasmidsaurus404s);statusalso takes"canceled".downloadablecounts the items that do have results. The server truncates the listing at 1000 items and offers no pagination, so atruncated: trueresponse means older orders exist that the API cannot reach. -
Custom enzymes + enzyme collections — list / get / create / update / delete-custom-enzyme; list / get / create / update / delete-enzyme-collection; get / set-active-enzyme-collection.
-
Feature library — list / get / create / update / delete-feature-library (reusable annotation snippets).
-
Primer collections — list-primer-collections, create-primer-collection (the primer-side parallel to create-collection), rename-primer-collection, delete-primer-collection (the last collection can't be removed; the active one auto-promotes + re-mirrors), set-active-primer-collection, move-primer (reassign a primer to another collection in one atomic call), plus per-primer CRUD.
list-primers/get-primer/delete-primeraccept a{collection}to read or prune just that collection's own primers (a collection is a real partition, not only for writes).delete-primersprunes a whole{names:[…]}batch in ONE locked save (the machine-friendly path for a large cleanup that would otherwise trip the rate limiter), reportingremoved/not_found/ambiguous. -
Data safety — list-backups, restore-backup, list-pre-update-snapshots, restore-pre-update-snapshot.
-
Settings — get-settings, set-setting (allowlisted toggles only; a handful of settings — the Plasmidsaurus secret, the online-search egress gate
allow_online_search— are deliberately NOT in the allowlist, so an agent can neither read nor flip them; those are human-armed via the GUI Settings dialog); get-feature-colors / set-feature-color (the per-feature-type render colour overrides). -
Experiments lab notebook — list / get / create / update / delete experiment entries; move-experiment (relocate an entry to another project in one atomic call); attach-experiment-image (attach a server-side image file + embed it in the entry body); list / create / rename / delete projects; set active project (full notebook-layer CRUD).
create-experiment/list-experiments/delete-experimentaccept a{project}to file into / read / prune just that named project's own entries (a project is a real partition, not only for writes; deleting from the active project by name is refused — switch away first so the live mirror stays consistent). -
Gels — list / get / create / update / delete saved gel snapshots (in addition to simulate-gel for one-shot runs).
-
Protein motifs — list (built-ins + user overrides), set (copy-on-write override), delete user overrides.
-
Entry vectors — list (carries each vector's
role; a grammar can hold several — Alpha1 / Alpha2 / Omega1 / Omega2 + the L0 vector), get, set, plus auto-detect across the full library and clear-for-grammar. -
Active pointers — every
set-active-*(collection, codon-table, primer-collection, parts-bin, experiment-project, enzyme-collection, hmm-database) has a matchingget-active-*so a client can read the current selection before changing it. -
Utility — check-primer-duplicates, capture-snapshot.
-
OT-2 / Opentrons (liquid-handler control) —
ot2-compileturns a plate-transfer plan (a pipette, labware on deck slots, andfrom → towell transfers with µL volumes) into an Opentrons Protocol API v2.pytext, validating it against a built-in deck catalog first (friendly labware aliases liketiprack_300/eppi_24/plate_24/plate_96/reservoir_12expand to canonical load names; volumes are range-checked against the pipette). For a multi-step protocol designer, pass an orderedstepslist instead oftransfers(it takes precedence): each step is a typed operation —transfer(with optionalmix_before/mix_after/blow_out/touch_tip),distribute(one source → many wells),consolidate(many wells → one),mix,delay,pause,comment— compiled into one protocol; a control-only protocol (delay/pause/comment) needs no tips or labware.ot2-analyzeuploads a plan/protocol to the robot for its built-in simulate — server-side validation with NO motion, the pre-flight for a run.ot2-statusreturns a full state snapshot for crash monitoring: reachability + versions, pipette OK flags + volume specs, motor engagement, deck / instrument calibration health, module sensor readings, robot settings (variables), the status light, and — for the active run or a passedrun_id— live run / command state, with a detectedfaultslist + anokverdict.ot2-run(a write endpoint) is GATED physical actuation: it moves the gantry only when the robot's own analysis passes AND the body carries{"confirm": true}, refuses on an already-faulted robot (pre-flight check), and monitors state throughout — halting and reporting the instant a fault is detected. Pass{"wait": false}to start a run and pollot2-statuswith the returnedrun_idto watch it live. Host comes from the body'shostor the persistedot2_hostsetting. (Compiler + client live in the app-freesplicecraft_opentronssibling.) -
OT-2 run control —
ot2-run-control(a write endpoint) pauses, resumes, or stops a live run:{"host": ..., "action": "pause"|"resume"|"stop", "run_id"?: ...}. The active run is resolved automatically whenrun_idis omitted (so it also controls a run started from the Opentrons App). This is the manual counterpart toot2-run's automatic stop-on-fault. -
OT-2 concentration normalisation —
ot2-normalizecomputes per-sample volumes to equalise DNA mass or hit a target concentration, no robot needed:{"items": [{"name", "well", "concentration" (ng/µL)}, ...], "target_ng" | ("target_conc" + "final_volume"), "pipette"?}. It respects the pipette's volume floor/ceiling (frompipette, ormin_vol/max_vol), flags samples too dilute/concentrated to reach target (never dropping them), and — when asrc+dst(+dst_wellsordst_labware, optionaldiluent_ref) is given — also returns compile-readysteps. -
OT-2 plate map —
ot2-plate-mapmaps a plasmid collection onto a labware's wells row-major (the "a plate IS a collection" link):{"collection", "labware"}→{map: {well: {id, name}}, wells, n, overflow}. Feed the result intoot2-normalize/ot2-compileto cherry-pick or replate a collection by identity. -
OT-2 calibration + safe motion —
ot2-calibrationreports deck + per-pipette-offset + tip-length calibration with areadyverdict and aneeds_calibrationlist (calibrate those in the Opentrons App).ot2-home(a write endpoint) homes the gantry — safe motion, no plunger.ot2-position-check(a write endpoint) runs a MOTION-ONLY tour: the gantry moves to the top of each loaded labware's wells so you can verify alignment / labware offsets, emitting no aspirate/dispense/tip pick-up (the plunger never actuates, the pipette never descends into a well). It is gated like a run ({"confirm": true}+ a clean analysis) but does NOT require a calibrated pipette — that is what you're checking; deck calibration + reachability are still required. Body:{host, plan|<plan fields>, wells?, confirm?, wait?}. Labware offsets: add anoffset: {x, y, z}(mm) to any plan labware entry and it is applied to the run's motion (ot2-runandot2-position-checkboth honour it); a liquidot2-runalso now refuses to move an uncalibrated pipette, refuses when the attached pipette does not match what the protocol loads (reason: "pipette-mismatch"), and refuses when the robot's door-safety switch is enabled and the door is open (reason: "door-open"). During a monitored run the robot's rail lights come on as a "gantry moving" indicator and are restored afterwards, and a run that overruns the poll timeout is stopped on the robot rather than left moving. -
OT-2 lights + motor disengage (zero-/low-motion controls) —
ot2-lights(a write endpoint) toggles the robot's rail (status) lights:{host, "on": true|false}(defaults on) — handy to blink/identify the robot or turn the lights off for the night.ot2-disengage(a write endpoint) de-energises the gantry motors so the carriage can be moved by hand ({host, "axes"?: [...]}, all six axes by default); it homes nothing and is refused while a run is active.ot2-statusadditionally surfaces the door state ({"status", "required_closed"}) alongside lights, motors, and modules. -
OT-2 protocol + custom-labware libraries — manage saved designs and custom labware headlessly, mirroring the AUTOLAB Deck/Labware tabs. Protocols:
list-protocols,get-protocol(returns a compile-ableplan),save-protocol(write; validates the plan first),delete-protocol, and the collection setlist-protocol-collections/create-protocol-collection(write) /delete-protocol-collection(write). Custom labware: the identicallist/get/save/delete-custom-labware+list/create/delete-labware-collectionset, wheresave-custom-labwaretakes an Opentronsdefinition(awellsmap). Both stores are single-file collections that embed their items — backed up, restorable, and swept by Master Delete like every other user-data store.
Call /tools for the live discovery endpoint. Each entry is
{name, method, write, doc, doc_full} — doc_full is the endpoint's
COMPLETE docstring, which documents the request body (required / optional
keys, aliases, enums, size caps), so a client forms a correct call in one
round-trip instead of N trial-and-error 400s. /tools is authoritative:
it lists every registered endpoint, including the ~49 app-coupled
handlers that live in splicecraft.py rather than splicecraft_agent.py
— don't rely on grepping a single file for @_agent_endpoint.
Every authenticated success response also carries a uniform ok: true
flag and a predictable data field. ok: true is one consistent success
signal across every endpoint (an explicit ok a handler already sets is never
overwritten). data saves you from knowing each endpoint's ad-hoc key (seq /
library / sites / matches / …): it's the result with the envelope/metadata
stripped, unwrapped to the bare value when there's a single content key (a
scalar or list lands directly under data). The original keys stay too, so
it's a superset — read whichever you prefer.
Two unauthenticated endpoints are served ahead of the envelope wrapper
and stay bare — neither carries data (nor the _stale warning): the
/healthz readiness probe ({ok, status, version, headless}) and the
/tools self-describe ({endpoints: [...]}, read it by name). Both answer
before a client has a token, so they short-circuit the authenticated response
pipeline; for /tools, mirroring its doc_full corpus under data would
also needlessly double the API's largest response. Don't blindly read
resp["data"] on these two — check by endpoint.
- Bearer-token auth on every write endpoint; reads are unauthenticated to keep scripted introspection ergonomic.
- Localhost only (
127.0.0.1) — single-tenant by design. Do not expose on a LAN. - Inputs are length-, range-, and shape-validated at the boundary.
- Symlink refusal: write paths go through
_check_agent_write_pathwhich walks the full ancestor chain viaresolve()divergence + per-segmentis_symlink(). Pre-fix this only checked the immediate parent — see the symlink ancestor-chain regression indocs/invariants.md. - Read-dir traversal uses
lstat+S_ISDIRto refuse directory-symlink escapes. - Per-handler size caps —
_h_load_file50 MB (force=trueoverride), agent paths capped via_safe_file_size_check, manifest reads capped at_PRE_UPDATE_MANIFEST_MAX_BYTES. - Secrets stay out of the agent surface. The Plasmidsaurus Client
Secret is excluded from the settings allowlist (so get-settings can't
read it back and set-setting can't change it) and is redacted to
<redacted>in the change log + event stream. The Plasmidsaurus download endpoint streams over the same hardened, HTTPS-only, size-capped fetch path as the rest of the network layer and verifies a zip-archive magic header before writing to disk.
load-entry resolves across collections: it checks the active
collection first, then the others. A unique cross-collection hit loads
(and the response names its collection); an ambiguous name returns
409 listing the holders; pass collection to pin the search.
search-library is cross-collection too, and the id it returns is a
valid load-entry key.
The mutation endpoints rename-plasmid, set-plasmid-status,
delete-from-library, and the lookup in diff-plasmid operate on the
active collection only (via _load_library()). To target a plasmid
elsewhere, set-active-collection to its home first — search-library
shows which collection holds it.
delete-from-library is undoable by the human: every entry it removes
is pushed onto the running app's library-undo stack, so the user can press
u (or Ctrl+Z) in the library panel and take back what an agent deleted —
restored to its original slot, newest first, for the rest of that session.
It matches by NAME, so one call can remove several entries; they come back
in their original order. Nothing is recorded if the save fails. Managing
the library IS an agent's job; what stays unreachable over HTTP is the
whole-data master wipe, which has no endpoint by design (enforced by
tests/test_master_delete.py::test_no_agent_endpoint_exposes_wipe).
transfer-annotations resolves its source_id across collections
(like load-entry): the active collection first, then the others, by
display name or id. Pass source_collection to pin the search, and an
ambiguous key across collections returns 409. So a backbone in a
non-active collection transfers onto the loaded record without a temp copy.
To apply (not preview) pass apply: true — or the legacy dry_run: false;
the default is a dry run.
list-library lists the active-collection library by default; pass
{collection} to scope it to any one collection's plasmids without
switching the active collection (404 if there's no such collection).
load-entry stamps the entry's stored display name onto the loaded
record, so a later save / add-current-to-library round-trips the real
name (spaces preserved) instead of the underscored GenBank LOCUS. A
record pulled in with fetch (inspect-only, saved:false) is not
auto-filed on save — creating a new library entry requires an explicit
{create:true} so an inspection can't silently pollute the active
collection. Unknown body keys are echoed back under ignored rather than
silently dropped. A stricter guard applies to a closed set of routing /
selection params (collection, source_collection, bin, parts_bin,
enzyme, enzymes, orientation, rename): passing one to an endpoint that
doesn't accept it is a hard 400 (it would change where/how the op applies,
so a silent drop is the SC-D footgun) — while any other unknown key stays soft
for forward-compat. Currently enforced on the move-primer / move-part /
move-experiment routers.
load-file and add-current-to-library accept optional {id, name}
overrides to stamp the record's identity directly. This is the
deterministic fix for a batch build loop: a from-scratch .dna
carries no name packet, so without an override every such file loads as
id Cloned and a loop of add-current calls silently REPLACES each
prior entry (the library dedups by id) down to a single survivor. Pass a
unique id (scrubbed to [A-Za-z0-9_-], 32-char cap) per build — and a
spaced name for the display name — and all N persist. The override is
applied to a copy, so the live canvas record is untouched.
- Heavy ops (BLAST build, BLAST search, HMMscan, alignment) run in
@work(thread=True)workers; the API returns immediately with a status the client can poll, OR blocks the request until the worker completes — endpoint-specific. - The agent server uses
_agent_save_or_500(save_fn, label)for every_save_*call so an OSError / RuntimeError becomes a 500 + in-app notify, not a silent in-memory / disk desync.
splicecraft-cli tools # list every endpoint + one-line doc
splicecraft-cli status # current record snapshot
splicecraft-cli features # features on the loaded record
splicecraft-cli call <endpoint> --json '{...}' # call ANY endpointcall is a generic passthrough that reuses the same token / host / port
plumbing as the named subcommands (method defaults to POST when --json
is given, else GET), so a client never has to hand-roll HTTP against the
private _request. An HTTP error surfaces as structured JSON (with
http_code) plus a non-zero exit, instead of a hard exit that would kill
a batch mid-run.
See the CLI sidecar for the full convenience wrapper.