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 @my-collective/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.
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 loginCheck the registry first
Before writing anything, we search the registry for existing extensions.
$ swamp extension search entropySwamp opens an interactive registry browser. Scan the results — if any
extensions appear, read their descriptions and check whether they measure
Shannon entropy the way we need. None of them do what we are about to build, so
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:
... INF repo·init Initialized swamp repository at "..." (tools: "claude")
... INF repo·init What's next:
... INF repo·init → Start Claude Code and run /swamp-getting-started
... INF 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:
your-username (you@example.com) on https://swamp-club.com
Collectives: my-collectiveNotice your username in the Collectives list — the extension we publish must
be named @<that-name>/....
Tip
This tutorial uses @my-collective as a placeholder. Replace it with your own
collective name from the output above in every command, code block, and
manifest field throughout the tutorial.
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: "@my-collective/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
────────────────────────────────────────────────────────────────────────────
@my-collective/text-entropy
────────────────────────────────────────────────────────────────────────────
@my-collective/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 @my-collective/text-entropy my-entropy \
--global-arg text="correct horse battery staple"You will see output like:
Created: my-entropy (@my-collective/text-entropy)
Path: models/@my-collective/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:
... INF model·method·run·my-entropy·analyze Found model "my-entropy" ("@my-collective/text-entropy")
... INF model·method·run·my-entropy·analyze Evaluating expressions
... INF model·method·run·my-entropy·analyze Executing method "analyze"
... INF model·method·run·my-entropy·analyze Data saved to ".swamp/data/..."
... INF model·method·run·my-entropy·analyze Running report: "@swamp/method-summary"
── Report: @swamp/method-summary ───────────────────────────────────────────────
# my-entropy (@my-collective/text-entropy) → analyze: succeeded
analyze on my-entropy (@my-collective/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:
# @my-collective/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 @my-collective/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 your-name
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: "@my-collective/text-entropy"
version: "2026.05.31.1"
description: "Measure the Shannon entropy of a piece of text"
repository: "https://github.com/my-collective/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:
... INF extension·version Current published: (none — not yet published)
... INF 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:
... INF extension·fmt Formatted 1 TypeScript files.
... INF extension·fmt "Checked 1 file"Confirm there is nothing left to fix:
$ swamp extension fmt extensions/models/manifest.yaml --check... INF 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:
... INF extension·quality Packaging extension for quality scoring...
... INF extension·quality Scoring extension against Swamp Club quality rubric...
... INF extension·quality Rubric v3 — 14/14 points (100%, "all factors earned")
... INF extension·quality "✓" "has-readme" ["2/2"] — "Has README or module doc"
... INF extension·quality "✓" "readme-example" ["1/1"] — "README has a code example"
... INF extension·quality "✓" "rich-readme" ["1/1"] — "README is substantive"
... INF extension·quality "✓" "symbols-docs" ["1/1"] — "Most symbols documented"
... INF extension·quality "✓" "fast-check" ["1/1"] — "No slow types (deprecated)"
... INF extension·quality ℹ This factor is deprecated and will be removed in a future release.
... INF extension·quality "✓" "description" ["1/1"] — "Has description"
... INF extension·quality "✓" "platforms" ["2/2"] — "Platform support declared (or universal)"
... INF extension·quality "✓" "has-license" ["1/1"] — "License declared"
... INF extension·quality "✓" "repository-verified" ["2/2"] — "Verified public repository (server confirms on publish)"
... INF extension·quality "✓" "dependency-trust" ["2/2"] — "Dependencies pass trust gates"
... INF extension·quality No npm/jsr dependencies to audit
... INF extension·quality Note: `repository-verified` earns here when the URL is well-formed ...
... INF extension·quality Packaged archive: 2638 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:
... INF extension·push Extension: "@my-collective/text-entropy"@"2026.05.31.1"
... INF extension·push Description: "Measure the Shannon entropy of a piece of text"
... INF extension·push Repository: "https://github.com/my-collective/text-entropy"
... INF extension·push Models (1):
... INF extension·push "@my-collective/text-entropy" ("extensions/models/text_entropy.ts")
... INF extension·push Global Arguments:
... INF extension·push "text": "string"
... INF extension·push Additional files (2):
... INF extension·push "extensions/models/README.md"
... INF extension·push "extensions/models/LICENSE.md"
... INF extension·push Labels: "text, analysis"
... WRN extension·push Extension review warnings:
... WRN extension·push ["medium"] "Testing Completeness" — ".../text_entropy.ts": "No sibling `_test.ts` found ..."
... WRN extension·push ["medium"] "Adversarial review" — "...": "No adversarial review recorded for the current code ..."
... INF extension·push Dry run complete for "@my-collective/text-entropy"@"2026.05.31.1"
... INF extension·push Archive size: "2.6KB"
... INF extension·push No API calls were made.The dry run lists two review warnings. These are expected at this stage of the tutorial — we have not written tests or performed an adversarial review yet. Both are medium-severity and do not block publishing. In a real extension, you would address them before the final push; the Create and Publish how-to covers this gate in full.
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:
... INF extension·push Pushed "@my-collective/text-entropy"@"2026.05.31.1"
... INF extension·push Extension ID: "bbc4f071-..."
... INF extension·push Archive size: "2.6KB"
... INF 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 @my-collective/text-entropyYou will see output like:
... INF extension·pull Pulling "@my-collective/text-entropy"@"2026.05.31.1"
... INF extension·pull Identity verified: "@my-collective/text-entropy"@"2026.05.31.1"
... INF extension·pull Repository: "https://github.com/my-collective/text-entropy"
... INF extension·pull Pulled "@my-collective/text-entropy"@"2026.05.31.1"Confirm the type is available in this fresh repo:
$ swamp model type search text-entropyThe interactive browser shows @my-collective/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.