**Registry modules:** ```mlld import { @sortBy, @unique } from @mlld/array import @corp/utils as @corp >> With version import { @helper } from @alice/utils@1.0.0 import { @helper } from @alice/utils@^1.0.0 >> semver range ``` **Local files (selected exports):** ```mlld import { @helper } from "./utils.mld" import { @config } from <@root/config.mld> import { @prompt } from "../prompts/main.mld" import { @renderHeader, @renderBody } from "./templates.mld" ``` **Local files (namespace import):** ```mlld import "./utils.mld" as @utils show @utils.helper("report") ``` **Path resolution:** - `./` and `../` paths resolve from the importing file's directory, not the shell cwd. - `<@root/...>` resolves from the project root. **Namespace imports:** ```mlld import @alice/utils as @alice import @bob/utils as @bob show @alice.format(@data) show @bob.format(@data) >> no collision ``` **Directory imports:** ```mlld import "@agents" as @agentRegistry show @agentRegistry.alice.tldr show @agentRegistry.support.helper.name >> With options import "./agents" as @agents with { skipDirs: [] } ``` Directories auto-load `*/index.mld`. Default `skipDirs: ["_*", ".*"]`. Use directory imports when you want a namespace object. If you want selected exports, import the entry file directly: ```mlld import { @helper } from "./agents/index.mld" ``` Relative directory paths resolve from the importing file's directory, not the shell cwd. **Template collections** import an entire directory of `.att` files as a callable namespace: ```mlld import templates from "@root/agents" as @agents(message, context) >> Access templates by name (bracket or dot notation) show @agents["alice"](@msg, @ctx) >> agents/alice.att show @agents.support["helper"](@msg, @ctx) >> agents/support/helper.att ``` **Directory structure example:** ``` agents/ alice.att >> @agents["alice"](msg, ctx) bob.att >> @agents["bob"](msg, ctx) support/ helper.att >> @agents.support["helper"](msg, ctx) escalate.att >> @agents.support["escalate"](msg, ctx) ``` **Key rules:** - All templates in a collection share the same parameter signature - Filenames with hyphens become underscores: `json-pretty.att` → `@tpl["json_pretty"]` - Use dot notation for directories, brackets for template names - Collections require parameters in the `as @name(params)` clause **When to use collections:** - Multiple templates with the same interface (agent prompts, formatters) - Dynamic template selection based on runtime values - Organizing related templates by category For single templates, use `exe @func(params) = template "file.att"` instead. **Basic node imports:** ```mlld import { basename } from node @path var @name = @basename("/tmp/file.txt") show @name ``` **Namespace imports:** ```mlld import { posix } from node @path var @dir = @posix.dirname("/tmp/file.txt") show @dir ``` **Constructor expressions:** ```mlld import { URL } from node @url exe @site = new @URL("https://example.com/path?x=1") show @site.hostname show @site.pathname ``` **Automatic wrapping:** - Functions are wrapped to accept and return mlld values - Method binding is preserved (correct `this` context) - Classes support `new @Constructor()` syntax - Async iterables are wrapped as streams - Promises work transparently - Callback-style functions trigger warnings **Module resolution:** - Uses Node.js module resolution from the importing file's directory - Supports both CommonJS and ES modules - Built-in modules: `node @path`, `node @fs`, `node @url`, etc. - NPM packages: Use package name without leading `@` (e.g., `node @chalk`) | Type | Behavior | Use Case | |------|----------|----------| | `module` | Content-addressed cache | Registry modules (default) | | `static` | Embedded at parse time | Prompts, templates | | `live` | Always fresh | Status APIs | | `cached(TTL)` | Time-based cache | Feeds, configs | | `local` | Dev modules (llm/modules/) | Development | | `templates` | Directory of .att files | Template collections | ```mlld import module { @api } from @corp/tools import static { @prompt } from "./prompt.md" import live as @status import cached(1h) as @feed import local { @dev } from @alice/experimental ``` **Built-in:** - `@author/module` → Registry - `@root/file` → Project root (preferred) - `@base/file` → Project root (alias for @root) - `./file.mld` → Local (with fuzzy extension matching) **Custom prefixes** (mlld-config.json): ```json { "resolvers": { "prefixes": [ { "prefix": "@lib/", "resolver": "LOCAL", "config": { "basePath": "./src/lib" } }, { "prefix": "@company/", "resolver": "GITHUB", "config": { "repository": "company/private-modules", "branch": "main" } } ] } } ``` `mlld` reads `resolvers.prefixes` for resolver mappings. It also reads top-level `resolverPrefixes` when present, and CLI writers persist the nested `resolvers.prefixes` shape. **Quick setup:** ```bash mlld alias --name notes --path ~/notes mlld alias --name shared --path ../shared --global mlld setup --github # private repo wizard ``` When you install modules, mlld creates `mlld-lock.json` to ensure reproducible imports. This file tracks exact versions and content hashes. ```json { "lockfileVersion": 1, "modules": { "@alice/utils": { "version": "1.0.0", "resolved": "abc123def456...", "source": "@alice/utils", "sourceUrl": "https://registry.mlld.org/modules/@alice/utils/1.0.0", "integrity": "sha256:abc123...", "fetchedAt": "2024-01-15T10:00:00Z", "registryVersion": "1.0.0" } } } ``` **Lock entry fields:** - **version** - The exact version installed (from registry version.json) - **resolved** - Content hash used for cache lookup (SHA256) - **source** - Original module specifier from your imports - **sourceUrl** - URL where the module was fetched from - **integrity** - Content hash for verification (sha256:...) - **fetchedAt** - Timestamp when module was installed - **registryVersion** - Version from registry metadata (only for registry modules) **Behavior:** - **Auto-generated** - Created/updated by `mlld install` - **Version control** - Commit to git for reproducible builds - **Never edit manually** - Use CLI commands to update - **Registry-only validation** - Lock file only enforces version matches for registry modules - **Version pinning** - `version: "latest"` updates to newest on `mlld update`; exact version (e.g. `"1.2.0"`) stays pinned until manually changed Module-first design: keep `.mld` files readable; move complexity into focused modules. Avoid "kitchen sink" modules or side effects on import. ```mlld --- name: text-utils author: alice version: 1.0.0 about: String helpers license: CC0 --- needs { js: [] } exe @upper(s) = js { return s.toUpperCase() } exe @trim(s) = js { return s.trim() } export { @upper, @trim } ``` **Frontmatter fields:** - `name` - Module name (required for registry) - `author` - Your username (required for registry) - `version` - Semver version - `about` - Brief description - `license` - License (CC0 recommended) Modules only expose items listed in `export { }`. Unexported items stay private. ```mlld exe @greet(name) = `Hello, @name!` exe @farewell(name) = `Goodbye, @name!` var @_helper = "internal" >> Only @greet and @farewell are visible to importers export { @greet, @farewell } ``` **Why explicit exports:** Encapsulation. Importers see a clean API surface. Internal helpers, intermediate variables, and implementation details stay hidden. Rename or remove internals without breaking callers. **Wildcard export:** Export everything (same as no `export` directive): ```mlld export { * } ``` **Environment module pattern:** Modules that wrap credentials export executables and let callers import the policy separately for credential configuration: ```mlld policy @p = { auth: { claude: { from: "keychain:mlld-box-{projectname}/claude-dev", as: "ANTHROPIC_API_KEY" } } } exe @spawn(prompt) = run cmd { claude -p "@prompt" } using auth:claude export { @spawn } ``` **Notes:** - Accessing unexported items via namespace import raises a runtime error - Exported executables do not expose captured module internals through field access - Guards can be exported alongside variables - Without an `export` directive, all module-level items are auto-exported Modules are directories with an entry point, manifest, and optional supporting files. ``` mymodule/ ├── index.mld # Entry point ├── module.yml # Manifest (or .yaml, .json) ├── README.md # Documentation └── lib/ # Optional supporting files ``` module.yml format: ```yaml name: myapp author: alice type: app # library | app | command | skill about: "Description" version: 1.0.0 license: CC0 ``` Directory modules use two metadata sources: | Source | Lives In | Used For | |--------|----------|----------| | Frontmatter | Entry `.mld` file (`--- ... ---`) | Runtime metadata (`@fm`, imported namespace `.__meta__`) | | `module.yml` | Module directory root | Packaging metadata (`type`, publish/install metadata) | Keep shared identity fields (`name`, `author`, `version`, `about`) aligned between frontmatter and `module.yml`. | Type | Purpose | Local Path | Global Path | |------|---------|------------|-------------| | library | Importable code | llm/lib/{name}/ | ~/.mlld/lib/{name}/ | | app | Runnable scripts | llm/run/{name}/ | ~/.mlld/run/{name}/ | | command | Claude slash cmd | .claude/commands/{name}/ | ~/.claude/commands/{name}/ | | skill | Claude skill | .claude/skills/{name}/ | ~/.claude/skills/{name}/ | ```bash mlld module app myapp # Create app in llm/run/myapp/ mlld module library utils # Create library in llm/lib/utils/ mlld module command review # Create command in .claude/commands/review/ mlld module skill helper # Create skill in .claude/skills/helper/ mlld module app myapp --global # Create in ~/.mlld/run/myapp/ ``` ```bash mlld run myapp # Runs llm/run/myapp/index.mld mlld run # Lists available scripts including apps ``` Use `index.mld` as the module entry point convention. ```bash mlld install @author/my-app --global # Install to ~/.mlld/run/my-app/ mlld install @author/my-lib -g # Install to ~/.mlld/lib/my-lib/ ``` Packed modules are single-file bundles created by `mlld pack` (future feature). Use packed format for gist publishing; standard module format otherwise. **Module patterns:** ```mlld >> Library module (wraps a core exe with shortcuts) import { @claude } from @mlld/claude exe llm @haiku(prompt) = @claude(@prompt, { model: "haiku" }) exe llm @sonnet(prompt) = @claude(@prompt, { model: "sonnet" }) export { @haiku, @sonnet } >> Config/agent module var @meta = { id: @fm.id, name: @fm.name } var @prompts = { primary: @primaryPrompt, optional: @optionalPrompt } export { @meta, @prompts } >> Gate module exe @gate(response, instruction, message) = [...] export { @gate } ``` ``` llm/modules/ ├── my-utils.mld.md # author: alice, name: experimental └── helpers.mld # author: bob, name: tools ``` ```mlld import local { @helper } from @alice/experimental ``` Matched by frontmatter `author` and `name` fields. ## Prerequisites 1. **GitHub account** - You'll authenticate via GitHub 2. **mlld CLI** - Install via `npm install -g mlld` 3. **Module file** - Your `.mld`/`.mld.md` file with frontmatter and `needs` ## Required Metadata ```yaml --- name: my-tool # or use title: My Tool (name falls back from title) author: yourname version: 1.0.0 about: Brief description # description: ... also supported tags: [utils, strings] # optional (keywords also supported) license: CC0 --- ``` Required: module name/title, author, version, and about/description. License must be CC0. `mlld publish` now reads these frontmatter fields as metadata defaults, and CLI metadata flags override them when provided. ## Authentication ```bash mlld auth login # Opens GitHub OAuth flow mlld auth status # Check auth status mlld auth logout # Logout ``` Grants `gist` scope (create Gists for module source) and `public_repo` scope (create PRs to registry). ## First-Time Module (PR Workflow) ```bash mlld publish my-tool.mld.md ``` 1. **Validation** - Checks syntax, exports, metadata 2. **Source Creation** - Creates Gist or references repo 3. **PR Creation** - Opens PR to mlld-lang/registry 4. **Automated Review** - LLM reviews for no hardcoded secrets, safe operations, real utility, proper licensing 5. **Manual Review** (if needed) - Maintainer approval 6. **Merge** - Module becomes available 7. **Publish Rights** - You can update directly going forward ## Module Updates (Direct Publish) After first module is merged: ```bash mlld publish my-tool.mld.md ``` 1. **Version Bump** - Prompts for patch/minor/major 2. **Validation** - Same checks as first-time 3. **Direct Publish** - No PR needed (if authenticated) 4. **Registry Update** - New version available immediately Force PR workflow: `mlld publish --pr my-tool.mld.md` ## Module Source **GitHub Repository** — If your module is in a git repo, `mlld publish` detects the repository URL, current commit SHA, module file path, and whether repo is clean. Source references the commit SHA, ensuring immutability. **Gist** — If not in a repo, creates a GitHub Gist automatically with module content, versioned via Gist revisions. ## Validation Your module must pass validation before publishing: - **Syntax** - No syntax errors, no reserved word conflicts, valid directives - **Exports** - `export` directive present, exported names exist, no duplicates - **Imports** - Valid module references, no circular dependencies - **Metadata** - All required fields present, author matches GitHub username, license is CC0, version follows semver ## Automated Review LLM reviews check for no secrets, safe operations, real utility, proper licensing, accurate metadata. Review posts as PR comment with APPROVE, REQUEST_CHANGES, or COMMENT. Push new commits to trigger re-review. Trusted authors in allowlist skip LLM review and auto-merge if CI passes. ## Direct Publish via API ```bash mlld publish my-tool.mld.md # Via CLI (recommended) curl -X POST https://registry-api.mlld.org/api/publish \ -H "Authorization: Bearer $TOKEN" \ -F "module=@my-tool.mld.md" # Via API directly ``` ## Commands ```bash mlld publish my-tool.mld.md # Publish to registry mlld publish --pr my-tool.mld.md # Force PR workflow mlld publish --tag beta my-tool.mld.md # Publish with tag mlld publish --dry-run my-tool.mld.md # Validate without publishing mlld publish my-tool.mld.md --title "My Tool" --tags utils,strings ``` For private or internal modules, use local imports or custom resolvers instead of the public registry. ## Local Filesystem Distribute modules via git or file sharing: ```mlld import { @helper } from "./shared/utils.mld" import "./lib/internal" as @internal ``` ## Custom Resolvers Configure custom `@` prefixes for private registries or internal repos: ```json { "resolvers": { "@company": "https://internal-registry.company.com/modules" } } ``` ```mlld import { @auth } from @company/auth-utils ``` See `mlld howto resolvers` for resolver configuration. ## Development Modules Use `llm/modules/` for in-development modules: ```mlld import local { @tool } from @alice/dev-module ``` These are resolved from the local `llm/modules/` directory without registry lookup. **Check installed modules:** ```bash mlld ls # list all installed modules with versions ``` **Update a specific module:** ```bash mlld update @alice/utils # fetch latest version ``` **Update all modules:** ```bash mlld update # updates everything in mlld-lock.json ``` **Check module info:** ```bash mlld registry info @alice/utils # show module details from registry ``` **Version pinning:** The lock file (`mlld-lock.json`) tracks installed versions: - `version: "latest"` - updates to newest on `mlld update` - `version: "1.2.0"` - stays pinned until manually changed **After updating:** Verify new exports are available: ```bash mlld validate your-file.mld # check imports resolve ``` ## Semantic Versioning Follow semver (major.minor.patch): - **1.0.0** - Initial release - **1.0.1** - Bug fix (backward compatible) - **1.1.0** - New feature (backward compatible) - **2.0.0** - Breaking change ## Version Tags Publish with tags: ```bash mlld publish --tag beta my-tool.mld.md ``` Import via tag: ```mlld import { @helper } from @alice/my-tool@beta import { @helper } from @alice/utils@^1.0.0 ``` Common tags: - `latest` - Most recent stable (default) - `stable` - Recommended version - `beta` - Beta testing - `alpha` - Alpha testing ## Version Ranges Specify ranges in `mlld-config.json`: ```json { "dependencies": { "@alice/my-tool": "^1.0.0", "@bob/utils": "~1.2.0", "@eve/lib": ">=1.0.0 <2.0.0" } } ``` Lock file pins exact versions. ## Version Resolution in Imports | Import | Version | |--------|---------| | `import ... from @alice/utils` | latest | | `import ... from @alice/utils@1.0.0` | exact | | `import ... from @alice/utils@^1.0.0` | compatible | | `import ... from @alice/utils@beta` | tag | ## Registry Structure Modules are stored with version history: ``` registry/ └── modules/ └── alice/ └── my-tool/ ├── metadata.json # Core info, owners ├── 1.0.0.json # Version 1.0.0 ├── 1.0.1.json # Version 1.0.1 └── tags.json # latest, stable, etc. ``` ## metadata.json ```json { "name": "my-tool", "author": "alice", "about": "Brief description", "owners": ["alice"], "maintainers": [], "created": "2024-01-01T00:00:00Z", "createdBy": 12345, "firstPublishPR": 123 } ``` ## {version}.json ```json { "version": "1.0.0", "needs": ["js", "sh"], "license": "CC0", "mlldVersion": ">=1.0.0", "source": { "type": "github", "url": "https://raw.githubusercontent.com/...", "contentHash": "sha256:abc123...", "repository": { "type": "git", "url": "https://github.com/alice/repo", "commit": "abc123", "path": "my-tool.mld.md" } }, "dependencies": { "js": { "packages": ["lodash"] } }, "keywords": ["utility", "automation"], "publishedAt": "2024-01-01T00:00:00Z", "publishedBy": 12345 } ``` Key fields: - **publishedBy** - GitHub user ID of the publisher (numeric ID, not username) - **publishedAt** - ISO timestamp when this version was published - **source.type** - Source type: `github`, `gist`, or `private-repo` - **source.contentHash** - SHA256 hash for content verification - **source.repository** - Git repository metadata (for github/private-repo sources) ## tags.json ```json { "latest": "1.0.1", "stable": "1.0.1", "beta": "2.0.0-beta.1" } ``` ## Registry API ```bash # Resolve version curl https://registry-api.mlld.org/api/resolve?module=@alice/my-tool # Direct publish curl -X POST https://registry-api.mlld.org/api/publish \ -H "Authorization: Bearer $TOKEN" \ -F "module=@my-tool.mld.md" ``` ## Module Owners After your first PR merges, you become module owner: - Can publish updates directly (no PR needed) - Can add maintainers - Module namespaced under your GitHub username ## Maintainers Add collaborators to `metadata.json`: ```json { "owners": ["alice"], "maintainers": ["bob", "eve"] } ``` Maintainers can also publish updates. ## Organization Modules Publish under org namespace: ```yaml --- author: company name: auth-tool --- ``` Requires write access to `@company` in registry. LLM modules follow a `(prompt, config)` calling convention. The first argument is always the prompt text. The second is an optional config object that controls model selection, tool access, streaming, and other behavior. ```mlld import { @claude, @haiku } from @mlld/claude >> Simple — model shortcut, no config show @haiku("What is REST?") >> Full control — config object var @result = @claude("Review the auth module", { model: "opus", tools: ["Read", "Grep"], stream: true }) ``` ## The config object The `@mlld/claude` module defines these config fields: | Field | Type | Default | Purpose | |-------|------|---------|---------| | `model` | string | `"sonnet"` | Model name: `haiku`, `sonnet`, `opus` | | `dir` | string | `@root` | Working directory for tool operations | | `tools` | array | — | Tool access list (see below) | | `stream` | boolean | — | Enable token streaming | | `system` | string | — | Appended system prompt | Poll variants (`@claudePoll`, `@claudePollJsonl`, `@claudePollEvent`) extend the config with additional fields like `poll`, `timeout`, `pattern`, `event`, and `itemId`. See the `@mlld/claude` module README for details. **Convention, not schema:** The runtime recognizes `tools` and `dir` from the config object (see next section). Everything else — `model`, `system`, `stream` — is handled by the module implementation, not the runtime. Module authors can add their own fields. ## Runtime support for `config.tools` When an `exe llm` function is invoked with a config object containing a `tools` property, the runtime automatically: 1. **Detects** the second argument as a config object (plain object with a `tools` key) 2. **Normalizes** the tools array (strings for built-in tools, exe refs for mlld functions) 3. **Reads `config.dir`** if present, to set the working directory for bridge operations 4. **Creates MCP bridges** — temporary servers that expose the requested tools 5. **Populates `@mx.llm`** with bridge metadata for the exe body to consume ```mlld exe llm @agent(prompt, config) = [ >> These are set automatically by the runtime: >> @mx.llm.config — path to generated MCP config file (empty string if no bridges) >> @mx.llm.allowed — unified tool name list for --allowedTools >> @mx.llm.native — native tool names CSV (empty when no native tools requested) >> @mx.llm.inBox — true when inside a box with active VFS bridge >> @mx.llm.hasTools — true when config.tools was specified => @prompt | cmd { claude -p --model sonnet --allowedTools "@mx.llm.allowed" } ] ``` This is why `exe llm` functions don't need to manually construct `--mcp-config` flags or manage bridge lifecycles — the runtime handles it. ### Tool types in the array The `tools` array accepts a mix of string names and exe references: ```mlld exe @summarize(text) = cmd { ... } var @r = @claude("Analyze this codebase", { tools: ["Read", "Grep", @summarize] }) ``` | Entry type | What happens | |------------|-------------| | String (`"Read"`) | Routed to built-in capability. Inside a box, proxied through VFS bridge. Outside a box, passed as native CLI tool name. | | Exe ref (`@summarize`) | Wrapped as an MCP tool via a function bridge. The runtime generates a JSON schema from the function signature. | ### Empty tools Passing an empty array signals "no tools" — the runtime sets `@mx.llm.hasTools = true` but provides no bridges. The module can use this to pass `--tools ""` to disable all default tools: ```mlld var @r = @claude("Pure text generation", { tools: [] }) ``` ## Box interaction Tool routing changes based on whether the call happens inside a `box`: **Outside a box:** String tools pass through as native names. The child process uses them directly. **Inside a box:** String tools are proxied through the workspace VFS bridge. File operations route through the box's in-memory filesystem, and the child process cannot access the real filesystem. ```mlld var @ws = box [ file "data.csv" = "id,name\n1,Alice\n2,Bob" let @r = @claude("Summarize the CSV file", { model: "haiku", tools: ["Read"] }) >> @claude's Read tool sees data.csv in the VFS, not the real filesystem => @r ] ``` The `@mx.llm.native` field tells module implementations which native tools are active. When `native` is empty — either because the call is inside a box (all tools route through VFS bridges) or because the tools array contains only exe refs — the module should pass `--tools ""` to suppress the CLI's default built-in tools. The `@mlld/claude` module does this automatically. ## Streaming Streaming is wired at the exe definition level using `with { stream, streamFormat }`: ```mlld exe llm @myLLM(prompt, config) = [ let @cfg = @config ? @config : {} => @prompt | cmd { claude -p --model sonnet } ] with { stream: @cfg.stream, streamFormat: @myAdapter } ``` The `@mlld/claude` module does this internally — when you pass `stream: true` in config, the underlying exe's `with` clause activates streaming. You don't need to configure this when using the module: ```mlld show @claude("Explain TCP/IP", { model: "haiku", stream: true }) ``` See `stream` for format adapters and streaming details. ## Writing your own LLM module An LLM module follows this pattern: ```mlld >> 1. Label with llm so the runtime processes config.tools >> 2. Accept (prompt, config) as the calling convention >> 3. Read @mx.llm for bridge metadata set by the runtime exe llm @myLLM(prompt, config) = [ let @cfg = @config ? @config : {} let @model = @cfg.model ? @cfg.model : "sonnet" => when [ @mx.llm && @mx.llm.config && @mx.llm.native => @prompt | cmd { my-llm-cli --model @model --mcp-config "@mx.llm.config" --tools "@mx.llm.allowed" } @mx.llm && @mx.llm.config => @prompt | cmd { my-llm-cli --model @model --disable-builtin-tools --mcp-config "@mx.llm.config" --tools "@mx.llm.allowed" } * => @prompt | cmd { my-llm-cli --model @model } ] ] >> Model shortcuts exe llm @fast(prompt) = @myLLM(@prompt, { model: "fast" }) exe llm @smart(prompt) = @myLLM(@prompt, { model: "smart" }) export { @myLLM, @fast, @smart } ``` **Key points:** - The `llm` label is required — without it, the runtime won't process `config.tools` or populate `@mx.llm` - Always default missing config: `let @cfg = @config ? @config : {}` - Branch on `@mx.llm.config` and `@mx.llm.native` to handle bridged, exe-ref-only, and unbridged invocations - Shortcuts should delegate to the core exe, not duplicate the implementation - The runtime cleans up bridge temp files automatically when the exe scope exits