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 12/12 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:
Resolved my-entropy (@my-collective/text-entropy)
Executing analyze
Completed analyze on my-entropy succeeded in 52ms
┌──────────────────────────────────────────────┐
│ Arguments │
│ input The quick brown fox jumps over … │
│ │
│ Data produced │
│ result │
│ entropy 4.056 │
│ length 43 │
│ unique 31 │
│ │
└──────────────────────────────────────────────┘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 license file ends in .md. The publisher accepts files ending in
.ts, .json, .md, .yaml, .yml, or .txt, plus a few extensionless
legal files such as a bare LICENSE.
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"
license: MIT
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.
Notice license: MIT names the license that LICENSE.md carries. Each earns a
point on the quality rubric.
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"
... [INF] extension·fmt: "Checked 1 file"Notice that fmt uses the project's deno.json when there is one, the same
configuration the push checks against.
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 — 12/12 points (100% (+2 pending server verification), 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" ["0/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: For next time:
... [INF] extension·quality: testing-completeness — text_entropy.ts: No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.
... [INF] extension·quality: fix: Add a sibling <name>_test.ts covering the success and failure paths.
... [INF] extension·quality: or accept it, at the top of text_entropy.ts:
... [INF] extension·quality: // swamp-quality-ignore testing-completeness: <reason>
... [INF] extension·quality: Packaged archive: 2027 bytesEvery locally verifiable factor is earned — 12 out of 12. 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. The repository-verified
factor shows a ~ marker and earns 0 points locally because the CLI cannot
confirm that the URL resolves to a public repository — only the registry server
can do that. Its 2 points are excluded from the local denominator so they do not
penalise you, and they appear as (+2 pending server verification) in the
summary. The server runs the final public-reachability check when you publish.
The For next time block names the one warning still open, a missing test file;
we will meet it again in the dry run.
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.10.07.1"
... [INF] extension·push: Requested visibility: default (registry decides)
... [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" — "extensions/models/text_entropy.ts": "No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing."
... [WRN] extension·push: ["medium"] "Adversarial review" — "/tmp/swamp-extension-review/_my-collective_text-entropy-bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c.json": "No adversarial review recorded for the current code — perform the review and write the report here (run with --dry-run --json for the fill-in skeleton, or see the swamp skill)."
... [WRN] extension·push: Version drift warnings (non-blocking):
... [WRN] extension·push: unable to check for version drift — no previously published version found in the registry (this is expected on first publish)
... [INF] extension·push: Dry run complete for "@my-collective/text-entropy@2026.10.07.1"
... [INF] extension·push: Archive size: "2.0KB"
... [INF] extension·push: Content hash: "bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c"
... [INF] extension·push: Registry checks:
... [INF] extension·push: authentication: passed — Signed in as my-collective.
... [INF] extension·push: reserved collective: passed — Collective "@my-collective" is not reserved.
... [INF] extension·push: collective membership: passed — Collective "@my-collective" is one of your collectives.
... [INF] extension·push: version exists: passed — Version 2026.10.07.1 is not published for @my-collective/text-entropy.
... [INF] extension·push: API calls made (3):
... [INF] extension·push: registry: GET https://swamp-club.com/api/whoami ok (200)
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40my-collective%2Ftext-entropy/versions?channel=stable&channel=rc&channel=beta&perPage=100&page=1 not-found (404)
... [INF] extension·push: registry: GET https://swamp-club.com/api/v1/extensions/%40my-collective%2Ftext-entropy/latest not-found (404)
... [INF] extension·push: For next time:
... [INF] extension·push: testing-completeness — text_entropy.ts: No sibling `_test.ts` found — cover both success and failure paths with unit tests before publishing.
... [INF] extension·push: fix: Add a sibling <name>_test.ts covering the success and failure paths.
... [INF] extension·push: or accept it, at the top of text_entropy.ts:
... [INF] extension·push: // swamp-quality-ignore testing-completeness: <reason>
... [INF] extension·push: adversarial-review-report — /tmp/swamp-extension-review/_my-collective_text-entropy-bf925572727082765765218abc0b4226c2233d2bd0884c6541ee610af83e529c.json: No adversarial review recorded for the current code — perform the review and write the report here (run with --dry-run --json for the fill-in skeleton, or see the swamp skill).
... [INF] extension·push: fix: Run the adversarial review and record its report at the path the finding names.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, and the For next time
block at the end repeats them with what would fix each one. 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 Registry checks block. The dry run asked the registry whether we
are signed in, whether @my-collective is ours, and whether this version is
already published, and every check passed. The API calls made lines are those
three lookups; 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: Channel: "stable"
... [INF] extension·push: Visibility: "public"
... [INF] extension·push: Extension ID: "bbc4f071-..."
... [INF] extension·push: Archive size: "2.0KB"
... [INF] extension·push: "Models: 1, Workflows: 0, Vaults: 0, Bundles: 1"Our 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 12/12, 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.