@andocorp/buzz-import
v0.3.0
Published
Move a Buzz community's history into an Ando workspace.
Keywords
Readme
ando-buzz-import
Copies a Buzz community's chat history into an Ando workspace.
It runs in two steps. export reads the Buzz relay and writes a JSONL snapshot of raw signed Nostr events. import turns that snapshot into Ando import records and uploads them. migrate does both. You can run the two steps on different machines, and you can build the snapshot from a Postgres dump instead of the relay.
What moves
| Buzz | Ando |
| --- | --- |
| Channels (kind 39000 metadata, 39002 roster) | Channels with name, description, public/private, archived state and members |
| DMs and group DMs (39000 with hidden / t=dm) | Direct messages |
| Messages: kind 9, 40002, forum posts 45001, diffs 40008 | Messages. A diff is wrapped in a diff code block |
| Replies: NIP-10 e tags, forum comments 45003 | Thread replies. Ando threads are one level deep, so nested replies attach to the top-level message |
| Edits (40003) | The latest edit's text, with edited_at set |
| Reactions (kind 7) | Reactions. + becomes 👍 and - becomes 👎 |
| Profiles (kind 0) | Members: display name and avatar URL |
| Agents: roster role bot, kind 10100, NIP-OA auth tag, or "bot": true in kind 0 | Members of kind bot |
| Archived identities (kind 13535) | Deactivated members |
| nostr:npub… / nostr:nprofile… mentions | @Display Name text |
| Attachments (NIP-92 imeta) | Files on the message. Files on the Buzz relay are downloaded with your Buzz key (Blossom get auth), checked against their sha256, and uploaded to Ando |
Channels come from the relay's channel list plus your own membership rosters, so every channel you belong to is included even on large relays.
What does not move
- Files that can't be copied stay as links. A file larger than
--max-file-mb(default 100), one the relay no longer has, one that fails its checksum, or every file when you pass--skip-files, stays a📎 [filename](url)line, as before. The summary lists each underfiles.failedwith a reason. Files on the Buzz relay needBUZZ_PRIVATE_KEY, including forimport --from. Links to any other host are fetched without credentials and only from public addresses (checked on every redirect), so a message can't make the importer read a service on your network. - Custom emoji become shortcodes. A
:partyparrot:reaction is sent aspartyparrot. Ando skips shortcodes it doesn't support and lists them underskippedin the summary. Custom emoji inside message text stay as:shortcode:text. - Deleted messages and reactions. Anything removed by kind 5 or 9005 is dropped.
- Private channels and DMs you are not in. The relay only returns what the exporting key can read. To include every private channel and DM, use the operator dump below.
- Other Buzz data: forum votes, pins, bookmarks, canvases, huddles, workflows, system messages (40099), user status and read state. The summary's
dropped.unknown_kindscounts the skipped kinds. - Past channel membership. Rosters are each channel's current 39002 snapshot. A channel with no roster gets the people who posted in it.
- Email addresses. Buzz has none. Members are created with
email: null. NIP-05 identifiers are not used as emails because they are not verified addresses.
Members with no kind 0 profile are still imported. Their display name is the first 12 characters of their npub (npub1 plus 7 more).
Prerequisites
- Node.js 22 or later.
- A Buzz key that belongs to a member of the community, set in
BUZZ_PRIVATE_KEYas hex ornsec1…. An agent key that relies on its owner's membership also needsBUZZ_AUTH_TAG, the same NIP-OA tag JSON the Buzz CLI uses. It is sent as thex-auth-tagheader. - An Ando API key with the Import history permission (
imports:writeandfiles:write), created by a workspace owner or admin, set inANDO_API_KEY. Without the role orimports:write, Ando returns 403 and the tool stops; withoutfiles:write, files stay as links.ANDO_API_URLdefaults tohttps://api.ando.so.
Never pass keys as command-line arguments. The CLI refuses anything that looks like an nsec.
Install
npx @andocorp/buzz-import <command> # run without installing
npm install -g @andocorp/buzz-import # or install the ando-buzz-import commandCommands
# 1. Snapshot the relay (wss:// or https://; ws:// and http:// only for localhost)
BUZZ_PRIVATE_KEY=nsec1... ando-buzz-import export --relay wss://buzz.example.com --out buzz.jsonl
# 2. Check what will be imported. This makes no calls to Ando.
ando-buzz-import import --from buzz.jsonl --dry-run
# 3. Import
ANDO_API_KEY=... ando-buzz-import import --from buzz.jsonl --map-self --self npub1...
# Or run steps 1 and 3 together (the snapshot goes to a temp file unless --out is given)
BUZZ_PRIVATE_KEY=... ANDO_API_KEY=... ando-buzz-import migrate --relay wss://buzz.example.com --map-selfProgress lines go to stderr. The final JSON summary goes to stdout: counts, dropped items, records written, and every record Ando skipped with its reason.
Mapping people onto existing Ando members
By default, every Buzz identity becomes an imported placeholder member. To attach an identity to someone who already has an Ando account:
--map <npub|hex>=<ando_member_id>maps one identity. Repeat the flag for more people.--map-selfmaps your own Buzz identity to the Ando member who owns the API key.migrategets your identity fromBUZZ_PRIVATE_KEY.importneeds it passed as--self <npub|hex>.
What people see in Ando
- Imported history notifies no one and adds no unread badges. Messages keep their Buzz time, author and edit time.
- A placeholder member shows the person's Buzz name and avatar on their messages. It is not an invitation and gets no email. Deactivated Buzz members stay archived.
- An imported channel never takes over an existing channel's name. If
#generalalready exists, Buzz's arrives as#general-buzz. - Every Buzz DM and group DM becomes its own imported conversation, even when the same people already have an Ando DM.
- Running the import again also brings over renames, archiving and visibility changes made in Buzz since the last run.
Other options
--source-workspace-id <host>: the Buzz relay host recorded on the Ando import. It defaults to the host in the snapshot, so you only need it for operator dumps.--source-workspace-name <name>: defaults to the relay's NIP-11 name.
Bring over your own agents
Any workspace member, not only an owner or admin, can bring the Buzz agents they own into Ando:
BUZZ_PRIVATE_KEY=nsec1... ANDO_API_KEY=... ando-buzz-import agents --relay wss://buzz.example.com --dry-run
BUZZ_PRIVATE_KEY=nsec1... ANDO_API_KEY=... ando-buzz-import agents --relay wss://buzz.example.com- It reads your own agent records (kind 30177) and the definitions they link to (kind 30175), so each agent keeps its name, instructions and model. A model Ando doesn't offer falls back to Ando's default. An agent set to answer only you answers only you in Ando.
- Each agent arrives as an Ando agent you created. No messages, channels or other people are written, which is why it doesn't need an owner or admin. It needs an Ando API key with Manage agents (
agents:write). - Ando checks ownership itself: each agent must have signed an event carrying its NIP-OA owner tag naming you, and the request carries a fresh proof signed by your Buzz key for that API key. An agent that never named you, or one another member already brought over, is skipped with a reason.
- Running it again returns the agents you already brought over instead of making copies. If an owner or admin later imports the community's history, those agents' Buzz messages are attributed to them.
- Not moved: avatars, the runtime or harness the agent ran on in Buzz (Goose, Claude Code), its secrets, and its memory.
Operator dump (all channels and DMs)
A relay operator can build the snapshot straight from Buzz's Postgres. Every private channel and DM is included, whoever runs it. Each output line is one event in the same shape export writes.
COMMUNITY=$(psql "$BUZZ_DATABASE_URL" -At -c \
"SELECT id FROM communities WHERE host = 'buzz.example.com' AND deleted_at IS NULL")
psql "$BUZZ_DATABASE_URL" -At -c "
SELECT json_build_object(
'id', encode(id, 'hex'),
'pubkey', encode(pubkey, 'hex'),
'created_at', extract(epoch from created_at)::bigint,
'kind', kind,
'tags', tags,
'content', content,
'sig', encode(sig, 'hex'))
FROM events
WHERE deleted_at IS NULL
AND community_id = '$COMMUNITY'
AND kind IN (0, 5, 7, 9, 9005, 10100, 13535, 39000, 39002, 40002, 40003, 40008, 45001, 45003)
" > buzz.jsonl
ando-buzz-import import --from buzz.jsonl --source-workspace-id buzz.example.com --dry-runThe column names and types come from schema/schema.sql in the Buzz repo (events.id, pubkey and sig are BYTEA, created_at is TIMESTAMPTZ, tags is JSONB). The kind list only keeps the file small; without it, the importer ignores the other kinds anyway.
Use psql -At -c "SELECT …", not COPY (SELECT …) TO STDOUT. COPY's default text format escapes backslashes, so JSON like "a\nb" comes out as "a\\nb", and every newline and quote in a message would turn into literal backslash sequences. If you must use COPY, use COPY (…) TO STDOUT WITH (FORMAT csv, QUOTE E'\x01', DELIMITER E'\x02'), which writes each row unescaped.
Safe to rerun
- Ando identifies each record by its Buzz id: pubkey hex for members, channel UUID for conversations, event id hex for messages. Rerunning
importormigrateupdates records instead of duplicating them. Starting again against the same relay host reuses the existing Ando import, and reopens it if it was completed. - If an upload fails partway through, run the same command again. When
migratewrote its snapshot to a temp file, it keeps that file on failure and prints animport --fromcommand, so the retry skips the export. Everything is resent in dependency order: members, then conversations, then messages oldest first (a thread root always comes before its replies), then reactions. - Files are keyed by their sha256. A rerun asks Ando which files this import already uploaded and only downloads and uploads the rest. A message is never stripped of files an earlier run attached.
exportwrites to<out>.partialand renames the file only when it finishes. An interrupted export never leaves a file that looks complete.- Relay requests are retried on 429 (using the relay's
retry in Nshint orRetry-After) and on 5xx. Ando requests are retried on 429 (usingRetry-After) and on 5xx.
Limits
- The whole snapshot is held in memory during
import, so memory use grows with the size of the community. - Messages longer than 40,000 characters are cut off, with a note at the end.
- Files up to 100 MB by default (
--max-file-mb), at most 10 per message. Each file is held in memory while it uploads. - Ando's per-request limits: 100 members, 10 conversations with at most 250 member ids in total, 100 messages, 200 reactions. A roster larger than 250 is sent as repeated slices of the same conversation.
Development
pnpm --filter @andocorp/buzz-import test # node --experimental-strip-types --test src/*.test.ts
pnpm --filter @andocorp/buzz-import typecheck
pnpm --filter @andocorp/buzz-import pack:npm # build, pack, and run the CLI from a clean npm installReleasing
Merging a version change to main publishes it. .github/workflows/publish-buzz-import.yml skips versions already on npm, runs the tests, typecheck and pack:npm, then publishes that exact tarball through npm trusted publishing (no stored token; npm records provenance). Versions with a prerelease suffix (0.2.0-next.0) go to the next tag instead of latest.
The workflow only works once @andocorp/buzz-import exists on npm with a trusted publisher of Ando-Corporation/ando, workflow publish-buzz-import.yml, environment npm-buzz-import. The first version therefore goes out by hand, from a clean checkout of main, by someone with publish rights on the @andocorp org:
pnpm --filter @andocorp/buzz-import pack:npm
npm publish artifacts/buzz-import/npm/andocorp-buzz-import-0.1.0.tgz --access publicSettings > Buzz import is shown to every workspace admin and tells them to run npx @andocorp/buzz-import, so publish the version you need to npm before relying on it.
