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 @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 login

Confirm nobody has built it already

Before writing anything, we check the registry. There is no point publishing something that already exists.

$ swamp extension search entropy

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

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

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

stack72 ([email protected]) on https://swamp-club.com
Collectives: swamp, system-initiative, stack72

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

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

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

You will see output like:

info    extension·fmt        Formatted 1 TypeScript files.
info    extension·fmt        Checked 1 file

Confirm there is nothing left to fix:

$ swamp extension fmt extensions/models/manifest.yaml --check
info    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:

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

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

You 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: 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 @stack72/text-entropy

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

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