Synoema Packages: From Your First Install to Publishing Your Own Library

Every programming language eventually needs a way to share code. Python has PyPI, Rust has crates.io, Node has npm. Synoema has its own package ecosystem built around one principle: the most important consumer of a package isn't a human typing commands — it's an LLM generating code.

This changes how the system is designed. When a human uses a package, they Google it, read the README, and copy the import statement. When an LLM agent uses a package, it needs the same information delivered automatically, inline, without any additional round-trips. The Synoema package system is built to serve both audiences — but it's optimized for the machine.

This article covers everything from first install to publishing your own library. We'll start with the basics of using existing packages, move through creating your own, explain how to distribute it, and finish with how MCP-connected AI agents discover and use packages automatically.

Part 1: Using Packages

Finding What You Need

The first question when you need a library is always: does it exist? There are two ways to find out.

The registry at synoema.tech/pkg is the central index. Each package has a page showing its description, version history, README, and the exact commands to install it. The search bar supports keyword and category filtering — if you're looking for something that parses data formats, searching "parsing" will surface packages tagged with that category.

The CLI gives you the same search without a browser:

sno pkg search "json"
sno pkg search "http client"
sno pkg search "csv parsing"

The output shows package name, latest version, description, and whether it's compatible with your current Synoema installation:

json          2.1.0  JSON parsing and serialization      ✓ compatible
json-stream   1.0.2  Streaming JSON parser               ✓ compatible
yaml          0.4.0  YAML support                        ⚠ requires lang >=1.0.0

The compatibility column matters. Packages declare which Synoema versions they support in their manifest. When a package requires a newer compiler than you have installed, you'll see a warning — but you can still install it if you choose.

Installing a Package

Installation is a single command:

sno pkg add json

This fetches the latest compatible version of json from the registry, extracts it to ~/.sno/packages/json@2.1.0/, and updates your sno.lock file. If the package has dependencies of its own, they're installed automatically — you don't need to chase them down manually.

The output tells you exactly what happened:

Installing json@2.1.0 from registry...
  Installing string-utils@1.3.0 (dependency)...
✓ Installed json@2.1.0

You can also pin to a specific version:

sno pkg add json@2.0        -- install exactly version 2.0
sno pkg add json@">=1.5"   -- install latest satisfying >=1.5

Packages don't have to come from the registry. If you have a package as a local directory — perhaps you're developing it or someone sent you a tarball — you can install directly:

sno pkg add ./my-local-lib          -- from a directory on disk
sno pkg add https://example.com/my-lib-1.0.tar.gz  -- from a URL

All three sources follow the same installation path: the package ends up in ~/.sno/packages/ and appears in your lock file.

The sno.lock File

After your first sno pkg add, a sno.lock file appears in your project directory. This is a deterministic record of every installed package and its exact version:

# This file is auto-generated by sno pkg. Do not edit manually.

[[package]]
name     = "json"
version  = "2.1.0"
source   = "registry+https://synoema.tech/pkg"
checksum = "sha256:a3f9b2..."

[[package]]
name     = "string-utils"
version  = "1.3.0"
source   = "registry+https://synoema.tech/pkg"
checksum = "sha256:c8d4e1..."

Commit this file to version control. Anyone who clones your repository and runs your code will get exactly the same package versions you used. This is the same philosophy as Cargo.lock or package-lock.json — reproducibility is not optional.

Importing a Package in Code

This is where Synoema packages feel different from other languages. The import statement uses the package name, not a file path:

import "json"

Compare this to the file-relative imports you might already know:

import "utils.sno"     -- file import: relative to current file
import "./lib/math.sno" -- file import: explicit path
import "json"           -- package import: resolved from ~/.sno/packages/

The rule is simple: if the import string contains a /, ends with .sno, or starts with ., it's a file path. Otherwise, it's a package name. This is fully backwards-compatible — all existing code continues to work.

After importing, you use the use declaration to bring specific names into scope:

import "json"
use Json (parseJson, formatJson)

main =
  let raw = "{\"key\": 42, \"active\": true}"
  let result = parseJson raw
  result

The module name in use is typically the capitalized version of the package name, though package authors can declare a custom module name. Check the package's documentation or use the MCP search_packages tool to find the correct module name and available exports.

If you try to import a package that isn't installed, the compiler tells you exactly what to do:

Error: package 'json' not installed
  Hint: run: sno pkg add json

Managing Installed Packages

To see everything currently installed:

sno pkg list
json          2.1.0  JSON parsing and serialization      ✓ compatible
string-utils  1.3.0  String utilities                    ✓ compatible
http          0.9.0  HTTP client                         ⚠ requires lang >=1.0.0

The --json flag produces machine-readable output — useful if you're scripting over your packages or building tooling on top of the package system.

To remove a package:

sno pkg remove json

This deletes the package directory and updates sno.lock. If other installed packages depend on the one you're removing, you'll see a warning — the system won't block you, but it will make sure you know.

Part 2: Creating a Package

Using packages is straightforward. Creating one takes slightly more thought, but not much more work.

The Directory Structure

A Synoema package is a directory with a sno.toml manifest and one or more .sno source files. The simplest possible package looks like this:

my-lib/
├── sno.toml       -- manifest (required)
└── lib.sno        -- entry point (required by default)

For a larger package you might organize sources into subdirectories:

my-lib/
├── sno.toml
├── lib.sno        -- re-exports everything
├── src/
│   ├── parser.sno
│   ├── formatter.sno
│   └── types.sno
└── SKILL.md       -- optional: AI guidance for this library

The main field in sno.toml tells the package system which file is the entry point — the one that gets loaded when someone writes import "my-lib". It defaults to lib.sno.

The sno.toml Manifest

The manifest is where you describe your package. Here's a complete example with every available field:

[package]
name        = "my-lib"
version     = "1.0.0"
description = "A short description of what this package does"
license     = "MIT"
authors     = ["Your Name <you@example.com>"]
keywords    = ["parsing", "utilities"]
categories  = ["data-formats"]
repository  = "https://github.com/you/sno-my-lib"
homepage    = "https://my-lib.dev"
readme      = "README.md"
main        = "lib.sno"

[lang]
min_version = "0.8.0"
max_version = "<1.0.0"

[dependencies]
string-utils = ">=1.0.0"

Let's go through each field:

name (required) — The package identifier. Must be kebab-case: lowercase letters, numbers, and hyphens. No spaces, no uppercase. This is what users type in sno pkg add <name>.

version (required) — Semantic versioning: MAJOR.MINOR.PATCH. Increment MAJOR for breaking changes, MINOR for new backwards-compatible functionality, PATCH for bug fixes. Start at 1.0.0 for a stable release or 0.1.0 for initial development.

description — One sentence. This appears in search results and on the registry page. Make it specific: "JSON parsing and serialization" is better than "utilities".

license — An SPDX identifier. Common choices: MIT, Apache-2.0, GPL-3.0. If you don't specify, the package is assumed to have no license (all rights reserved).

authors — A list of strings in the format "Name <email>". The email is optional but helps users contact you about issues.

keywords — Up to 5 tags used for registry search. Think about what terms someone would type when looking for what your package does. These feed directly into the search_packages MCP tool used by AI agents.

categories — A structured taxonomy. Unlike keywords, categories have a fixed list (visible on the registry). Common categories: encoding, parsing, networking, math, iot, data-structures.

repository — A link to the source code. The registry shows a "View source" button when this is present.

readme — The path to your README file, relative to the package root. The registry renders this as the main content of your package page. If you omit this and a README.md exists, it's used automatically.

main — The entry point file. Defaults to lib.sno. If you organize sources differently, set this explicitly: main = "src/main.sno".

[lang] min_version / max_version — The range of Synoema versions your package supports. Use min_version to declare the oldest compiler you've tested against. Use max_version to set an exclusive upper bound — for example, "<1.0.0" means "works on any 0.x release, not tested on 1.0 or later". Users installing on an incompatible version get a warning, not an error.

[dependencies] — Other packages your library needs. Each entry is a package name and a semver range. The range syntax supports: >=1.0.0, <2.0.0, ^1.2.0 (compatible with 1.2.0, less than 2.0.0), ~1.2.0 (compatible with 1.2.0, less than 1.3.0), and =1.2.0 (exact). When someone installs your package, its dependencies are installed automatically.

Writing the Library Code

The entry point file (lib.sno) is just a regular Synoema source file. Users who import your package will load this file first. A common pattern is to define your core logic in submodules and re-export the public API from lib.sno:

-- lib.sno
import "src/parser.sno"
import "src/formatter.sno"

use Parser (parseJson, parseJsonArray)
use Formatter (formatJson, formatPretty)

-- Re-export the public API
module Json where

parseJson = Parser.parseJson
parseJsonArray = Parser.parseJsonArray
formatJson = Formatter.formatJson
formatPretty = Formatter.formatPretty

Users of your package see only what you export from lib.sno. Internal implementation details in subdirectory files stay internal.

Adding AI Guidance with SKILL.md

This is where the Synoema package system diverges from traditional package managers. You can ship a SKILL.md file alongside your library code. This file is guidance for LLM agents about how to use your package correctly.

---
name: json
description: JSON parsing and serialization
version: 2.1.0
when: Working with JSON data, parsing API responses, serializing records to JSON
---

# JSON Package

## Import

```
import "json"
use Json (parseJson, formatJson, parseJsonArray)
```

## Parsing

```
-- Parse a JSON string into a Synoema value
result = parseJson "{\"name\": \"Alice\", \"age\": 30}"

-- Parse a JSON array
items = parseJsonArray "[1, 2, 3]"
```

## Serialization

```
-- Format a value as JSON
json = formatJson {name = "Alice", age = 30}
```

## Common mistakes

- Don't use single quotes in JSON strings — JSON requires double quotes
- `parseJson` returns a Result — handle both Ok and Err cases

When this file is present, the MCP search_skills tool includes it in its results. An AI agent that asks "how do I parse JSON in Synoema?" will receive your guidance directly, with the source tagged as your package. This dramatically improves the accuracy of LLM-generated code using your library.

Part 3: Publishing to the Registry

Getting an API Key

Publishing requires an account. Authentication is handled through GitHub OAuth — if you have a GitHub account, you're one click away from an API key.

Go to synoema.tech/pkg and click "Sign in with GitHub". After authorizing the OAuth application, you'll land on your dashboard. Click "Generate API Key" — the key will be shown exactly once, so copy it immediately.

The key looks like this: sno_pkg_4f3a9b2e1c...

Saving the Key Locally

Save the key to your local configuration so the CLI can use it:

sno pkg login --key "sno_pkg_4f3a9b2e1c..."

Or let the CLI prompt you interactively (useful when you don't want the key in your shell history):

sno pkg login
Enter API key: [hidden input]
✓ API key saved. Run 'sno publish' from your package directory.

The key is stored in ~/.sno/config.toml under the [registry] section with file permissions set to 600 — readable only by your user account. You only need to do this once per machine.

Publishing

From your package directory (the one containing sno.toml):

sno publish

The command validates your manifest, creates a compressed tarball of your package files, and uploads it to the registry. Here's what you'll see for a successful publish:

Publishing my-lib@1.0.0...
✓ Published my-lib@1.0.0 → synoema.tech/pkg/my-lib

A few things the publish command checks before uploading:

If any of these checks fail, you get a clear error message before any upload attempt. Common issues and their fixes:

Error: Invalid API key. Run: sno pkg login --key YOUR_KEY
-- Fix: regenerate your key at synoema.tech/pkg and run sno pkg login again

Error: my-lib@1.0.0 already exists in registry
-- Fix: bump the version in sno.toml and publish again

Error: 'lib.sno' not found (declared as main in sno.toml)
-- Fix: create lib.sno or update the main field to point to your actual entry file

Versioning Your Releases

Once a version is published, it cannot be overwritten. The registry is append-only for versions. This is intentional: users who depend on my-lib@1.0.0 should always get the same code, forever.

To release an update, increment the version in sno.toml and publish again:

-- sno.toml: change "1.0.0" to "1.1.0"
sno publish

The semver convention is: PATCH for bug fixes that don't change the API, MINOR for new functions added backwards-compatibly, MAJOR for anything that could break existing code. If you add a new function to your library, that's a MINOR bump. If you rename or remove a function, that's a MAJOR bump.

The [lang] max_version field deserves special mention here. If you're not sure whether your package works with a future Synoema release, set an upper bound: max_version = "<1.0.0". Users on Synoema 1.x will see a warning but can still install. When you've tested your package against 1.x and confirmed it works, release a new version with the constraint removed or updated.

Declaring Dependencies

If your package uses other Synoema packages, declare them in sno.toml:

[dependencies]
string-utils = ">=1.0.0"
json = "^2.0.0"

When users install your package, these dependencies are installed automatically. The semver ranges give you flexibility: >=1.0.0 means any version 1.0 or later. ^2.0.0 means any version compatible with 2.0.0 — in practice, any 2.x release.

Keep your dependency ranges as broad as possible without breaking compatibility. Overly tight ranges (like =2.1.0) cause version conflicts when users have multiple packages that depend on the same library at different versions.

Part 4: How AI Agents Discover Packages

This is the part of the package system that has no equivalent in other languages. When a Synoema project is accessed via MCP — the Model Context Protocol used by AI coding assistants like Claude — the package system integrates directly with the AI's code generation loop.

Auto-Suggestions on Unknown Identifiers

Consider an LLM agent writing Synoema code that uses parseJson without having installed the json package. The compiler rejects the code:

Error: unknown identifier: parseJson

In a traditional workflow, this error goes back to the LLM, which then has to figure out what package provides parseJson, then generate a new version of the code that installs it first. That's slow and often fails — the LLM might guess wrong about the package name.

With Synoema's MCP integration, something different happens. The MCP server intercepts the error response and automatically queries the registry: "what packages provide an identifier called parseJson?" It finds the json package, and adds this to the response returned to the agent:

{
  "error": "unknown identifier: parseJson",
  "retrieval_context": {
    "packages": [{
      "name": "json",
      "version": "2.1.0",
      "provides": ["parseJson", "formatJson"],
      "install": "sno pkg add json",
      "import": "import \"json\"\nuse Json (parseJson)"
    }]
  }
}

The agent sees this suggestion, decides to install the package, and retries — all within the same generation turn. No human intervention required. This is why the keywords and provides metadata in your package matters: it's the signal the registry uses to match unknown identifiers to packages.

Active Package Search

AI agents connected via MCP can also search the registry proactively using two tools:

search_packages(query, lang_version?) — searches by name, description, and keywords. Returns a list of packages with install commands and import hints. An agent researching how to do HTTP requests might call search_packages("http client") before writing any code.

suggest_packages(code) — takes a code snippet as input, analyzes it for identifiers that look like they might come from packages, and returns suggestions. This is useful when an agent has already drafted code and wants to find what needs to be installed before running it.

SKILL.md as AI Documentation

When your package includes a SKILL.md file, it becomes part of the MCP skill discovery system. The search_skills tool scans installed packages for SKILL.md files and returns them as part of its results, tagged with your package name and version.

This means that the moment a user installs your package, any AI agent they're using gains guidance about how to use it correctly. The SKILL.md file is not just documentation — it's a direct channel from you, the package author, to every AI that will ever work with your library.

Write your SKILL.md with this in mind. Focus on:

The format is a YAML frontmatter block followed by Markdown content. Keep it concise — AI models have context limits, and a 200-line SKILL.md that covers the 80% case is more useful than a 2000-line one that covers everything.

Putting It All Together

Here's a complete workflow from finding a package to using it in production code:

-- 1. Find the package
sno pkg search "json"

-- 2. Install it
sno pkg add json

-- 3. Use it in your code
import "json"
use Json (parseJson, formatJson)

parseResponse : String -> Result String Error
parseResponse raw =
  let parsed = parseJson raw
  ? parsed is Ok data -> Ok (data.message)
                : Err (error "missing 'message' field")

main =
  let response = "{\"message\": \"Hello, world!\"}"
  let result = parseResponse response
  ? result is Ok msg -> printLine msg
                : Err e -> printLine ("Error: " ++ show e)

And for publishing your own work:

-- 1. Create your package
mkdir my-parser
cd my-parser

-- 2. Write sno.toml
-- [package]
-- name = "my-parser"
-- version = "1.0.0"
-- description = "A parser for my custom format"
-- keywords = ["parsing", "my-format"]

-- 3. Write your library code in lib.sno

-- 4. (Optional) Add SKILL.md for AI guidance

-- 5. Get your API key at synoema.tech/pkg

-- 6. Publish
sno pkg login --key "sno_pkg_..."
sno publish

What's Next

The package ecosystem is new. The registry is running but the community of packages is just beginning to form. If you've built something useful in Synoema — a data format parser, a utility library, an API client — publishing it now means being among the first libraries that AI agents will learn to use.

A few things on the roadmap that will improve the experience: a lockfile-based reproducibility guarantee for CI environments, project-local package directories for monorepo setups, and a verification pass that runs package tests as part of publish.

For now, the basics are solid. You can install packages, write libraries, publish them, and watch AI agents discover and use your work automatically. That's a complete loop — and it's all running today.

Explore the Package Registry → Language Reference