PUBLISH YOUR FIRST EXTENSION
In this tutorial, we will build a swamp extension model from scratch, run it, score it, take it through every gate the publishing pipeline enforces, and then install it into a second repository. The extension measures the Shannon entropy of a piece of text — a small, self-contained computation that lets us focus on the journey from source file to published package.
Publishing is a state machine: each step gates the next, and the final push is refused until every earlier gate has passed. We will walk that machine one gate at a time and watch swamp check our work before letting us continue.
What we will build
We will write a model called @stack72/text-entropy, give it a README and a
license, run it locally, score it 14/14 on the quality rubric, validate it with
a dry run, and finish by pulling it into a fresh repository. Replace @stack72
with your own collective throughout.
Before we start
This tutorial assumes swamp is installed and you have signed in. If you have not done either, run:
$ curl -fsSL https://swamp-club.com/install.sh | sh$ swamp auth loginConfirm nobody has built it already
Before writing anything, we check the registry. There is no point publishing something that already exists.
$ swamp extension search entropyswamp opens an interactive registry browser. With no matches, it shows:
> entropy 0 / 0
──────────────────────────────────┬───────────────────────────────────────
No matching extensions found │
──────────────────────────────────┴───────────────────────────────────────
↑/↓ navigate Ctrl-u/d scroll preview i install Enter select Esc cancelNothing covers text entropy. Press Esc to exit the browser. We will build it
ourselves.
Initialize a repository
Create a directory and initialize it as a swamp repo:
$ mkdir entropy-ext
$ cd entropy-ext
$ swamp repo initYou will see the swamp banner followed by:
info repo·init Initialized swamp repository at "..." (tools: "claude")
info repo·init What's next:
info repo·init → Start Claude Code and run /swamp-getting-started
info repo·init → Read the manual at https://swamp-club.com/manualNotice the .swamp.yaml file this created — it marks the directory as a swamp
repository, and the publishing pipeline refuses to run anywhere that file is
absent.
Confirm who we are
The pipeline publishes under a collective, and that collective must match our account. Check it:
$ swamp auth whoamiYou will see output like:
stack72 ([email protected]) on https://swamp-club.com
Collectives: swamp, system-initiative, stack72Notice your username in the Collectives list — the extension we publish must
be named @<that-name>/....
Write the model
Create the model file at extensions/models/text_entropy.ts:
/**
* Computes the Shannon entropy of a piece of text.
*
* @module
*/
import { z } from "npm:zod@4";
/** Global arguments: the text whose entropy we measure. */
const GlobalArgsSchema = z.object({
text: z.string().describe("The text to measure entropy for"),
});
/** Shape of the stored entropy result. */
const ResultSchema = z.object({
text: z.string(),
length: z.number(),
uniqueChars: z.number(),
bitsPerChar: z.number(),
totalBits: z.number(),
});
/**
* Returns the Shannon entropy of `text` in bits per character.
*
* @param text The input string.
* @returns Entropy in bits per character; `0` for an empty string.
*/
function shannonEntropy(text: string): number {
if (text.length === 0) return 0;
const counts = new Map<string, number>();
for (const char of text) {
counts.set(char, (counts.get(char) ?? 0) + 1);
}
let entropy = 0;
for (const count of counts.values()) {
const p = count / text.length;
entropy -= p * Math.log2(p);
}
return entropy;
}
/** Model definition for measuring text entropy. */
export const model = {
type: "@stack72/text-entropy",
version: "2026.05.31.1",
globalArguments: GlobalArgsSchema,
resources: {
result: {
description: "Shannon entropy statistics for the configured text",
schema: ResultSchema,
lifetime: "infinite" as const,
garbageCollection: 10,
},
},
methods: {
analyze: {
description: "Measure the Shannon entropy of the configured text",
arguments: z.object({}),
execute: async (
_args: Record<string, unknown>,
context: {
globalArgs: z.infer<typeof GlobalArgsSchema>;
writeResource: (
spec: string,
name: string,
data: z.infer<typeof ResultSchema>,
) => Promise<{ name: string }>;
},
): Promise<{ dataHandles: { name: string }[] }> => {
const { text } = context.globalArgs;
const bitsPerChar = shannonEntropy(text);
const uniqueChars = new Set(text).size;
const handle = await context.writeResource("result", "result", {
text,
length: text.length,
uniqueChars,
bitsPerChar: Math.round(bitsPerChar * 1000) / 1000,
totalBits: Math.round(bitsPerChar * text.length * 1000) / 1000,
});
return { dataHandles: [handle] };
},
},
},
};Notice the import { z } from "npm:zod@4"; line — this exact pinned, inline
form is the one that works both when swamp bundles the extension and when the
scorer audits it. The closing 2026.05.31.1 version is a placeholder; we will
confirm the real one with the registry shortly.
Confirm swamp loaded it
swamp discovers extension files in extensions/models/ on startup. Confirm the
new type registered:
$ swamp model type search text-entropyswamp opens an interactive type browser. Our new type appears in the list, with its version and methods in the preview pane:
─swamp──────────────────────────────────────────────────── types search ──
> text-entropy 1 / 1
────────────────────────────────────────────────────────────────────────────
@stack72/text-entropy
────────────────────────────────────────────────────────────────────────────
@stack72/text-entropy
version: 2026.05.31.1
Methods:
analyze - Measure the Shannon entropy of the configured text
────────────────────────────────────────────────────────────────────────────
↑/↓ navigate Ctrl-u/d scroll preview Enter select Esc cancelNotice the type matches the type field in our model file. The extension is
loaded. Press Esc to exit the browser.
Run it
Create a definition from the type, passing some text to measure:
$ swamp model create @stack72/text-entropy my-entropy \
--global-arg text="correct horse battery staple"You will see output like:
Created: my-entropy (@stack72/text-entropy)
Path: models/@stack72/text-entropy/....yaml
Version: 2026.05.31.1
Global Arguments:
text (string) *required
Methods:
analyze - Measure the Shannon entropy of the configured text
Data Outputs:
result [resource] - Shannon entropy statistics for the configured text (infinite)Now run the analyze method:
$ swamp model method run my-entropy analyzeYou will see output like:
info model·method·run·my-entropy·analyze Executing method "analyze"
info model·method·run·my-entropy·analyze Data saved to ".swamp/data/..."
── Report: @swamp/method-summary ───────────────────────────────────────────────
# my-entropy (@stack72/text-entropy) → analyze: succeeded
analyze on my-entropy (@stack72/text-entropy) succeeded, producing 1 resource
(result).Read back the stored result:
$ swamp data query \
'modelName == "my-entropy" && specName == "result"' \
--select 'attributes'You will see output like:
┌──────────────────────────────┬────────┬─────────────┬─────────────┬───────────┐
│ text │ length │ uniqueChars │ bitsPerChar │ totalBits │
├──────────────────────────────┼────────┼─────────────┼─────────────┼───────────┤
│ correct horse battery staple │ 28 │ 13 │ 3.495 │ 97.851 │
└──────────────────────────────┴────────┴─────────────┴─────────────┴───────────┘Our model works. The same input always produces these same numbers.
Add a README and a license
Two files raise the extension's quality score and tell consumers how to use it.
Create extensions/models/README.md:
# @stack72/text-entropy
Measures the Shannon entropy of a piece of text. It reports entropy per
character, total entropy, and the number of distinct characters.
## Usage
Create a model definition, passing the text as a global argument:
```bash
swamp model create @stack72/text-entropy my-entropy \
--global-arg text="correct horse battery staple"
```
Run the `analyze` method:
```bash
swamp model method run my-entropy analyze
```
## Output
The `result` resource has `text`, `length`, `uniqueChars`, `bitsPerChar`, and
`totalBits` fields.Now create extensions/models/LICENSE.md with an MIT license:
MIT License
Copyright (c) 2026 stack72
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction...Notice the .md extension on the license file. The publisher only accepts files
ending in .ts, .json, .md, .yaml, .yml, or .txt, so a bare LICENSE
with no extension is rejected — name it LICENSE.md.
Write the manifest
The manifest declares what gets published. Create
extensions/models/manifest.yaml:
manifestVersion: 1
name: "@stack72/text-entropy"
version: "2026.05.31.1"
description: "Measure the Shannon entropy of a piece of text"
repository: "https://github.com/stack72/text-entropy"
models:
- text_entropy.ts
additionalFiles:
- README.md
- LICENSE.md
labels:
- text
- analysisNotice the name begins with our collective — the same one swamp auth whoami
reported. The README and LICENSE entries use bare filenames so the scorer finds
them at the archive root.
Get the version from the registry
Rather than guess the version, ask the registry what comes next:
$ swamp extension version --manifest extensions/models/manifest.yamlYou will see output like:
info extension·version Current published: (none — not yet published)
info extension·version Next version (today): "2026.05.31.1"This extension has never been published, so the next version is today's date
with a .1 suffix. Set both the version field in manifest.yaml and the
version field in text_entropy.ts to the value swamp reported.
Format and lint
The pipeline will not publish unformatted code. Format the extension:
$ swamp extension fmt extensions/models/manifest.yamlYou will see output like:
info extension·fmt Formatted 1 TypeScript files.
info extension·fmt Checked 1 fileConfirm there is nothing left to fix:
$ swamp extension fmt extensions/models/manifest.yaml --checkinfo extension·fmt All quality checks passed.Score the extension
Now score the extension against the quality rubric:
$ swamp extension quality extensions/models/manifest.yamlYou will see output like:
info extension·quality Packaging extension for quality scoring...
info extension·quality Scoring extension against Swamp Club quality rubric...
info extension·quality Rubric v3 — 14/14 points (100%, "all factors earned")
info extension·quality "✓" "has-readme" ["2/2"] — "Has README or module doc"
info extension·quality "✓" "readme-example" ["1/1"] — "README has a code example"
info extension·quality "✓" "rich-readme" ["1/1"] — "README is substantive"
info extension·quality "✓" "symbols-docs" ["1/1"] — "Most symbols documented"
info extension·quality "✓" "fast-check" ["1/1"] — "No slow types"
info extension·quality "✓" "description" ["1/1"] — "Has description"
info extension·quality "✓" "platforms" ["2/2"] — "Platform support declared (or universal)"
info extension·quality "✓" "has-license" ["1/1"] — "License declared"
info extension·quality "✓" "repository-verified" ["2/2"] — "Verified public repository (server confirms on publish)"
info extension·quality "✓" "dependency-trust" ["2/2"] — "Dependencies pass trust gates"
info extension·quality No npm/jsr dependencies to audit
info extension·quality Packaged archive: 3563 bytesEvery factor is earned — 14 out of 14. Notice the dependency-trust line says
there are no dependencies to audit: zod is shared with swamp rather than
bundled, so it is not counted. Notice too that repository-verified is earned
here on a well-formed URL; the registry runs the final public-reachability check
when we publish.
Validate with a dry run
The dry run builds the archive and runs every safety and quality check the real push runs, without uploading anything:
$ swamp extension push extensions/models/manifest.yaml --dry-runYou will see output like:
info extension·push Extension: "@stack72/text-entropy"@"2026.05.31.1"
info extension·push Description: "Measure the Shannon entropy of a piece of text"
info extension·push Repository: "https://github.com/stack72/text-entropy"
info extension·push Models (1):
info extension·push "@stack72/text-entropy" ("extensions/models/text_entropy.ts")
info extension·push Additional files (2):
info extension·push "extensions/models/README.md"
info extension·push "extensions/models/LICENSE.md"
info extension·push Labels: "text, analysis"
info extension·push Dry run complete for "@stack72/text-entropy"@"2026.05.31.1"
info extension·push Archive size: "3.5KB"
info extension·push No API calls were made.Notice the closing line: No API calls were made. Every gate passed and nothing
was uploaded. We are clear to publish.
Publish
Run the push for real:
$ swamp extension push extensions/models/manifest.yamlYou will see output like:
info extension·push Pushed "@stack72/text-entropy"@"2026.05.31.1"
info extension·push Extension ID: "bbc4f071-..."
info extension·push Archive size: "3.5KB"
info extension·push Models: 1, Workflows: 0, Vaults: 0, Bundles: 1Our extension is live on the registry.
Install it somewhere else
Move to a different swamp repository and pull the extension we just published:
$ cd ..
$ mkdir use-entropy && cd use-entropy
$ swamp repo init
$ swamp extension pull @stack72/text-entropyYou will see output like:
info extension·pull Pulling "@stack72/text-entropy"@"2026.05.31.1"
info extension·pull Identity verified: "@stack72/text-entropy"@"2026.05.31.1"
info extension·pull Repository: "https://github.com/stack72/text-entropy"
info extension·pull Pulled "@stack72/text-entropy"@"2026.05.31.1"Confirm the type is available in this fresh repo:
$ swamp model type search text-entropyThe interactive browser shows @stack72/text-entropy. The extension we wrote,
scored, and published is now installed in a repository that never saw its
source.
We have built a TypeScript model, run it, scored it 14/14, taken it through every publishing gate, pushed it to the registry, and installed it somewhere new. To retire it later, see Deprecate an Extension. To understand why the pipeline is built as a sequence of gates, see About the Extension Publishing Lifecycle.