Packages
Packages are ZIP archives or directories containing Scriptling libraries or applications. They enable easy distribution and reuse of code.
Overview
Every package requires manifest.toml. The default import directory is lib/, but that directory is not required for a main-only application. Directories named explicitly in the manifest’s libs field are required.
mylib.zip
├── manifest.toml # Required - package metadata
├── lib/ # Conventional/default location for importable modules
│ ├── __init__.py
│ └── utils.py
└── docs/ # Optional - documentation
└── guide.mdPackage Manifest
The manifest.toml file describes the package:
name = "mylib"
version = "1.0.0"
description = "A useful library"
main = "app.main" # Optional: entry point for runningFields:
| Field | Required | Description |
|---|---|---|
name |
Yes | Package name |
version |
Yes | Version string |
description |
No | Brief description |
main |
No | Entry point as module.function for running |
Loading Packages
Use the --package (or -p) flag to load packages:
# Load from local file
scriptling --package ./libs/mylib.zip script.py
# Load from URL
scriptling --package https://example.com/libs/mylib.zip script.py
# Load multiple packages
scriptling --package core.zip --package utils.zip script.pyPackage Priority
When loading multiple packages, the last one has highest priority. Later packages can override modules from earlier ones:
# override.zip can shadow modules from core.zip
scriptling --package core.zip --package override.zip script.pySelf-Signed Certificates
Use --insecure (or -k) to allow self-signed HTTPS certificates:
scriptling --insecure --package https://self-signed.local/lib.zip script.pyHash Verification
Verify package integrity by specifying an expected SHA256 hash:
# Verify package hash before loading
scriptling --package mylib.zip#sha256=abc123... script.py
# With URL (download and verify)
scriptling --package https://example.com/lib.zip#sha256=abc123... script.pyPlugin Fetcher Schemes
Loading a fetcher plugin automatically attaches its declared library, so you normally should not add its scheme://libs source with --package. Explicit plugin-scheme package sources are supported after the serving plugin is loaded, and a scheme source can also be the positional script:
scriptling --plugin /usr/local/bin/knot --plugin-arg scriptling-server \
knot://scripts/helloIf no loaded plugin serves the scheme, the error names the scheme and points at
--plugin, rather than reporting a missing file. See
Plugin Fetchers for details.
How it works:
- Append
#sha256=<hash>to the package path or URL - Scriptling computes the SHA256 hash after fetching
- If the hash doesn’t match, loading fails with an error
- For local files, the hash is optional (no hash = no verification)
- For remote URLs, this ensures the package hasn’t been tampered with
Getting the hash:
When you create a package, the hash is printed:
scriptling pack ./mylib -o mylib.zip
# Output includes: sha256=abc123def456...Or use the manifest command to print a package’s metadata:
scriptling pack manifest mylib.zip
# Shows: Name, Version, Description, and Main fields from the manifestCustom Cache Directory
Remote packages are cached locally. Override the cache location with --cache-dir:
scriptling --cache-dir ./cache --package https://example.com/lib.zip script.pyOr set the SCRIPTLING_CACHE_DIR environment variable.
Running Packages
If a package defines a main entry point in its manifest, you can run it directly:
# Run the package's main function
scriptling --package mylib.zip
# With arguments
scriptling --package mylib.zip -- arg1 arg2Execution order:
- If
-cgiven → execute inline code - Else if
--interactive→ start REPL - Else if script file or stdin → execute script
- Else if packages with
main→ run entry point from last package - Else → error
Inline Code
Use -c to execute inline code with packages loaded:
scriptling --package mylib.zip -c "import utils; print(utils.hello('World'))"Creating Packages
Package Structure
Create a directory with your code:
mylib/
├── manifest.toml
├── lib/
│ ├── __init__.py
│ ├── utils.py
│ └── submodule/
│ ├── __init__.py
│ └── helpers.py
└── docs/
└── guide.mdPack Command
Create a package from a directory:
# Create package
scriptling pack ./mylib -o mylib.zip
# Overwrite existing
scriptling pack ./mylib -o mylib.zip -fThe SHA256 hash is printed on success: use it with #sha256=... to verify integrity on load.
Unpack Command
Extract a package for development:
# Unpack to current directory
scriptling unpack mylib.zip
# Unpack to specific directory
scriptling unpack mylib.zip -d ./mylib-dev
# List contents without extracting
scriptling unpack mylib.zip --list
# From URL
scriptling unpack https://example.com/lib.zip -d ./libViewing Package Information
Manifest Command
View package metadata:
# From local package
scriptling pack manifest mylib.zip
# From URL
scriptling pack manifest https://example.com/lib.zip
# From source directory
scriptling pack manifest ./mylib
# JSON output
scriptling pack manifest mylib.zip --jsonDocs Command
Browse package documentation interactively:
# Launch TUI browser
scriptling pack docs mylib.zip
# From URL
scriptling pack docs https://example.com/lib.zip
# From unpacked directory
scriptling pack docs ./mylib-dev
# List docs without TUI
scriptling pack docs mylib.zip --listCache Management
Remote packages (http:// and https://) are cached locally to avoid redundant downloads. Scriptling uses HTTP conditional requests to check for updates efficiently.
How Caching Works
When loading a remote package:
- First download - Package is cached to disk along with its
ETagandLast-Modifiedheaders - Subsequent loads - Scriptling sends a conditional
GETrequest with:If-None-Match: <etag>- if the server provided an ETagIf-Modified-Since: <last-modified>- if the server provided Last-Modified
- If server responds
304 Not Modified- Uses cached copy (no body transferred) - If server responds
200 OK- Downloads and caches the updated package
This means:
- Single request - One GET request, whether cached or not
- Automatic updates - New versions are fetched immediately when available
- Bandwidth efficient - No body transferred when unchanged
Cache Location
Default cache directory:
| Platform | Location |
|---|---|
| macOS | ~/Library/Caches/scriptling/packages/ |
| Linux | ~/.cache/scriptling/packages/ |
| Windows | %LOCALAPPDATA%\scriptling\packages\ |
Override with --cache-dir or SCRIPTLING_CACHE_DIR environment variable.
Cache Commands
# Clear all cached packages
scriptling cache clearCache TTL
Cached packages are automatically pruned after 7 days of non-use. Each access resets the TTL, so frequently used packages stay cached indefinitely.
Using Packages in Code
Once loaded, packages work like any other module:
# Import from package
import utils
from submodule import helpers
# Use functions
result = utils.process("data")
helpers.format(result)App Bundles
A package with a manifest.toml is a self-contained unit — code, data, and
metadata shipped as one folder or zip. Three types exist:
- App bundle (
servedeclared): starts an HTTP, MCP, or JSON-RPC server. - Script package (
maindeclared, noserve): runs the entry-point script and exits — a standalone tool packaged with its libraries and data. - Library pack (no
main, noserve): provides importable modules only.
Manifest
name = "myapp" # REQUIRED — unique across loaded packages
version = "1.0.0" # REQUIRED
main = "setup.py" # optional: .py file or "module.function"
libs = ["lib", "vendor"] # optional: module search dirs (default ["lib"])
serve = ["http", "mcp"] # optional: protocols to serve
additional_files = ["data/", "LICENSE"] # optional: extra dirs/files to include| Field | Required | Description |
|---|---|---|
name |
yes | Package name. Used by scriptling.package for file access. Must be unique across loaded packages. |
version |
yes | Version string (e.g., "1.0.0"). |
main |
no | Entry point: a .py file path (runs top-level) or module.function. Without serve, the script runs and exits. |
libs |
no | Module search dirs inside the package, searched in order. Default ["lib"]. |
serve |
no | Protocols to serve (e.g. ["http", "mcp"]). When present, the package starts a server instead of running and exiting. |
additional_files |
no | Extra files or directories to include. A trailing / includes the entire directory tree; a bare path includes a single file. |
Transport
The serve list declares what the app provides, not how it’s reached.
The CLI flags decide the transport:
| CLI flags | What happens |
|---|---|
--package . (no --server) |
MCP or JSON-RPC over stdio (whichever is in serve) |
--server :8000 --package . |
All declared protocols over HTTP: MCP at /mcp, JSON-RPC at /json-rpc, HTTP routes at their registered paths |
So serve = ["mcp"] works for both scriptling --package . (stdio) and scriptling --server :8000 --package . (HTTP at /mcp).
Convention Dirs
These top-level dirs are auto-discovered when present:
| Dir | Protocol | Contents |
|---|---|---|
tools/ |
mcp | .py + .toml pairs (MCP tools) |
resources/ |
mcp | Resource tree (static files and {var} templates) |
prompts/ |
mcp | .toml + .py pairs or .md/.txt (MCP prompts) |
webroot/ |
http | Static assets served at the HTTP root |
docs/ |
— | Documentation viewer |
Additional Files
Declare extra files or directories in the manifest to ship data, specs, or configuration alongside your code:
additional_files = ["data/", "LICENSE", "templates/"]A trailing / includes the entire directory tree; a bare path includes
a single file. These are packed into the zip alongside the libs and
convention dirs.
At runtime, files inside a package — including those from additional_files
— are accessible via the scriptling.package library. This works identically
in directory mode and zip mode:
import scriptling.package as package
# Read a file shipped via additional_files
spec = package.read_file("myapp", "data/spec.md")
# List all loaded packages
for name in package.names():
print(name)
# Glob for files
for f in package.glob("myapp", "**/*.md"):
print(f)Every function takes the package name (from the manifest’s name field) as
its first argument, so there’s no ambiguity when multiple packages are loaded.
Use package.exists("name") to check if a package is loaded, and
package.file_exists("name", "path") to check for a specific file.
A fetcher plugin’s attached library is a package here too, named after the
plugin — package.read_file("knot", "...") reads a served file with the same
functions. See Plugin Fetchers for the serving
side.
glob speaks the same pattern language as the fetcher protocol: * and ?
stay within a path segment, ** crosses any number of segments (including
none, so **/*.md also matches a file at the package root).
Note: os.read_file reads from the real filesystem only — it cannot read
files inside a zip package. Use scriptling.package for that.
Running
# Development — run from a folder (hot-reloadable)
scriptling --server :8000 --package ./myapp # HTTP (all serve protocols)
scriptling --package ./myapp # stdio (MCP or JSON-RPC)
# Production — run from a zip
scriptling pack ./myapp -o myapp.zip
scriptling --server :8000 --package myapp.zip
scriptling --server :8000 --package https://host/myapp.zip#sha256=...In app-bundle mode the CLI rejects path/registration flags (-L, --script,
--mcp-tools, --mcp-resources, --mcp-prompts, --web-root, --code,
--interactive) because the manifest owns them. Deployment flags (--server,
--tls-*, --bearer-token, secrets) remain valid.
Extra positional arguments after -- are available to tools and handlers
via sys.argv — useful for conditional tool registration (e.g. gating
write tools behind -- --allow-write). See
Conditional Tool Registration
for details.
main Resolution
main accepts two forms, resolved at boot by lookup order:
- Ends in
.pyand the file exists → run the file top-level (the bundle analogue of--script). - Otherwise →
module.function(evalimport mod+mod.fn()). - Neither resolves → boot error.
So main = "setup.py" runs the file; main = "demo.run" calls the function.
main = "foo.py" with no such file falls back to module foo, function py.
Library Packs (without serve)
A package without serve is a library pack — it provides importable modules
only, exactly as before. The --package flag accepts multiple library packs
alongside one app bundle:
scriptling --server :8000 --package ./myapp --package ./vendor-deps.zipBuild Inclusion
pack build includes exactly: manifest.toml, every libs dir, the main
script file, and the convention dirs when present. Dotfiles are excluded
silently; anything else at the top level produces a warning. Missing declared
libs dirs or main scripts are build errors.
Examples
examples/app-bundle/— reference HTTP + MCP app with routes, tools and webroot.examples/jsonrpc-package/— JSON-RPC server shipped as a package (stdio + HTTP).examples/sample-package/— classic library pack (noserve, proves backward compatibility).
Distribution
Share packages via any HTTP server:
# Create and upload
scriptling pack ./mylib -o mylib.zip
scp mylib.zip server:/var/www/libs/
# Others can use directly
scriptling --package https://yourserver.com/libs/mylib.zip app.pySee Also
- Basic Usage - Running scripts and interactive mode
- HTTP Server Mode - Running as an HTTP server
- MCP Server Mode - Model Context Protocol integration