Skip to main content

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 login

Check the registry first

Before writing anything, we search the registry for existing extensions.

$ swamp extension search entropy

Swamp 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 init

You 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/manual

Notice 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 whoami

You will see output like:

your-username (you@example.com) on https://swamp-club.com
Collectives: my-collective

Notice 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-entropy

Swamp 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 cancel

Notice 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 analyze

You 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
  - analysis

Notice 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.yaml

You 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.yaml

You 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.yaml

You 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 bytes

Every 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-run

You 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.yaml

You 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: 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-entropy

You 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-entropy

The 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.