refactor: rename sprout backend to buzz (#958)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@squareup.com>
Signed-off-by: Will Pfleger <wpfleger96@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Will Pfleger <wpfleger96@gmail.com>
This commit is contained in:
Will Pfleger
2026-06-10 19:29:51 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 Will Pfleger
parent 53e3f09485
commit d99ad131f1
306 changed files with 3249 additions and 3373 deletions
+2 -1
View File
@@ -1 +1,2 @@
* @block/sprout-oss-team
# TODO: verify @block/buzz-oss-team exists in GitHub org
* @block/buzz-oss-team
+28 -28
View File
@@ -262,37 +262,37 @@ jobs:
docker logs "${container}" || true
return 1
}
wait_healthy "Postgres" "sprout-postgres"
wait_healthy "Redis" "sprout-redis"
wait_healthy "Typesense" "sprout-typesense"
wait_healthy "MinIO" "sprout-minio"
wait_healthy "Postgres" "buzz-postgres"
wait_healthy "Redis" "buzz-redis"
wait_healthy "Typesense" "buzz-typesense"
wait_healthy "MinIO" "buzz-minio"
- name: Apply database schema
run: ./bin/pgschema apply --file schema/schema.sql --auto-approve
env:
PGHOST: localhost
PGPORT: "5432"
PGUSER: sprout
PGPASSWORD: sprout_dev
PGDATABASE: sprout
PGUSER: buzz
PGPASSWORD: buzz_dev
PGDATABASE: buzz
- name: Build relay
run: cargo build --profile ci -p sprout-relay
run: cargo build --profile ci -p buzz-relay
- name: Start relay
run: |
nohup env \
DATABASE_URL=postgres://sprout:sprout_dev@localhost:5432/sprout \
DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz \
REDIS_URL=redis://localhost:6379 \
TYPESENSE_URL=http://localhost:8108 \
TYPESENSE_API_KEY=sprout_dev_key \
TYPESENSE_API_KEY=buzz_dev_key \
RELAY_URL=ws://localhost:3000 \
SPROUT_BIND_ADDR=0.0.0.0:3000 \
SPROUT_REQUIRE_AUTH_TOKEN=false \
SPROUT_RECONCILE_CHANNELS=true \
SPROUT_GIT_PROBE_WRITERS=8 \
./target/ci/sprout-relay > /tmp/sprout-relay.log 2>&1 &
echo $! > /tmp/sprout-relay.pid
BUZZ_BIND_ADDR=0.0.0.0:3000 \
BUZZ_REQUIRE_AUTH_TOKEN=false \
BUZZ_RECONCILE_CHANNELS=true \
BUZZ_GIT_PROBE_WRITERS=8 \
./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 &
echo $! > /tmp/buzz-relay.pid
for attempt in $(seq 1 60); do
if ! kill -0 "$(cat /tmp/sprout-relay.pid)" 2>/dev/null; then
cat /tmp/sprout-relay.log
if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then
cat /tmp/buzz-relay.log
exit 1
fi
status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true)
@@ -301,7 +301,7 @@ jobs:
fi
sleep 1
done
cat /tmp/sprout-relay.log
cat /tmp/buzz-relay.log
exit 1
- name: Seed desktop e2e data
run: bash scripts/setup-desktop-test-data.sh
@@ -315,7 +315,7 @@ jobs:
path: |
desktop/playwright-report
desktop/test-results
/tmp/sprout-relay.log
/tmp/buzz-relay.log
if-no-files-found: ignore
- name: Save pnpm store cache
if: github.event_name == 'push'
@@ -444,10 +444,10 @@ jobs:
TARGET: ${{ matrix.target }}
run: |
cross build --release --target "$TARGET" \
-p sprout-relay \
-p sprout-acp \
-p sprout-agent \
-p sprout-dev-mcp \
-p buzz-relay \
-p buzz-acp \
-p buzz-agent \
-p buzz-dev-mcp \
-p git-credential-nostr \
-p git-sign-nostr
@@ -472,11 +472,11 @@ jobs:
run: |
TARGET=$(rustc -vV | sed -n 's|host: ||p')
mkdir -p desktop/src-tauri/binaries
touch "desktop/src-tauri/binaries/sprout-acp-$TARGET"
touch "desktop/src-tauri/binaries/sprout-agent-$TARGET"
touch "desktop/src-tauri/binaries/sprout-dev-mcp-$TARGET"
touch "desktop/src-tauri/binaries/buzz-acp-$TARGET"
touch "desktop/src-tauri/binaries/buzz-agent-$TARGET"
touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET"
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
touch "desktop/src-tauri/binaries/sprout-$TARGET"
touch "desktop/src-tauri/binaries/buzz-$TARGET"
# Mesh rev is derived from Cargo.lock so a dependency bump needs no
# lockstep edit here; the cache key tracks it automatically.
- name: Resolve mesh-llm rev
+16 -16
View File
@@ -67,12 +67,12 @@ jobs:
- name: Generate release config
run: cd desktop && node scripts/build-release-config.mjs
env:
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/buzz-desktop-latest/latest.json
- name: Build sidecars
run: |
cargo build --release -p sprout-acp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli
cargo build --release -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh
# Mesh rev derived from Cargo.lock (no lockstep edit on dep bump); cache key tracks it.
@@ -118,8 +118,8 @@ jobs:
- name: Build unsigned Tauri app
run: cd desktop && pnpm tauri build --verbose --no-sign --config src-tauri/tauri.release.conf.json
env:
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/buzz-desktop-latest/latest.json
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
@@ -148,7 +148,7 @@ jobs:
codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }}
unsigned-artifact-path: ${{ steps.unsigned.outputs.dmg }}
entitlements-plist-path: desktop/src-tauri/Entitlements.plist
artifact-name: sprout-${{ github.sha }}-${{ github.run_id }}-arm64
artifact-name: buzz-${{ github.sha }}-${{ github.run_id }}-arm64
- name: Replace DMG and rebuild updater archive
env:
@@ -214,7 +214,7 @@ jobs:
bash desktop/scripts/generate-oss-latest-json.sh \
"$VERSION" \
"$SIG_PATH" \
"https://github.com/block/sprout/releases/download/sprout-desktop-latest/$ARCHIVE_NAME" \
"https://github.com/block/sprout/releases/download/buzz-desktop-latest/$ARCHIVE_NAME" \
> latest.json
cat latest.json
env:
@@ -244,12 +244,12 @@ jobs:
- name: Update rolling release for auto-updater
run: |
gh release create sprout-desktop-latest \
gh release create buzz-desktop-latest \
--prerelease \
--title "Sprout Desktop Auto-Update" \
--notes "Rolling release for the Tauri auto-updater. Do not download manually — use the versioned release instead." \
2>/dev/null || true
gh release upload sprout-desktop-latest \
gh release upload buzz-desktop-latest \
latest.json \
"$ARCHIVE_PATH" \
"$SIG_PATH" \
@@ -292,19 +292,19 @@ jobs:
- name: Generate release config
run: cd desktop && node scripts/build-release-config.mjs
env:
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/buzz-desktop-latest/latest.json
- name: Build sidecars
run: |
cargo build --release --target "$TARGET" -p sprout-acp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli
cargo build --release --target "$TARGET" -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh "$TARGET"
- name: Build unsigned Tauri app
run: cd desktop && pnpm tauri build --verbose --no-sign --target "$TARGET" --config src-tauri/tauri.release.conf.json
env:
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/buzz-desktop-latest/latest.json
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
@@ -330,7 +330,7 @@ jobs:
codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }}
unsigned-artifact-path: ${{ steps.unsigned.outputs.dmg }}
entitlements-plist-path: desktop/src-tauri/Entitlements.plist
artifact-name: sprout-${{ github.sha }}-${{ github.run_id }}-x64
artifact-name: buzz-${{ github.sha }}-${{ github.run_id }}-x64
- name: Replace DMG and signed .app
env:
@@ -422,7 +422,7 @@ jobs:
- name: Build sidecars
run: |
cargo build --release -p sprout-acp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli
cargo build --release -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh
- name: Build Linux Tauri app
+5 -5
View File
@@ -1,9 +1,9 @@
name: Sprig
# Builds and publishes Sprig — one deploy-anywhere Linux multicall binary for:
# sprout-acp ACP harness that bridges Sprout events to the LLM agent
# sprout-agent ACP-compliant agent (spawns MCP, calls LLMs)
# sprout-dev-mcp Developer MCP server (multicall: rg, tree, sprout,
# buzz-acp ACP harness that bridges Sprout events to the LLM agent
# buzz-agent ACP-compliant agent (spawns MCP, calls LLMs)
# buzz-dev-mcp Developer MCP server (multicall: rg, tree, buzz,
# git-credential-nostr, git-sign-nostr)
#
# Targets: x86_64-unknown-linux-musl and aarch64-unknown-linux-musl (static
@@ -137,7 +137,7 @@ jobs:
set -euo pipefail
TAG="sprig-latest"
TITLE="Sprig (rolling)"
NOTES="Rolling Linux build of Sprig (all-in-one sprout-acp + sprout-agent + sprout-dev-mcp), tracking \`main\` (\`${SHA}\`)."
NOTES="Rolling Linux build of Sprig (all-in-one buzz-acp + buzz-agent + buzz-dev-mcp), tracking \`main\` (\`${SHA}\`)."
if gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1; then
gh api -X PATCH "repos/${REPO}/git/refs/tags/${TAG}" \
@@ -197,5 +197,5 @@ jobs:
set -euo pipefail
gh release create "$TAG" \
--title "Sprig v${VERSION}" \
--notes "Sprig v${VERSION} — Linux all-in-one builds of sprout-acp + sprout-agent + sprout-dev-mcp." \
--notes "Sprig v${VERSION} — Linux all-in-one builds of buzz-acp + buzz-agent + buzz-dev-mcp." \
dist/*
Generated
+464 -464
View File
@@ -701,6 +701,466 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "buzz-acp"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"buzz-core",
"buzz-persona",
"buzz-sdk",
"chrono",
"clap",
"evalexpr",
"futures-util",
"hex",
"nix 0.31.3",
"nostr",
"reqwest 0.13.3",
"rustls",
"serde",
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tokio-util",
"toml 1.1.2+spec-1.1.0",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "buzz-admin"
version = "0.1.0"
dependencies = [
"anyhow",
"buzz-auth",
"buzz-core",
"buzz-db",
"clap",
"hex",
"nostr",
"serde_json",
"tokio",
]
[[package]]
name = "buzz-agent"
version = "0.1.0"
dependencies = [
"arc-swap",
"async-trait",
"axum",
"base64",
"getrandom 0.4.2",
"hex",
"nix 0.31.3",
"reqwest 0.13.3",
"rmcp",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.11.0",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"urlencoding",
"webbrowser",
]
[[package]]
name = "buzz-audit"
version = "0.1.0"
dependencies = [
"buzz-core",
"chrono",
"futures-util",
"hex",
"serde",
"serde_json",
"sha2 0.11.0",
"sqlx",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "buzz-auth"
version = "0.1.0"
dependencies = [
"buzz-core",
"hex",
"nostr",
"rand 0.10.1",
"serde",
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.18",
"tokio",
"tracing",
"url",
"uuid",
]
[[package]]
name = "buzz-cli"
version = "0.1.0"
dependencies = [
"base64",
"buzz-core",
"buzz-persona",
"buzz-sdk",
"buzz-ws-client",
"clap",
"diffy",
"hex",
"infer",
"nostr",
"reqwest 0.13.3",
"serde",
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.18",
"tokio",
"url",
"uuid",
]
[[package]]
name = "buzz-core"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"hmac 0.13.0",
"nostr",
"percent-encoding",
"rand 0.10.1",
"serde",
"serde_json",
"sha2 0.11.0",
"subtle",
"thiserror 2.0.18",
"url",
"uuid",
"zeroize",
]
[[package]]
name = "buzz-db"
version = "0.1.0"
dependencies = [
"buzz-core",
"chrono",
"hex",
"nostr",
"serde",
"serde_json",
"sha2 0.11.0",
"sqlx",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "buzz-dev-mcp"
version = "0.1.0"
dependencies = [
"base64",
"buzz-cli",
"git-credential-nostr",
"git-sign-nostr",
"ignore",
"image",
"nix 0.31.3",
"nostr",
"reqwest 0.13.3",
"rmcp",
"schemars",
"serde",
"serde_json",
"similar",
"tempfile",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"zeroize",
]
[[package]]
name = "buzz-media"
version = "0.1.0"
dependencies = [
"axum",
"blurhash",
"buzz-core",
"bytes",
"chrono",
"futures-core",
"futures-util",
"hex",
"image",
"imagesize",
"infer",
"mp4",
"nostr",
"rust-s3",
"serde",
"serde_json",
"sha2 0.11.0",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "buzz-pair-relay"
version = "0.1.0"
dependencies = [
"futures-util",
"http-body-util",
"hyper",
"hyper-util",
"parking_lot",
"secp256k1 0.31.1",
"serde_json",
"sha2 0.11.0",
"tokio",
"tokio-tungstenite 0.29.0",
"tokio-util",
]
[[package]]
name = "buzz-pairing-cli"
version = "0.1.0"
dependencies = [
"buzz-core",
"clap",
"futures-util",
"hex",
"nostr",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"url",
"zeroize",
]
[[package]]
name = "buzz-persona"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror 2.0.18",
]
[[package]]
name = "buzz-proxy"
version = "0.1.0"
dependencies = [
"axum",
"buzz-core",
"chrono",
"dashmap",
"futures-util",
"hex",
"hmac 0.13.0",
"moka",
"nostr",
"rand 0.10.1",
"reqwest 0.13.3",
"serde",
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "buzz-pubsub"
version = "0.1.0"
dependencies = [
"buzz-auth",
"buzz-core",
"chrono",
"deadpool-redis",
"futures-util",
"nostr",
"redis",
"serde",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "buzz-relay"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"base64",
"buzz-audit",
"buzz-auth",
"buzz-core",
"buzz-db",
"buzz-media",
"buzz-pubsub",
"buzz-sdk",
"buzz-search",
"buzz-workflow",
"bytes",
"chrono",
"dashmap",
"deadpool-redis",
"futures-util",
"hex",
"hmac 0.13.0",
"infer",
"mesh-llm-host-runtime",
"mesh-llm-sdk",
"metrics",
"metrics-exporter-prometheus",
"moka",
"nostr",
"rand 0.10.1",
"redis",
"reqwest 0.13.3",
"rust-s3",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.11.0",
"sqlx",
"subtle",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "buzz-sdk"
version = "0.1.0"
dependencies = [
"buzz-core",
"nostr",
"serde",
"serde_json",
"thiserror 2.0.18",
"uuid",
]
[[package]]
name = "buzz-search"
version = "0.1.0"
dependencies = [
"buzz-core",
"chrono",
"nostr",
"reqwest 0.13.3",
"serde",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "buzz-test-client"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"buzz-core",
"buzz-ws-client",
"chrono",
"futures-util",
"hex",
"nostr",
"rand 0.10.1",
"reqwest 0.13.3",
"rust-s3",
"rustls",
"serde",
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "buzz-workflow"
version = "0.1.0"
dependencies = [
"buzz-core",
"buzz-db",
"chrono",
"cron",
"dashmap",
"evalexpr",
"hex",
"nostr",
"reqwest 0.13.3",
"serde",
"serde_json",
"serde_yaml",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "buzz-ws-client"
version = "0.1.0"
dependencies = [
"futures-util",
"nostr",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tracing",
"url",
]
[[package]]
name = "bytemuck"
version = "1.25.0"
@@ -1091,10 +1551,10 @@ name = "countdown-bot"
version = "0.1.0"
dependencies = [
"anyhow",
"buzz-sdk",
"futures-util",
"nostr",
"serde_json",
"sprout-sdk",
"tokio",
"tokio-tungstenite 0.29.0",
"url",
@@ -7010,469 +7470,9 @@ dependencies = [
name = "sprig"
version = "0.1.0"
dependencies = [
"sprout-acp",
"sprout-agent",
"sprout-dev-mcp",
]
[[package]]
name = "sprout-acp"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"chrono",
"clap",
"evalexpr",
"futures-util",
"hex",
"nix 0.31.3",
"nostr",
"reqwest 0.13.3",
"rustls",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"sprout-persona",
"sprout-sdk",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tokio-util",
"toml 1.1.2+spec-1.1.0",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "sprout-admin"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"hex",
"nostr",
"serde_json",
"sprout-auth",
"sprout-core",
"sprout-db",
"tokio",
]
[[package]]
name = "sprout-agent"
version = "0.1.0"
dependencies = [
"arc-swap",
"async-trait",
"axum",
"base64",
"getrandom 0.4.2",
"hex",
"nix 0.31.3",
"reqwest 0.13.3",
"rmcp",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.11.0",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"urlencoding",
"webbrowser",
]
[[package]]
name = "sprout-audit"
version = "0.1.0"
dependencies = [
"chrono",
"futures-util",
"hex",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"sqlx",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "sprout-auth"
version = "0.1.0"
dependencies = [
"hex",
"nostr",
"rand 0.10.1",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"thiserror 2.0.18",
"tokio",
"tracing",
"url",
"uuid",
]
[[package]]
name = "sprout-cli"
version = "0.1.0"
dependencies = [
"base64",
"clap",
"diffy",
"hex",
"infer",
"nostr",
"reqwest 0.13.3",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"sprout-persona",
"sprout-sdk",
"sprout-ws-client",
"thiserror 2.0.18",
"tokio",
"url",
"uuid",
]
[[package]]
name = "sprout-core"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"hmac 0.13.0",
"nostr",
"percent-encoding",
"rand 0.10.1",
"serde",
"serde_json",
"sha2 0.11.0",
"subtle",
"thiserror 2.0.18",
"url",
"uuid",
"zeroize",
]
[[package]]
name = "sprout-db"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"nostr",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"sqlx",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "sprout-dev-mcp"
version = "0.1.0"
dependencies = [
"base64",
"git-credential-nostr",
"git-sign-nostr",
"ignore",
"image",
"nix 0.31.3",
"nostr",
"reqwest 0.13.3",
"rmcp",
"schemars",
"serde",
"serde_json",
"similar",
"sprout-cli",
"tempfile",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"zeroize",
]
[[package]]
name = "sprout-media"
version = "0.1.0"
dependencies = [
"axum",
"blurhash",
"bytes",
"chrono",
"futures-core",
"futures-util",
"hex",
"image",
"imagesize",
"infer",
"mp4",
"nostr",
"rust-s3",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "sprout-pair-relay"
version = "0.1.0"
dependencies = [
"futures-util",
"http-body-util",
"hyper",
"hyper-util",
"parking_lot",
"secp256k1 0.31.1",
"serde_json",
"sha2 0.11.0",
"tokio",
"tokio-tungstenite 0.29.0",
"tokio-util",
]
[[package]]
name = "sprout-pairing-cli"
version = "0.1.0"
dependencies = [
"clap",
"futures-util",
"hex",
"nostr",
"serde_json",
"sprout-core",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"url",
"zeroize",
]
[[package]]
name = "sprout-persona"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror 2.0.18",
]
[[package]]
name = "sprout-proxy"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"dashmap",
"futures-util",
"hex",
"hmac 0.13.0",
"moka",
"nostr",
"rand 0.10.1",
"reqwest 0.13.3",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "sprout-pubsub"
version = "0.1.0"
dependencies = [
"chrono",
"deadpool-redis",
"futures-util",
"nostr",
"redis",
"serde",
"serde_json",
"sprout-auth",
"sprout-core",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "sprout-relay"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"base64",
"bytes",
"chrono",
"dashmap",
"deadpool-redis",
"futures-util",
"hex",
"hmac 0.13.0",
"infer",
"mesh-llm-host-runtime",
"mesh-llm-sdk",
"metrics",
"metrics-exporter-prometheus",
"moka",
"nostr",
"rand 0.10.1",
"redis",
"reqwest 0.13.3",
"rust-s3",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.11.0",
"sprout-audit",
"sprout-auth",
"sprout-core",
"sprout-db",
"sprout-media",
"sprout-pubsub",
"sprout-sdk",
"sprout-search",
"sprout-workflow",
"sqlx",
"subtle",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "sprout-sdk"
version = "0.1.0"
dependencies = [
"nostr",
"serde",
"serde_json",
"sprout-core",
"thiserror 2.0.18",
"uuid",
]
[[package]]
name = "sprout-search"
version = "0.1.0"
dependencies = [
"chrono",
"nostr",
"reqwest 0.13.3",
"serde",
"serde_json",
"sprout-core",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "sprout-test-client"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"chrono",
"futures-util",
"hex",
"nostr",
"rand 0.10.1",
"reqwest 0.13.3",
"rust-s3",
"rustls",
"serde",
"serde_json",
"sha2 0.11.0",
"sprout-core",
"sprout-ws-client",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "sprout-workflow"
version = "0.1.0"
dependencies = [
"chrono",
"cron",
"dashmap",
"evalexpr",
"hex",
"nostr",
"reqwest 0.13.3",
"serde",
"serde_json",
"serde_yaml",
"sprout-core",
"sprout-db",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "sprout-ws-client"
version = "0.1.0"
dependencies = [
"futures-util",
"nostr",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.29.0",
"tracing",
"url",
"buzz-acp",
"buzz-agent",
"buzz-dev-mcp",
]
[[package]]
+33 -33
View File
@@ -1,29 +1,29 @@
[workspace]
members = [
"crates/sprout-relay",
"crates/sprout-core",
"crates/sprout-db",
"crates/sprout-pubsub",
"crates/sprout-auth",
"crates/sprout-search",
"crates/sprout-audit",
"crates/sprout-acp",
"crates/sprout-agent",
"crates/buzz-relay",
"crates/buzz-core",
"crates/buzz-db",
"crates/buzz-pubsub",
"crates/buzz-auth",
"crates/buzz-search",
"crates/buzz-audit",
"crates/buzz-acp",
"crates/buzz-agent",
"crates/sprig",
"crates/sprout-proxy",
"crates/sprout-test-client",
"crates/sprout-ws-client",
"crates/sprout-admin",
"crates/sprout-workflow",
"crates/sprout-media",
"crates/sprout-cli",
"crates/sprout-pairing-cli",
"crates/sprout-sdk",
"crates/sprout-persona",
"crates/buzz-proxy",
"crates/buzz-test-client",
"crates/buzz-ws-client",
"crates/buzz-admin",
"crates/buzz-workflow",
"crates/buzz-media",
"crates/buzz-cli",
"crates/buzz-pairing-cli",
"crates/buzz-sdk",
"crates/buzz-persona",
"crates/git-credential-nostr",
"crates/git-sign-nostr",
"crates/sprout-pair-relay",
"crates/sprout-dev-mcp",
"crates/buzz-pair-relay",
"crates/buzz-dev-mcp",
"examples/countdown-bot",
]
exclude = ["desktop/src-tauri"]
@@ -102,22 +102,22 @@ futures-util = "0.3"
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] }
url = "2"
# MCP SDK (used by sprout-dev-mcp and sprout-agent)
# MCP SDK (used by buzz-dev-mcp and buzz-agent)
rmcp = { version = "1.1.0", features = ["server", "transport-io", "macros"] }
schemars = { version = "1", default-features = false }
# Internal crates
sprout-core = { path = "crates/sprout-core" }
sprout-db = { path = "crates/sprout-db" }
sprout-auth = { path = "crates/sprout-auth" }
sprout-pubsub = { path = "crates/sprout-pubsub" }
sprout-search = { path = "crates/sprout-search" }
sprout-audit = { path = "crates/sprout-audit" }
sprout-proxy = { path = "crates/sprout-proxy" }
sprout-workflow = { path = "crates/sprout-workflow" }
sprout-media = { path = "crates/sprout-media" }
sprout-sdk = { path = "crates/sprout-sdk" }
sprout-ws-client = { path = "crates/sprout-ws-client" }
buzz-core = { path = "crates/buzz-core" }
buzz-db = { path = "crates/buzz-db" }
buzz-auth = { path = "crates/buzz-auth" }
buzz-pubsub = { path = "crates/buzz-pubsub" }
buzz-search = { path = "crates/buzz-search" }
buzz-audit = { path = "crates/buzz-audit" }
buzz-proxy = { path = "crates/buzz-proxy" }
buzz-workflow = { path = "crates/buzz-workflow" }
buzz-media = { path = "crates/buzz-media" }
buzz-sdk = { path = "crates/buzz-sdk" }
buzz-ws-client = { path = "crates/buzz-ws-client" }
# CI profile — release-grade codegen for the relay so e2e tests hit a
# realistic binary, not an unoptimised debug build. Inherits `release`
+6 -6
View File
@@ -3,8 +3,8 @@
FROM --platform=linux/amd64 rust:1.95-bookworm AS builder
WORKDIR /build
COPY . .
RUN cargo build --release -p sprout-relay \
&& strip target/release/sprout-relay
RUN cargo build --release -p buzz-relay \
&& strip target/release/buzz-relay
# ── Web build stage (Node/pnpm) ────────────────────────────
FROM --platform=linux/amd64 node:24-bookworm-slim AS web-builder
@@ -18,22 +18,22 @@ RUN pnpm -C web build
FROM --platform=linux/amd64 debian:bookworm-slim
# CAKE: non-root UID 1000 (numeric, not username)
RUN groupadd -g 1000 sprout && useradd -u 1000 -g sprout -m sprout
RUN groupadd -g 1000 buzz && useradd -u 1000 -g buzz -m buzz
# CAKE: writable dirs
RUN mkdir -p /cache /tmp && chown sprout:sprout /cache /tmp
RUN mkdir -p /cache /tmp && chown buzz:buzz /cache /tmp
# git: relay shells out to `git` for hydrate/receive-pack/upload-pack (S3-backed repos)
# socat: Istio abstract→file socket bridge
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git socat && rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/target/release/sprout-relay /code/sprout-relay
COPY --from=builder /build/target/release/buzz-relay /code/buzz-relay
COPY --from=web-builder /build/web/dist /code/web
COPY script/start /code/start
RUN chmod +x /code/start
ENV SPROUT_WEB_DIR="/code/web"
ENV BUZZ_WEB_DIR="/code/web"
# CAKE: required Envoy env vars (overridden at runtime by CAKE).
ENV ENVOY_ADMIN_SOCKET_PATH="@envoy-admin.sock" \
@@ -1,25 +1,25 @@
[package]
name = "sprout-acp"
name = "buzz-acp"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "ACP harness that bridges Sprout events to AI agents"
description = "ACP harness that bridges Buzz events to AI agents"
[lib]
name = "sprout_acp"
name = "buzz_acp"
path = "src/lib.rs"
[[bin]]
name = "sprout-acp"
name = "buzz-acp"
path = "src/main.rs"
[dependencies]
# Internal
sprout-core = { workspace = true }
sprout-sdk = { workspace = true }
sprout-persona = { path = "../sprout-persona" }
buzz-core = { workspace = true }
buzz-sdk = { workspace = true }
buzz-persona = { path = "../buzz-persona" }
# Nostr
nostr = { workspace = true }
@@ -1,11 +1,11 @@
# sprout-acp
# buzz-acp
ACP harness that connects AI agents to Sprout. The harness listens for @mentions on the relay, prompts your agent, and the agent replies using the Sprout CLI.
ACP harness that connects AI agents to Buzz. The harness listens for @mentions on the relay, prompts your agent, and the agent replies using the Buzz CLI.
```
Sprout Relay ──WS──→ sprout-acp ──stdio──→ Your Agent
Buzz Relay ──WS──→ buzz-acp ──stdio──→ Your Agent
│
Sprout CLI
Buzz CLI
(send_message, etc.)
```
@@ -13,22 +13,22 @@ Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdi
## Prerequisites
- A running Sprout relay (`just relay` starts Docker services automatically, or use a hosted instance)
- A running Buzz relay (`just relay` starts Docker services automatically, or use a hosted instance)
- A Nostr keypair for the agent (see [Generating Keys](#generating-keys))
Build:
```bash
cargo build --release -p sprout-acp
cargo build --release -p buzz-acp
export PATH="$PWD/target/release:$PATH"
```
## Generating Keys
Each agent needs a Nostr keypair — this is the agent's identity in Sprout. Use `sprout-admin` to mint one:
Each agent needs a Nostr keypair — this is the agent's identity in Buzz. Use `buzz-admin` to mint one:
```bash
cargo run -p sprout-admin -- mint-token --name "my-agent" --scopes "messages:read,messages:write,channels:read"
cargo run -p buzz-admin -- mint-token --name "my-agent" --scopes "messages:read,messages:write,channels:read"
```
This prints an `nsec1...` private key and an API token. **Save both immediately — they're shown only once.**
@@ -41,19 +41,19 @@ The harness discovers channels by querying the relay with the agent's authentica
By default, the harness discovers only channels the agent is a **member** of (`GET /api/channels?member=true`). When the agent is added to a new channel, the membership notification subscription auto-subscribes to it.
**Private channels** require explicit membership. The relay doesn't yet have a REST/event API for managing channel members — this is a known gap. For now, use `create_channel` via the Sprout CLI to create new channels (the creator is automatically a member).
**Private channels** require explicit membership. The relay doesn't yet have a REST/event API for managing channel members — this is a known gap. For now, use `create_channel` via the Buzz CLI to create new channels (the creator is automatically a member).
## Quick Start (goose)
```bash
export SPROUT_PRIVATE_KEY="nsec1..." # your agent's key (see "Generating Keys")
export SPROUT_RELAY_URL="ws://localhost:3000"
export BUZZ_PRIVATE_KEY="nsec1..." # your agent's key (see "Generating Keys")
export BUZZ_RELAY_URL="ws://localhost:3000"
export GOOSE_MODE=auto
sprout-acp
buzz-acp
```
That's it. The harness spawns `goose acp`, connects to the relay, discovers channels, and starts listening. When someone @mentions the agent, goose receives the message and can reply using the Sprout CLI that the harness configures automatically.
That's it. The harness spawns `goose acp`, connects to the relay, discovers channels, and starts listening. When someone @mentions the agent, goose receives the message and can reply using the Buzz CLI that the harness configures automatically.
## Running with Codex
@@ -65,10 +65,10 @@ cd /path/to/codex-acp && cargo build --release
# Run
export OPENAI_API_KEY="sk-..." # required — use an OpenAI API key, not a ChatGPT subscription
export SPROUT_ACP_AGENT_COMMAND="/path/to/codex-acp/target/release/codex-acp"
export SPROUT_ACP_AGENT_ARGS='-c,permissions.approval_policy="never"'
export BUZZ_ACP_AGENT_COMMAND="/path/to/codex-acp/target/release/codex-acp"
export BUZZ_ACP_AGENT_ARGS='-c,permissions.approval_policy="never"'
sprout-acp
buzz-acp
```
> **API key note:** `codex-acp` always attempts a ChatGPT WebSocket login first, which logs a `426 Upgrade Required` error. This is expected and non-fatal — it falls back to `OPENAI_API_KEY` automatically. Set `OPENAI_API_KEY` to ensure it has a working fallback.
@@ -83,12 +83,12 @@ npm install -g @agentclientprotocol/claude-agent-acp
# Run
export ANTHROPIC_API_KEY="sk-ant-..."
export SPROUT_ACP_AGENT_COMMAND="claude-agent-acp"
export BUZZ_ACP_AGENT_COMMAND="claude-agent-acp"
sprout-acp
buzz-acp
```
Older installs that still expose `claude-code-acp` are also supported. `sprout-acp`
Older installs that still expose `claude-code-acp` are also supported. `buzz-acp`
treats both Claude ACP command names as the same zero-arg runtime.
## Configuration
@@ -99,27 +99,27 @@ All configuration is via environment variables (or CLI flags — every env var h
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `SPROUT_PRIVATE_KEY` | **yes** | — | Agent's Nostr private key (`nsec1...`). Used for relay auth and agent identity. |
| `SPROUT_RELAY_URL` | no | `ws://localhost:3000` | Relay WebSocket URL. |
| `SPROUT_ACP_AGENT_COMMAND` | no | `goose` | Agent binary to spawn. |
| `SPROUT_ACP_AGENT_ARGS` | no | `acp` | Agent arguments (comma-separated). |
| `SPROUT_ACP_MCP_COMMAND` | no | `""` (empty) | Path to an optional MCP server binary to provide to the agent subprocess. |
| `SPROUT_ACP_IDLE_TIMEOUT` | no | `620` | Idle timeout: max seconds of silence before cancelling a turn. Resets on any agent stdout activity. |
| `SPROUT_ACP_MAX_TURN_DURATION` | no | `3600` | Absolute wall-clock cap per turn (safety valve). |
| `SPROUT_API_TOKEN` | no | — | API token (required if relay enforces token auth). |
| `BUZZ_PRIVATE_KEY` | **yes** | — | Agent's Nostr private key (`nsec1...`). Used for relay auth and agent identity. |
| `BUZZ_RELAY_URL` | no | `ws://localhost:3000` | Relay WebSocket URL. |
| `BUZZ_ACP_AGENT_COMMAND` | no | `goose` | Agent binary to spawn. |
| `BUZZ_ACP_AGENT_ARGS` | no | `acp` | Agent arguments (comma-separated). |
| `BUZZ_ACP_MCP_COMMAND` | no | `""` (empty) | Path to an optional MCP server binary to provide to the agent subprocess. |
| `BUZZ_ACP_IDLE_TIMEOUT` | no | `620` | Idle timeout: max seconds of silence before cancelling a turn. Resets on any agent stdout activity. |
| `BUZZ_ACP_MAX_TURN_DURATION` | no | `3600` | Absolute wall-clock cap per turn (safety valve). |
| `BUZZ_API_TOKEN` | no | — | API token (required if relay enforces token auth). |
**Note:** `SPROUT_ACP_AGENT_ARGS` splits on commas. For args with values, use: `-c,key="value"`.
**Note:** `BUZZ_ACP_AGENT_ARGS` splits on commas. For args with values, use: `-c,key="value"`.
**Legacy env vars:** `SPROUT_ACP_PRIVATE_KEY`, `SPROUT_ACP_API_TOKEN`, and `SPROUT_ACP_TURN_TIMEOUT` (replaced by `SPROUT_ACP_IDLE_TIMEOUT`) are still accepted as fallbacks.
**Legacy env vars:** `BUZZ_ACP_PRIVATE_KEY`, `BUZZ_ACP_API_TOKEN`, and `BUZZ_ACP_TURN_TIMEOUT` (replaced by `BUZZ_ACP_IDLE_TIMEOUT`) are still accepted as fallbacks.
### Parallel Agents & Heartbeat
| Flag | Env Var | Default | Description |
|------|---------|---------|-------------|
| `--agents` | `SPROUT_ACP_AGENTS` | `1` | Number of agent subprocesses (1–32). |
| `--heartbeat-interval` | `SPROUT_ACP_HEARTBEAT_INTERVAL` | `0` | Seconds between heartbeat prompts. `0` = disabled. Must be `0` or ≥10 when enabled. |
| `--heartbeat-prompt` | `SPROUT_ACP_HEARTBEAT_PROMPT` | (built-in) | Custom heartbeat prompt text. Conflicts with `--heartbeat-prompt-file`. |
| `--heartbeat-prompt-file` | `SPROUT_ACP_HEARTBEAT_PROMPT_FILE` | — | Read heartbeat prompt from a file. Conflicts with `--heartbeat-prompt`. |
| `--agents` | `BUZZ_ACP_AGENTS` | `1` | Number of agent subprocesses (1–32). |
| `--heartbeat-interval` | `BUZZ_ACP_HEARTBEAT_INTERVAL` | `0` | Seconds between heartbeat prompts. `0` = disabled. Must be `0` or ≥10 when enabled. |
| `--heartbeat-prompt` | `BUZZ_ACP_HEARTBEAT_PROMPT` | (built-in) | Custom heartbeat prompt text. Conflicts with `--heartbeat-prompt-file`. |
| `--heartbeat-prompt-file` | `BUZZ_ACP_HEARTBEAT_PROMPT_FILE` | — | Read heartbeat prompt from a file. Conflicts with `--heartbeat-prompt`. |
### Inbound Author Gate
@@ -127,8 +127,8 @@ Controls which authors' events the harness forwards to the agent. Events from di
| Flag | Env Var | Default | Description |
|------|---------|---------|-------------|
| `--respond-to` | `SPROUT_ACP_RESPOND_TO` | `owner-only` | Author gate mode: `owner-only`, `allowlist`, `anyone`, `nobody`. |
| `--respond-to-allowlist` | `SPROUT_ACP_RESPOND_TO_ALLOWLIST` | — | Comma-separated 64-char hex pubkeys (required when mode is `allowlist`). Owner is always implicitly included. |
| `--respond-to` | `BUZZ_ACP_RESPOND_TO` | `owner-only` | Author gate mode: `owner-only`, `allowlist`, `anyone`, `nobody`. |
| `--respond-to-allowlist` | `BUZZ_ACP_RESPOND_TO_ALLOWLIST` | — | Comma-separated 64-char hex pubkeys (required when mode is `allowlist`). Owner is always implicitly included. |
**Modes:**
@@ -147,39 +147,39 @@ The gate applies to **all** inbound events — @mentions, DMs, thread replies, a
```bash
# Default: only respond to owner
sprout-acp
buzz-acp
# Respond to a team of three users (owner always included automatically)
sprout-acp --respond-to allowlist \
buzz-acp --respond-to allowlist \
--respond-to-allowlist "abc123...64hex,def456...64hex,789abc...64hex"
# Respond to anyone (open agent)
sprout-acp --respond-to anyone
buzz-acp --respond-to anyone
# Broadcast-only: post on heartbeat, ignore all inbound events
sprout-acp --respond-to nobody --heartbeat-interval 300
buzz-acp --respond-to nobody --heartbeat-interval 300
```
### Configuration Examples
**Single agent, no heartbeat (default):**
```bash
sprout-acp
buzz-acp
```
**Four agents, no heartbeat (high-throughput event processing):**
```bash
sprout-acp --agents 4
buzz-acp --agents 4
```
**Two agents with 5-minute heartbeat:**
```bash
sprout-acp --agents 2 --heartbeat-interval 300
buzz-acp --agents 2 --heartbeat-interval 300
```
**Custom heartbeat prompt:**
```bash
sprout-acp --agents 2 --heartbeat-interval 300 \
buzz-acp --agents 2 --heartbeat-interval 300 \
--heartbeat-prompt "Check get_feed_actions() for pending approvals, then get_feed_mentions() for unanswered mentions. If nothing actionable, end your turn immediately."
```
@@ -208,12 +208,12 @@ By default, the ACP harness subscribes to stream message kinds (9, 46010, 40007)
**CLI flags:**
```bash
sprout-acp --kinds 9,46010,40007,45001,45002,45003 --no-mention-filter
buzz-acp --kinds 9,46010,40007,45001,45002,45003 --no-mention-filter
```
**Or with `--subscribe all`:**
```bash
sprout-acp --subscribe all --kinds 9,46010,40007,45001,45002,45003
buzz-acp --subscribe all --kinds 9,46010,40007,45001,45002,45003
```
**Per-channel config:**
@@ -236,7 +236,7 @@ Forum event kinds:
2. **Channel discovery** — Queries the relay REST API for accessible channels, subscribes to each.
3. **Event loop** — Listens for @mention events (kind 9 with the agent's pubkey in a `#p` tag). Events queue per channel.
4. **Prompting** — When events are pending and no prompt is in flight for that channel, drains all queued events for the oldest channel into a single batched prompt via ACP `session/prompt`.
5. **Agent response** — The agent processes the prompt and uses the Sprout CLI (`send_message`, `get_messages`, etc.) to interact with Sprout.
5. **Agent response** — The agent processes the prompt and uses the Buzz CLI (`send_message`, `get_messages`, etc.) to interact with Buzz.
6. **Recovery** — If the agent crashes, the harness respawns it. If the relay disconnects, the harness reconnects with a `since` filter to avoid missing events.
Each channel has at most one prompt in flight. Multiple channels can be processed concurrently when agents > 1.
@@ -252,7 +252,7 @@ The harness works with any agent that implements the [ACP spec](https://agentcli
- Accept `session/prompt` with a text message and stream `session/update` notifications
- Return a `stopReason` (`end_turn`, `cancelled`, `max_tokens`, etc.)
Set `SPROUT_ACP_AGENT_COMMAND` and `SPROUT_ACP_AGENT_ARGS` to point at your agent binary.
Set `BUZZ_ACP_AGENT_COMMAND` and `BUZZ_ACP_AGENT_ARGS` to point at your agent binary.
## Testing
@@ -202,7 +202,7 @@ impl AcpClient {
// Callers MUST still call shutdown().await for guaranteed cleanup.
.kill_on_drop(true);
// Per-persona env vars (e.g., GOOSE_PROVIDER, SPROUT_AGENT_PROVIDER).
// Per-persona env vars (e.g., GOOSE_PROVIDER, BUZZ_AGENT_PROVIDER).
// Only injected if not already set in parent env (operator precedence).
for (key, value) in extra_env {
if std::env::var(key).is_err() {
@@ -274,7 +274,7 @@ impl AcpClient {
"protocolVersion": 1,
"clientCapabilities": {},
"clientInfo": {
"name": "sprout-acp",
"name": "buzz-acp",
"version": env!("CARGO_PKG_VERSION")
}
});
@@ -374,7 +374,7 @@ impl AcpClient {
///
/// Used for slash-command pass-through: ACP connectors detect commands via
/// the **first** block's text starting with `/`, so the harness sends
/// `["/cmd args", "<sprout context>"]` instead of one wrapped block.
/// `["/cmd args", "<buzz context>"]` instead of one wrapped block.
pub async fn session_prompt_blocks_with_idle_timeout(
&mut self,
session_id: &str,
@@ -1394,7 +1394,7 @@ mod tests {
"protocolVersion": 1,
"clientCapabilities": {},
"clientInfo": {
"name": "sprout-acp",
"name": "buzz-acp",
"version": "0.1.0"
}
}
@@ -1402,7 +1402,7 @@ mod tests {
assert_eq!(msg["params"]["protocolVersion"].as_u64(), Some(1));
assert_eq!(
msg["params"]["clientInfo"]["name"].as_str(),
Some("sprout-acp")
Some("buzz-acp")
);
assert!(msg["params"]["clientCapabilities"].is_object());
}
@@ -1416,11 +1416,11 @@ mod tests {
args: vec![],
env: vec![
EnvVar {
name: "SPROUT_RELAY_URL".into(),
name: "BUZZ_RELAY_URL".into(),
value: "ws://localhost:3000".into(),
},
EnvVar {
name: "SPROUT_PRIVATE_KEY".into(),
name: "BUZZ_PRIVATE_KEY".into(),
value: "nsec1abc".into(),
},
],
@@ -1437,13 +1437,13 @@ mod tests {
assert_eq!(serialized["env"].as_array().unwrap().len(), 2);
assert_eq!(
serialized["env"][0]["name"].as_str(),
Some("SPROUT_RELAY_URL")
Some("BUZZ_RELAY_URL")
);
}
#[test]
fn session_prompt_request_format() {
let prompt_text = "[Sprout @mention]\nChannel: test\nFrom: npub1...\nMessage: hello";
let prompt_text = "[Buzz @mention]\nChannel: test\nFrom: npub1...\nMessage: hello";
let msg = serde_json::json!({
"jsonrpc": "2.0",
"id": 2u64,
@@ -1469,7 +1469,7 @@ mod tests {
"sess_abc123",
&[
"/goal ship it",
"[Sprout event: @mention]\nContent: @Eva /goal ship it",
"[Buzz event: @mention]\nContent: @Eva /goal ship it",
],
);
let prompt = params["prompt"].as_array().unwrap();
@@ -1,24 +1,24 @@
You are operating inside the Sprout platform — a Nostr-based messaging platform for human-agent collaboration. The sprout-acp harness routes channel events to your session.
You are operating inside the Buzz platform — a Nostr-based messaging platform for human-agent collaboration. The buzz-acp harness routes channel events to your session.
## Sprout CLI
## Buzz CLI
The `sprout` CLI is your primary interface. Auth env vars: `SPROUT_RELAY_URL`, `SPROUT_PRIVATE_KEY`, `SPROUT_AUTH_TAG`. Exit codes: 0 ok, 1 user error, 2 network, 3 auth, 4 other. Output is structured JSON — pipe through `jq` as needed.
The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ_PRIVATE_KEY`, `BUZZ_AUTH_TAG`. Exit codes: 0 ok, 1 user error, 2 network, 3 auth, 4 other. Output is structured JSON — pipe through `jq` as needed.
| Group | Key commands |
|-------|-------------|
| `sprout messages` | `send`, `get`, `thread`, `search` |
| `sprout channels` | `list`, `get`, `create`, `join`, `members` |
| `sprout canvas` | `get`, `set` |
| `sprout reactions` | `add`, `remove` |
| `sprout dms` | `list`, `open` |
| `sprout users` | `get`, `set-profile`, `presence` |
| `sprout workflows` | `list`, `trigger`, `runs` |
| `sprout feed` | `get` |
| `sprout social` | `publish`, `notes` |
| `sprout repos` | `create`, `get`, `list` |
| `sprout upload` | `file` |
| `buzz messages` | `send`, `get`, `thread`, `search` |
| `buzz channels` | `list`, `get`, `create`, `join`, `members` |
| `buzz canvas` | `get`, `set` |
| `buzz reactions` | `add`, `remove` |
| `buzz dms` | `list`, `open` |
| `buzz users` | `get`, `set-profile`, `presence` |
| `buzz workflows` | `list`, `trigger`, `runs` |
| `buzz feed` | `get` |
| `buzz social` | `publish`, `notes` |
| `buzz repos` | `create`, `get`, `list` |
| `buzz upload` | `file` |
Run `sprout --help` or `sprout <group> --help` for full usage.
Run `buzz --help` or `buzz <group> --help` for full usage.
## Communication Patterns
@@ -27,18 +27,18 @@ Run `sprout --help` or `sprout <group> --help` for full usage.
- Respond promptly to @mentions.
- Be direct. State what you did, what you found, or what you need. No preamble.
- Message content supports GitHub-flavored Markdown. Use fenced code blocks with a language tag (` ```python `, ` ```typescript `, etc.) for syntax-highlighted rendering on desktop and mobile. Omitting the language tag renders monochrome.
- Reply to the thread root (`sprout messages send --reply-to <thread-root-event-id>`), not the latest message — flat threads stay readable; reply chains bury context 3+ levels deep. One thread = one unit of work: ask sub-questions inline. A real tangent starts a new top-level message.
- Reply to the thread root (`buzz messages send --reply-to <thread-root-event-id>`), not the latest message — flat threads stay readable; reply chains bury context 3+ levels deep. One thread = one unit of work: ask sub-questions inline. A real tangent starts a new top-level message.
- Work in the thread, report milestones at the root. The thread is the messy middle — progress, dead ends, clarifying questions, and routine updates. Use a top-level post for channel-visible milestones: picked up, blocked + need input, change ready / PR up, done, or anything teammates skimming only root-level messages must act on. Thread notifications are easy to miss; a top-level post ensures the requester sees the outcome.
- New topic → new top-level message. Don't graft an unrelated task onto an existing thread.
- When you are mentioned in multiple threads, prioritize the most recent one chronologically. If someone steers or redirects you in a newer thread while you are working from an older dispatch, reply in the newer thread to acknowledge — do not bury your response in the original thread where it may go unseen.
- No push notifications — poll with `sprout messages get --channel <UUID> --since <ts>`. When `since` is set without `before`, results are oldest-first (chronological).
- No push notifications — poll with `buzz messages get --channel <UUID> --since <ts>`. When `since` is set without `before`, results are oldest-first (chronological).
## Startup Recovery
1. `sprout feed get` — surface pending mentions and action items. Filter by type: `mentions`, `needs_action`, `activity`, `agent_activity`.
2. `sprout messages get --channel <UUID>` on assigned channels — catch up on recent history.
1. `buzz feed get` — surface pending mentions and action items. Filter by type: `mentions`, `needs_action`, `activity`, `agent_activity`.
2. `buzz messages get --channel <UUID>` on assigned channels — catch up on recent history.
3. Check `AGENTS.md` in your working directory for team context.
4. Check `RESEARCH/`, `GUIDES/`, `PLANS/` before searching externally. Use `sprout messages search --query "..."` for cross-channel keyword lookups.
4. Check `RESEARCH/`, `GUIDES/`, `PLANS/` before searching externally. Use `buzz messages search --query "..."` for cross-channel keyword lookups.
## Workspace Layout
@@ -1,4 +1,4 @@
//! Configuration for the sprout-acp harness.
//! Configuration for the buzz-acp harness.
//!
//! CLI-first: every option is a CLI flag with env var fallback.
//! Config file (TOML) for complex subscription rules.
@@ -19,11 +19,11 @@ use crate::filter::SubscriptionRule;
/// deprecated `--turn-timeout` is set.
///
/// Sized for slow turns where the agent may go silent on its outer ACP channel
/// while running long sub-tools (e.g. a sprout-agent running another agent, or
/// while running long sub-tools (e.g. a buzz-agent running another agent, or
/// codex/claude doing multi-minute single tool calls). 900s gives 300s of
/// breathing room above the 600s max shell timeout, so legitimate long-running
/// tool calls don't race the idle deadline.
/// Override via `--idle-timeout` / `SPROUT_ACP_IDLE_TIMEOUT`.
/// Override via `--idle-timeout` / `BUZZ_ACP_IDLE_TIMEOUT`.
pub(crate) const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 900;
// ── Errors ────────────────────────────────────────────────────────────────────
@@ -153,25 +153,25 @@ impl std::fmt::Display for PermissionMode {
// ── Models subcommand ─────────────────────────────────────────────────────────
/// CLI args for `sprout-acp models` — query available models from an agent.
/// CLI args for `buzz-acp models` — query available models from an agent.
///
/// This is a standalone `Parser` (not a subcommand variant) because the
/// `models` path must bypass `Config::from_cli()` entirely — no relay,
/// no private key, no harness setup.
#[derive(Debug, Parser)]
#[command(
name = "sprout-acp models",
name = "buzz-acp models",
about = "Query available models from the configured agent"
)]
pub struct ModelsArgs {
/// Agent binary to spawn (e.g. "goose", "claude-agent-acp", "codex-acp").
#[arg(long, env = "SPROUT_ACP_AGENT_COMMAND", default_value = "goose")]
#[arg(long, env = "BUZZ_ACP_AGENT_COMMAND", default_value = "goose")]
pub agent_command: String,
/// Arguments passed to the agent binary.
#[arg(
long,
env = "SPROUT_ACP_AGENT_ARGS",
env = "BUZZ_ACP_AGENT_ARGS",
default_value = "acp",
value_delimiter = ','
)]
@@ -186,74 +186,74 @@ pub struct ModelsArgs {
#[derive(Debug, Parser)]
#[command(
name = "sprout-acp",
about = "ACP harness that bridges Sprout events to AI agents"
name = "buzz-acp",
about = "ACP harness that bridges Buzz events to AI agents"
)]
pub struct CliArgs {
#[arg(long, env = "SPROUT_RELAY_URL", default_value = "ws://localhost:3000")]
#[arg(long, env = "BUZZ_RELAY_URL", default_value = "ws://localhost:3000")]
pub relay_url: String,
#[arg(long, env = "SPROUT_PRIVATE_KEY")]
#[arg(long, env = "BUZZ_PRIVATE_KEY")]
pub private_key: String,
/// Agent owner pubkey (64-char hex). Used for --respond-to=owner-only gate.
#[arg(long, env = "SPROUT_ACP_AGENT_OWNER")]
#[arg(long, env = "BUZZ_ACP_AGENT_OWNER")]
pub agent_owner: Option<String>,
#[arg(long, env = "SPROUT_ACP_AGENT_COMMAND", default_value = "goose")]
#[arg(long, env = "BUZZ_ACP_AGENT_COMMAND", default_value = "goose")]
pub agent_command: String,
#[arg(
long,
env = "SPROUT_ACP_AGENT_ARGS",
env = "BUZZ_ACP_AGENT_ARGS",
default_value = "acp",
value_delimiter = ','
)]
pub agent_args: Vec<String>,
#[arg(long, env = "SPROUT_ACP_MCP_COMMAND", default_value = "")]
#[arg(long, env = "BUZZ_ACP_MCP_COMMAND", default_value = "")]
pub mcp_command: String,
/// Idle timeout: max seconds of silence before killing a turn.
/// Resets on any agent stdout activity.
#[arg(long, env = "SPROUT_ACP_IDLE_TIMEOUT")]
#[arg(long, env = "BUZZ_ACP_IDLE_TIMEOUT")]
pub idle_timeout: Option<u64>,
/// Absolute wall-clock cap per turn (safety valve).
#[arg(long, env = "SPROUT_ACP_MAX_TURN_DURATION", default_value = "3600")]
#[arg(long, env = "BUZZ_ACP_MAX_TURN_DURATION", default_value = "3600")]
pub max_turn_duration: u64,
/// Deprecated: alias for --idle-timeout. If both set, --idle-timeout wins.
#[arg(long, env = "SPROUT_ACP_TURN_TIMEOUT", hide = true)]
#[arg(long, env = "BUZZ_ACP_TURN_TIMEOUT", hide = true)]
pub turn_timeout: Option<u64>,
#[arg(
long,
env = "SPROUT_ACP_SYSTEM_PROMPT",
env = "BUZZ_ACP_SYSTEM_PROMPT",
conflicts_with = "system_prompt_file"
)]
pub system_prompt: Option<String>,
#[arg(
long,
env = "SPROUT_ACP_SYSTEM_PROMPT_FILE",
env = "BUZZ_ACP_SYSTEM_PROMPT_FILE",
conflicts_with = "system_prompt"
)]
pub system_prompt_file: Option<PathBuf>,
/// Number of parallel agent subprocesses.
#[arg(long, env = "SPROUT_ACP_AGENTS", default_value_t = 1,
#[arg(long, env = "BUZZ_ACP_AGENTS", default_value_t = 1,
value_parser = clap::value_parser!(u32).range(1..=32))]
pub agents: u32,
/// Seconds between heartbeat prompts. 0 = disabled.
#[arg(long, env = "SPROUT_ACP_HEARTBEAT_INTERVAL", default_value_t = 0)]
#[arg(long, env = "BUZZ_ACP_HEARTBEAT_INTERVAL", default_value_t = 0)]
pub heartbeat_interval: u64,
/// Heartbeat prompt text. Conflicts with --heartbeat-prompt-file.
#[arg(
long,
env = "SPROUT_ACP_HEARTBEAT_PROMPT",
env = "BUZZ_ACP_HEARTBEAT_PROMPT",
conflicts_with = "heartbeat_prompt_file"
)]
pub heartbeat_prompt: Option<String>,
@@ -261,35 +261,35 @@ pub struct CliArgs {
/// Read heartbeat prompt from file.
#[arg(
long,
env = "SPROUT_ACP_HEARTBEAT_PROMPT_FILE",
env = "BUZZ_ACP_HEARTBEAT_PROMPT_FILE",
conflicts_with = "heartbeat_prompt"
)]
pub heartbeat_prompt_file: Option<PathBuf>,
#[arg(long, env = "SPROUT_ACP_INITIAL_MESSAGE")]
#[arg(long, env = "BUZZ_ACP_INITIAL_MESSAGE")]
pub initial_message: Option<String>,
#[arg(
long,
env = "SPROUT_ACP_SUBSCRIBE",
env = "BUZZ_ACP_SUBSCRIBE",
default_value = "mentions",
value_enum
)]
pub subscribe: SubscribeMode,
#[arg(long, env = "SPROUT_ACP_KINDS", value_delimiter = ',')]
#[arg(long, env = "BUZZ_ACP_KINDS", value_delimiter = ',')]
pub kinds: Option<Vec<u32>>,
#[arg(long, env = "SPROUT_ACP_CHANNELS", value_delimiter = ',')]
#[arg(long, env = "BUZZ_ACP_CHANNELS", value_delimiter = ',')]
pub channels: Option<Vec<String>>,
#[arg(long, env = "SPROUT_ACP_NO_MENTION_FILTER")]
#[arg(long, env = "BUZZ_ACP_NO_MENTION_FILTER")]
pub no_mention_filter: bool,
#[arg(long, env = "SPROUT_ACP_CONFIG", default_value = "./sprout-acp.toml")]
#[arg(long, env = "BUZZ_ACP_CONFIG", default_value = "./buzz-acp.toml")]
pub config: PathBuf,
#[arg(long, env = "SPROUT_ACP_DEDUP", default_value = "queue", value_enum)]
#[arg(long, env = "BUZZ_ACP_DEDUP", default_value = "queue", value_enum)]
pub dedup: DedupMode,
/// How to handle new @mentions while a turn is already in-flight.
@@ -297,33 +297,33 @@ pub struct CliArgs {
/// owner-interrupt: cancel only for agent owner's mentions.
#[arg(
long,
env = "SPROUT_ACP_MULTIPLE_EVENT_HANDLING",
env = "BUZZ_ACP_MULTIPLE_EVENT_HANDLING",
default_value = "queue",
value_enum
)]
pub multiple_event_handling: MultipleEventHandling,
#[arg(long, env = "SPROUT_ACP_NO_IGNORE_SELF")]
#[arg(long, env = "BUZZ_ACP_NO_IGNORE_SELF")]
pub no_ignore_self: bool,
/// Maximum number of context messages to include for thread replies and DMs.
/// Set to 0 to disable automatic context fetching. Max 100.
#[arg(long, env = "SPROUT_ACP_CONTEXT_MESSAGE_LIMIT", default_value_t = 12,
#[arg(long, env = "BUZZ_ACP_CONTEXT_MESSAGE_LIMIT", default_value_t = 12,
value_parser = clap::value_parser!(u32).range(0..=100))]
pub context_message_limit: u32,
/// Maximum turns per session before proactive rotation. 0 = disabled
/// (rotate only on MaxTokens / MaxTurnRequests).
#[arg(long, env = "SPROUT_ACP_MAX_TURNS_PER_SESSION", default_value_t = 0,
#[arg(long, env = "BUZZ_ACP_MAX_TURNS_PER_SESSION", default_value_t = 0,
value_parser = clap::value_parser!(u32))]
pub max_turns_per_session: u32,
/// Disable automatic presence (online/offline) status.
#[arg(long, env = "SPROUT_ACP_NO_PRESENCE")]
#[arg(long, env = "BUZZ_ACP_NO_PRESENCE")]
pub no_presence: bool,
/// Disable typing indicators while agent is processing.
#[arg(long, env = "SPROUT_ACP_NO_TYPING")]
#[arg(long, env = "BUZZ_ACP_NO_TYPING")]
pub no_typing: bool,
/// Enable NIP-AE agent core memory injection.
@@ -331,13 +331,13 @@ pub struct CliArgs {
/// Memory injection is on by default. When enabled, the harness
/// fetches the agent's per-session core engram and renders it as an
/// `[Agent Memory — core]` prompt section (or renders the onboarding nudge
/// when the relay confirms no core engram exists). The `sprout mem` CLI
/// when the relay confirms no core engram exists). The `buzz mem` CLI
/// and the relay's acceptance of kind:30174 engrams are unaffected — this
/// flag controls prompt-time injection in the ACP harness only.
/// Pass `--no-memory` / `SPROUT_ACP_NO_MEMORY=true` to disable.
/// Pass `--no-memory` / `BUZZ_ACP_NO_MEMORY=true` to disable.
#[arg(
long,
env = "SPROUT_ACP_MEMORY",
env = "BUZZ_ACP_MEMORY",
conflicts_with = "no_memory",
default_value_t = true
)]
@@ -346,26 +346,26 @@ pub struct CliArgs {
/// Disable NIP-AE agent core memory injection.
///
/// Memory injection is on by default; set this flag/env var to opt out.
#[arg(long, env = "SPROUT_ACP_NO_MEMORY", conflicts_with = "memory")]
#[arg(long, env = "BUZZ_ACP_NO_MEMORY", conflicts_with = "memory")]
pub no_memory: bool,
/// Disable the [Base] platform-context section prepended to every prompt.
/// When set, agents receive only the persona [System] prompt with no Sprout orientation.
#[arg(long, env = "SPROUT_ACP_NO_BASE_PROMPT")]
/// When set, agents receive only the persona [System] prompt with no Buzz orientation.
#[arg(long, env = "BUZZ_ACP_NO_BASE_PROMPT")]
pub no_base_prompt: bool,
/// Path to a custom base prompt file. Overrides the compiled-in default.
/// Mutually exclusive with --no-base-prompt.
#[arg(
long,
env = "SPROUT_ACP_BASE_PROMPT_FILE",
env = "BUZZ_ACP_BASE_PROMPT_FILE",
conflicts_with = "no_base_prompt"
)]
pub base_prompt_file: Option<PathBuf>,
/// Desired LLM model ID. Applied to every new ACP session after creation.
/// Use `sprout-acp models` to discover available model IDs.
#[arg(long, env = "SPROUT_ACP_MODEL")]
/// Use `buzz-acp models` to discover available model IDs.
#[arg(long, env = "BUZZ_ACP_MODEL")]
pub model: Option<String>,
/// Permission mode for agents that support `session/set_config_option`
@@ -376,7 +376,7 @@ pub struct CliArgs {
/// behaviour.
#[arg(
long,
env = "SPROUT_ACP_PERMISSION_MODE",
env = "BUZZ_ACP_PERMISSION_MODE",
default_value = "bypass-permissions",
value_enum
)]
@@ -386,7 +386,7 @@ pub struct CliArgs {
/// Modes: owner-only (default), allowlist, anyone, nobody.
#[arg(
long,
env = "SPROUT_ACP_RESPOND_TO",
env = "BUZZ_ACP_RESPOND_TO",
default_value = "owner-only",
value_enum
)]
@@ -394,20 +394,20 @@ pub struct CliArgs {
/// Comma-separated 64-char hex pubkeys for allowlist mode.
/// Owner pubkey is always implicitly included.
#[arg(long, env = "SPROUT_ACP_RESPOND_TO_ALLOWLIST", value_delimiter = ',')]
#[arg(long, env = "BUZZ_ACP_RESPOND_TO_ALLOWLIST", value_delimiter = ',')]
pub respond_to_allowlist: Option<Vec<String>>,
/// Path to a persona pack directory. Used with --persona-name to configure
/// the agent from a .persona.md pack instead of CLI flags.
#[arg(long, env = "SPROUT_ACP_PERSONA_PACK")]
#[arg(long, env = "BUZZ_ACP_PERSONA_PACK")]
pub persona_pack: Option<PathBuf>,
/// Name of the persona within the pack to use. Required when --persona-pack is set.
#[arg(long, env = "SPROUT_ACP_PERSONA_NAME")]
#[arg(long, env = "BUZZ_ACP_PERSONA_NAME")]
pub persona_name: Option<String>,
/// Publish encrypted ACP observer frames over the relay.
#[arg(long, env = "SPROUT_ACP_RELAY_OBSERVER", default_value_t = false)]
#[arg(long, env = "BUZZ_ACP_RELAY_OBSERVER", default_value_t = false)]
pub relay_observer: bool,
}
@@ -454,7 +454,7 @@ pub struct Config {
/// Whether NIP-AE agent core memory injection is enabled. When false,
/// the harness skips the per-session core engram fetch and renders no
/// `[Agent Memory — core]` section. On by default; disabled via the
/// `--no-memory` / `SPROUT_ACP_NO_MEMORY` opt-out.
/// `--no-memory` / `BUZZ_ACP_NO_MEMORY` opt-out.
pub memory_enabled: bool,
/// Desired LLM model ID. Applied after every `session_new_full()`.
pub model: Option<String>,
@@ -464,7 +464,7 @@ pub struct Config {
pub respond_to: RespondTo,
/// Validated allowlist of pubkey hex strings (used when respond_to == Allowlist).
pub respond_to_allowlist: HashSet<String>,
/// Per-persona env vars to inject at agent spawn time (e.g., GOOSE_PROVIDER, GOOSE_MODEL, SPROUT_AGENT_MODEL).
/// Per-persona env vars to inject at agent spawn time (e.g., GOOSE_PROVIDER, GOOSE_MODEL, BUZZ_AGENT_MODEL).
/// Populated from persona pack resolution. Empty when no pack is configured.
pub persona_env_vars: Vec<(String, String)>,
/// Whether to publish encrypted observer frames through the relay.
@@ -517,7 +517,7 @@ fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_agent_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "sprout-agent" => Some(Vec::new()),
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
}
}
@@ -560,8 +560,8 @@ pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<Strin
/// // Must be called before tokio runtime starts — see Rust 2024 edition safety.
pub fn propagate_legacy_env_vars() {
for (legacy, canonical) in [
("SPROUT_ACP_PRIVATE_KEY", "SPROUT_PRIVATE_KEY"),
("SPROUT_ACP_API_TOKEN", "SPROUT_API_TOKEN"),
("BUZZ_ACP_PRIVATE_KEY", "BUZZ_PRIVATE_KEY"),
("BUZZ_ACP_API_TOKEN", "BUZZ_API_TOKEN"),
] {
if std::env::var(canonical).is_err() {
if let Ok(val) = std::env::var(legacy) {
@@ -675,16 +675,16 @@ impl Config {
let raw = match (args.idle_timeout, args.turn_timeout) {
(Some(idle), Some(_turn)) => {
tracing::warn!(
"--turn-timeout / SPROUT_ACP_TURN_TIMEOUT is deprecated and ignored \
when --idle-timeout / SPROUT_ACP_IDLE_TIMEOUT is also set"
"--turn-timeout / BUZZ_ACP_TURN_TIMEOUT is deprecated and ignored \
when --idle-timeout / BUZZ_ACP_IDLE_TIMEOUT is also set"
);
idle
}
(Some(idle), None) => idle,
(None, Some(turn)) => {
tracing::warn!(
"--turn-timeout / SPROUT_ACP_TURN_TIMEOUT is deprecated; \
use --idle-timeout / SPROUT_ACP_IDLE_TIMEOUT instead"
"--turn-timeout / BUZZ_ACP_TURN_TIMEOUT is deprecated; \
use --idle-timeout / BUZZ_ACP_IDLE_TIMEOUT instead"
);
turn
}
@@ -743,7 +743,7 @@ impl Config {
let (persona_system_prompt, persona_model, persona_env_vars) =
match (&args.persona_pack, &args.persona_name) {
(Some(pack_dir), Some(name)) => {
let pack = sprout_persona::resolve::resolve_pack(pack_dir).map_err(|e| {
let pack = buzz_persona::resolve::resolve_pack(pack_dir).map_err(|e| {
ConfigError::ConfigFile(format!(
"failed to resolve pack {}: {e}",
pack_dir.display()
@@ -966,7 +966,7 @@ pub fn resolve_channel_filters(
discovered_channels: &[Uuid],
rules: &[SubscriptionRule],
) -> HashMap<Uuid, ChannelFilter> {
use sprout_core::kind::{
use buzz_core::kind::{
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
};
@@ -1068,7 +1068,7 @@ pub fn resolve_dynamic_channel_filter(
channel_id: Uuid,
rules: &[crate::filter::SubscriptionRule],
) -> Option<ChannelFilter> {
use sprout_core::kind::{
use buzz_core::kind::{
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
};
@@ -1183,7 +1183,7 @@ mod tests {
kinds_override: None,
channels_override: None,
no_mention_filter: false,
config_path: PathBuf::from("./sprout-acp.toml"),
config_path: PathBuf::from("./buzz-acp.toml"),
context_message_limit: 12,
max_turns_per_session: 0,
presence_enabled: true,
@@ -1234,9 +1234,9 @@ mod tests {
let f = result.get(ch).expect("channel should be present");
assert!(f.require_mention, "mentions mode requires mention");
let kinds = f.kinds.as_ref().expect("should have kinds");
assert!(kinds.contains(&sprout_core::kind::KIND_STREAM_MESSAGE));
assert!(kinds.contains(&sprout_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED));
assert!(kinds.contains(&sprout_core::kind::KIND_STREAM_REMINDER));
assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE));
assert!(kinds.contains(&buzz_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED));
assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_REMINDER));
}
}
@@ -1309,13 +1309,13 @@ mod tests {
}
#[test]
fn normalizes_sprout_agent_args_to_empty() {
fn normalizes_buzz_agent_args_to_empty() {
assert_eq!(
normalize_agent_args("sprout-agent", Vec::new()),
normalize_agent_args("buzz-agent", Vec::new()),
Vec::<String>::new()
);
assert_eq!(
normalize_agent_args("sprout-agent", vec!["acp".into()]),
normalize_agent_args("buzz-agent", vec!["acp".into()]),
Vec::<String>::new()
);
}
@@ -1556,7 +1556,7 @@ mod tests {
#[test]
fn test_load_rules_valid_toml() {
let dir = std::env::temp_dir().join("sprout-acp-test-valid");
let dir = std::env::temp_dir().join("buzz-acp-test-valid");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
@@ -1579,7 +1579,7 @@ require_mention = false
#[test]
fn test_load_rules_empty_name_rejected() {
let dir = std::env::temp_dir().join("sprout-acp-test-empty-name");
let dir = std::env::temp_dir().join("buzz-acp-test-empty-name");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
@@ -1599,7 +1599,7 @@ channels = "all"
#[test]
fn test_load_rules_duplicate_name_rejected() {
let dir = std::env::temp_dir().join("sprout-acp-test-dup-name");
let dir = std::env::temp_dir().join("buzz-acp-test-dup-name");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
@@ -1623,7 +1623,7 @@ channels = "all"
#[test]
fn test_load_rules_invalid_filter_rejected() {
let dir = std::env::temp_dir().join("sprout-acp-test-bad-filter");
let dir = std::env::temp_dir().join("buzz-acp-test-bad-filter");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
// evalexpr rejects unbalanced parens at parse time.
@@ -1645,7 +1645,7 @@ filter = "((("
#[test]
fn test_load_rules_channel_scope_typo_rejected() {
let dir = std::env::temp_dir().join("sprout-acp-test-scope-typo");
let dir = std::env::temp_dir().join("buzz-acp-test-scope-typo");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
@@ -1665,7 +1665,7 @@ channels = "ALL"
#[test]
fn test_load_rules_too_many_rules_rejected() {
let dir = std::env::temp_dir().join("sprout-acp-test-too-many");
let dir = std::env::temp_dir().join("buzz-acp-test-too-many");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
let mut toml = String::new();
@@ -1683,7 +1683,7 @@ channels = "ALL"
#[test]
fn test_load_rules_filter_too_long_rejected() {
let dir = std::env::temp_dir().join("sprout-acp-test-long-filter");
let dir = std::env::temp_dir().join("buzz-acp-test-long-filter");
let path = dir.join("rules.toml");
std::fs::create_dir_all(&dir).unwrap();
let long_expr = format!("\"{}\"", "a".repeat(4097));
@@ -1890,7 +1890,7 @@ channels = "ALL"
#[test]
fn test_permission_mode_value_enum_camel_case_aliases() {
// Operators may set env vars using the camelCase wire-format strings
// (e.g. SPROUT_ACP_PERMISSION_MODE=bypassPermissions). The #[value(alias)]
// (e.g. BUZZ_ACP_PERMISSION_MODE=bypassPermissions). The #[value(alias)]
// attributes ensure these parse correctly.
use clap::ValueEnum;
let cases = [
@@ -11,9 +11,9 @@
//! overwrite real, just-unreachable memory with a fresh profile.
//! - Either way, session creation is never blocked.
use buzz_core::engram::{conversation_key, d_tag, select_head, validate_and_decrypt, Body};
use buzz_core::kind::KIND_AGENT_ENGRAM;
use nostr::{Event, Keys, PublicKey};
use sprout_core::engram::{conversation_key, d_tag, select_head, validate_and_decrypt, Body};
use sprout_core::kind::KIND_AGENT_ENGRAM;
use crate::relay::RestClient;
@@ -22,10 +22,10 @@ const SECTION_LABEL: &str = "Agent Memory — core";
/// Onboarding nudge for new agents with no core yet.
///
/// Wording is from Tyler's brief: "No core memory found. Use `sprout mem`
/// Wording is from Tyler's brief: "No core memory found. Use `buzz mem`
/// to create a core memory. Ask your user about yourself."
pub const ONBOARDING_NUDGE: &str = "No core memory found. \
Use `sprout mem set core \"…\"` to create one (it will hold your identity, \
Use `buzz mem set core \"…\"` to create one (it will hold your identity, \
rules, and goals across sessions). Ask your user about yourself.";
/// Build the rendered prompt section for the agent's core.
@@ -68,7 +68,7 @@ async fn fetch_core_body(
owner: &PublicKey,
) -> Result<Option<String>, String> {
let k_c = conversation_key(agent_keys.secret_key(), owner);
let d = d_tag(&k_c, sprout_core::engram::CORE_SLUG);
let d = d_tag(&k_c, buzz_core::engram::CORE_SLUG);
let filter = nostr::Filter::new()
.kind(nostr::Kind::Custom(KIND_AGENT_ENGRAM as u16))
@@ -166,8 +166,8 @@ fn decode_core_body(
#[cfg(test)]
mod tests {
use super::*;
use buzz_core::engram::{build_event, Body};
use serde_json::json;
use sprout_core::engram::{build_event, Body};
/// Empty array → confirmed absence → Ok(None), so the caller emits the
/// onboarding nudge. This is the only path that maps to "no core."
@@ -203,7 +203,7 @@ static FILTER_EVAL_SEMAPHORE: std::sync::LazyLock<Arc<tokio::sync::Semaphore>> =
/// `node.eval_boolean_with_context()` instead of re-parsing the expression
/// string on every call (finding #34).
/// - Registers custom string helpers: `str_contains`, `str_starts_with`,
/// `str_ends_with`, `str_len` (duplicated intentionally from sprout-workflow).
/// `str_ends_with`, `str_len` (duplicated intentionally from buzz-workflow).
pub async fn evaluate_filter(
expr: &str,
ctx: &FilterContext,
@@ -269,8 +269,8 @@ pub async fn evaluate_filter(
/// | `timestamp` | int | `event.created_at` |
///
/// Also registers `str_contains`, `str_starts_with`, `str_ends_with`,
/// `str_len` — duplicated from sprout-workflow intentionally so this crate
/// has no runtime dependency on sprout-workflow.
/// `str_len` — duplicated from buzz-workflow intentionally so this crate
/// has no runtime dependency on buzz-workflow.
fn build_eval_context(ctx: &FilterContext) -> Result<evalexpr::HashMapContext, String> {
use evalexpr::*;
@@ -15,6 +15,13 @@ use std::time::Duration;
use acp::{AcpClient, EnvVar, McpServer};
use anyhow::Result;
use buzz_core::kind::{
KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_STREAM_MESSAGE,
KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
};
use buzz_core::observer::{
decrypt_observer_payload, encrypt_observer_payload, OBSERVER_FRAME_TELEMETRY,
};
use clap::Parser;
use config::{Config, DedupMode, ModelsArgs, MultipleEventHandling, RespondTo, SubscribeMode};
use filter::SubscriptionRule;
@@ -26,13 +33,6 @@ use pool::{
};
use queue::{prepend_base_prompt, EventQueue, QueuedEvent, ThreadTags};
use relay::{HarnessRelay, RelayEventPublisher};
use sprout_core::kind::{
KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_STREAM_MESSAGE,
KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
};
use sprout_core::observer::{
decrypt_observer_payload, encrypt_observer_payload, OBSERVER_FRAME_TELEMETRY,
};
use tokio::sync::{mpsc, watch};
use tracing_subscriber::EnvFilter;
use uuid::Uuid;
@@ -46,12 +46,12 @@ use uuid::Uuid;
/// `ModelsArgs` parser; the default path uses the existing `CliArgs`.
///
/// **Constraint**: subcommand must be argv[1] — flags before the subcommand
/// name (e.g., `sprout-acp --verbose models`) are not supported.
/// name (e.g., `buzz-acp --verbose models`) are not supported.
fn is_subcommand(name: &str) -> bool {
std::env::args().nth(1).map(|a| a == name).unwrap_or(false)
}
/// Timeout for the `sprout-acp models` subcommand (spawn + init + session/new).
/// Timeout for the `buzz-acp models` subcommand (spawn + init + session/new).
const MODELS_TIMEOUT: Duration = Duration::from_secs(10);
// ── Presence helper ───────────────────────────────────────────────────────────
@@ -69,8 +69,8 @@ async fn publish_presence(
keys: &nostr::Keys,
status: &str,
) -> Result<(), relay::RelayError> {
use buzz_core::kind::KIND_PRESENCE_UPDATE;
use nostr::{EventBuilder, Kind};
use sprout_core::kind::KIND_PRESENCE_UPDATE;
let event = EventBuilder::new(Kind::Custom(KIND_PRESENCE_UPDATE as u16), status)
.tags([])
@@ -85,22 +85,22 @@ async fn publish_presence(
/// Resolve the agent's owner pubkey at startup.
///
/// Priority:
/// 1. `SPROUT_AUTH_TAG` env var — NIP-OA attestation signed by the owner.
/// 1. `BUZZ_AUTH_TAG` env var — NIP-OA attestation signed by the owner.
/// Verified against the agent's own pubkey to extract the owner pubkey.
/// 2. `--agent-owner` CLI flag / `SPROUT_ACP_AGENT_OWNER` env var.
/// 2. `--agent-owner` CLI flag / `BUZZ_ACP_AGENT_OWNER` env var.
fn resolve_agent_owner(config: &Config) -> Option<String> {
// Try SPROUT_AUTH_TAG first (NIP-OA attestation).
if let Ok(auth_tag) = std::env::var("SPROUT_AUTH_TAG") {
// Try BUZZ_AUTH_TAG first (NIP-OA attestation).
if let Ok(auth_tag) = std::env::var("BUZZ_AUTH_TAG") {
if !auth_tag.is_empty() {
let agent_pk = config.keys.public_key();
match sprout_sdk::nip_oa::verify_auth_tag(&auth_tag, &agent_pk) {
match buzz_sdk::nip_oa::verify_auth_tag(&auth_tag, &agent_pk) {
Ok(owner_pk) => {
let owner_hex = owner_pk.to_hex().to_ascii_lowercase();
tracing::info!("owner resolved from SPROUT_AUTH_TAG: {owner_hex}");
tracing::info!("owner resolved from BUZZ_AUTH_TAG: {owner_hex}");
return Some(owner_hex);
}
Err(e) => {
tracing::warn!("SPROUT_AUTH_TAG verification failed: {e} — falling back");
tracing::warn!("BUZZ_AUTH_TAG verification failed: {e} — falling back");
}
}
}
@@ -246,7 +246,7 @@ async fn check_sibling_via_profile(
}
// Cryptographically verify the NIP-OA attestation signature.
let tag_json = serde_json::to_string(tag).unwrap_or_default();
match sprout_sdk::nip_oa::verify_auth_tag(&tag_json, &agent_pk) {
match buzz_sdk::nip_oa::verify_auth_tag(&tag_json, &agent_pk) {
Ok(_) => {
tracing::debug!(author, expected_owner, "sibling verified via NIP-OA");
return true;
@@ -455,7 +455,7 @@ async fn publish_relay_observer_event(
return;
}
};
let builder = match sprout_sdk::build_agent_observer_frame(
let builder = match buzz_sdk::build_agent_observer_frame(
owner_pubkey_hex,
agent_pubkey_hex,
OBSERVER_FRAME_TELEMETRY,
@@ -490,7 +490,7 @@ fn handle_relay_observer_control_event(
owner_pubkey_hex: &str,
) {
// Defense-in-depth: verify signature even though the relay already checked.
if let Err(e) = sprout_core::verify_event(&event) {
if let Err(e) = buzz_core::verify_event(&event) {
tracing::warn!(error = %e, "observer control frame failed signature verification");
return;
}
@@ -789,13 +789,13 @@ async fn tokio_main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("sprout_acp=info")),
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("buzz_acp=info")),
)
.compact()
.init();
let mut config = Config::from_cli().map_err(|e| anyhow::anyhow!("configuration error: {e}"))?;
tracing::info!("sprout-acp starting: {}", config.summary());
tracing::info!("buzz-acp starting: {}", config.summary());
let observer = config
.relay_observer
@@ -889,7 +889,7 @@ async fn tokio_main() -> Result<()> {
tracing::info!("agent_pool_ready agents={}", live_count);
let mut pool = AgentPool::from_slots(agent_slots);
// ── Step 2: Connect to Sprout relay ──────────────────────────────────────
// ── Step 2: Connect to Buzz relay ──────────────────────────────────────
//
// Finding #22: capture a startup watermark BEFORE connecting to the relay.
// This timestamp is used for membership notification replay (via
@@ -904,11 +904,11 @@ async fn tokio_main() -> Result<()> {
let pubkey_hex = config.keys.public_key().to_hex();
// Parse SPROUT_AUTH_TAG into a nostr::Tag for NIP-OA relay membership delegation.
let relay_auth_tag: Option<nostr::Tag> = std::env::var("SPROUT_AUTH_TAG")
// Parse BUZZ_AUTH_TAG into a nostr::Tag for NIP-OA relay membership delegation.
let relay_auth_tag: Option<nostr::Tag> = std::env::var("BUZZ_AUTH_TAG")
.ok()
.filter(|s| !s.is_empty())
.and_then(|s| sprout_sdk::nip_oa::parse_auth_tag(&s).ok());
.and_then(|s| buzz_sdk::nip_oa::parse_auth_tag(&s).ok());
let mut relay =
HarnessRelay::connect(&config.relay_url, &config.keys, &pubkey_hex, relay_auth_tag)
@@ -943,7 +943,7 @@ async fn tokio_main() -> Result<()> {
}
// ── Step 2d: Resolve agent owner ────────────────────────────────────────
// Priority: SPROUT_AUTH_TAG (NIP-OA attestation) → --agent-owner flag.
// Priority: BUZZ_AUTH_TAG (NIP-OA attestation) → --agent-owner flag.
let startup_owner: Option<String> = resolve_agent_owner(&config);
if let Some(ref owner) = startup_owner {
tracing::info!("agent owner: {owner}");
@@ -956,7 +956,7 @@ async fn tokio_main() -> Result<()> {
RespondTo::OwnerOnly => {
tracing::warn!(
"respond-to=owner-only but no owner is set — all events will be \
dropped. Set SPROUT_AUTH_TAG or --agent-owner, or use --respond-to=anyone."
dropped. Set BUZZ_AUTH_TAG or --agent-owner, or use --respond-to=anyone."
);
}
RespondTo::Allowlist => {
@@ -1104,7 +1104,7 @@ async fn tokio_main() -> Result<()> {
if !config.memory_enabled {
tracing::info!(
target: "engram::core",
"NIP-AE core memory injection disabled (re-enable by removing --no-memory / SPROUT_ACP_NO_MEMORY)"
"NIP-AE core memory injection disabled (re-enable by removing --no-memory / BUZZ_ACP_NO_MEMORY)"
);
}
@@ -1349,19 +1349,19 @@ async fn tokio_main() -> Result<()> {
None
}
// Remaining branches don't touch pool — evaluated when pool is idle.
sprout_event = relay.next_event() => {
buzz_event = relay.next_event() => {
let _ = result_rx; // end split borrow before relay handling
match sprout_event {
Some(sprout_event) => {
let kind_u32 = sprout_event.event.kind.as_u16() as u32;
match buzz_event {
Some(buzz_event) => {
let kind_u32 = buzz_event.event.kind.as_u16() as u32;
// ── Membership notification handling ──────────────
if kind_u32 == KIND_MEMBER_ADDED_NOTIFICATION
|| kind_u32 == KIND_MEMBER_REMOVED_NOTIFICATION
{
let ch = sprout_event.channel_id;
let ts = sprout_event.event.created_at.as_secs();
let eid = sprout_event.event.id.to_hex();
let ch = buzz_event.channel_id;
let ts = buzz_event.event.created_at.as_secs();
let eid = buzz_event.event.id.to_hex();
// Two-layer membership dedup:
//
@@ -1464,26 +1464,26 @@ async fn tokio_main() -> Result<()> {
}
// ── End membership notification handling ──────────
if config.ignore_self && sprout_event.event.pubkey.to_hex() == pubkey_hex {
tracing::debug!(channel_id = %sprout_event.channel_id, "dropping self-authored event");
if config.ignore_self && buzz_event.event.pubkey.to_hex() == pubkey_hex {
tracing::debug!(channel_id = %buzz_event.channel_id, "dropping self-authored event");
continue;
}
// ── Shutdown command handling ─────────────────────
// Check: kind:9, content "!shutdown", from owner, mentions THIS agent.
let is_shutdown = kind_u32 == KIND_STREAM_MESSAGE
&& sprout_event.event.content.trim() == "!shutdown"
&& sprout_event.event.tags.iter().any(|t| {
&& buzz_event.event.content.trim() == "!shutdown"
&& buzz_event.event.tags.iter().any(|t| {
t.as_slice().first().map(|s| s.as_str()) == Some("p")
&& t.as_slice().get(1).map(|s| s.as_str()) == Some(pubkey_hex.as_str())
});
if is_shutdown {
let owner = owner_cache.get();
if let Some(owner) = owner {
if sprout_event.event.pubkey.to_hex() == *owner {
if buzz_event.event.pubkey.to_hex() == *owner {
tracing::info!(
channel_id = %sprout_event.channel_id,
sender = %sprout_event.event.pubkey.to_hex(),
channel_id = %buzz_event.channel_id,
sender = %buzz_event.event.pubkey.to_hex(),
"shutdown command from owner — exiting gracefully"
);
let _ = shutdown_tx.send(());
@@ -1505,18 +1505,18 @@ async fn tokio_main() -> Result<()> {
// --multiple-event-handling. It is explicit user
// intent, not an automatic policy decision.
let is_cancel = kind_u32 == KIND_STREAM_MESSAGE
&& sprout_event.event.content.trim() == "!cancel"
&& sprout_event.event.tags.iter().any(|t| {
&& buzz_event.event.content.trim() == "!cancel"
&& buzz_event.event.tags.iter().any(|t| {
t.as_slice().first().map(|s| s.as_str()) == Some("p")
&& t.as_slice().get(1).map(|s| s.as_str()) == Some(pubkey_hex.as_str())
});
if is_cancel {
if let Some(owner) = owner_cache.get() {
if sprout_event.event.pubkey.to_hex() == *owner {
let fired = cancel_in_flight_task(&mut pool, sprout_event.channel_id, CancelMode::Stop);
if buzz_event.event.pubkey.to_hex() == *owner {
let fired = cancel_in_flight_task(&mut pool, buzz_event.channel_id, CancelMode::Stop);
if !fired {
tracing::warn!(
channel_id = %sprout_event.channel_id,
channel_id = %buzz_event.channel_id,
"!cancel received but no in-flight task — no-op"
);
}
@@ -1539,7 +1539,7 @@ async fn tokio_main() -> Result<()> {
// same human). Allowlist is unchanged: owner +
// explicit pubkey list only.
{
let author = sprout_event.event.pubkey.to_hex();
let author = buzz_event.event.pubkey.to_hex();
let allowed = match &config.respond_to {
RespondTo::Anyone => true,
RespondTo::Nobody => false,
@@ -1554,8 +1554,8 @@ async fn tokio_main() -> Result<()> {
};
if !allowed {
tracing::debug!(
channel_id = %sprout_event.channel_id,
author = %sprout_event.event.pubkey.to_hex(),
channel_id = %buzz_event.channel_id,
author = %buzz_event.event.pubkey.to_hex(),
mode = %config.respond_to,
"inbound author gate — dropping event"
);
@@ -1564,21 +1564,21 @@ async fn tokio_main() -> Result<()> {
}
// ── End inbound author gate ──────────────────────
let matched = filter::match_event(&sprout_event.event, sprout_event.channel_id, &rules, &pubkey_hex).await;
let matched = filter::match_event(&buzz_event.event, buzz_event.channel_id, &rules, &pubkey_hex).await;
let prompt_tag = match matched {
Some(m) => m.prompt_tag,
None => {
tracing::debug!(channel_id = %sprout_event.channel_id, kind = sprout_event.event.kind.as_u16(), "event matched no rule — dropping");
tracing::debug!(channel_id = %buzz_event.channel_id, kind = buzz_event.event.kind.as_u16(), "event matched no rule — dropping");
continue;
}
};
// Capture author pubkey before queue.push() moves
// sprout_event.event (needed for mode gate below).
let author_hex = sprout_event.event.pubkey.to_hex();
let event_id_hex = sprout_event.event.id.to_hex();
// buzz_event.event (needed for mode gate below).
let author_hex = buzz_event.event.pubkey.to_hex();
let event_id_hex = buzz_event.event.id.to_hex();
let accepted = queue.push(QueuedEvent {
channel_id: sprout_event.channel_id,
event: sprout_event.event,
channel_id: buzz_event.channel_id,
event: buzz_event.event,
received_at: std::time::Instant::now(),
prompt_tag,
});
@@ -1596,7 +1596,7 @@ async fn tokio_main() -> Result<()> {
// ── Multiple-event-handling mode gate ─────────────
// Event is already queued. If mode requires it AND
// the channel has an in-flight task, fire cancel.
if accepted && queue.is_channel_in_flight(sprout_event.channel_id) {
if accepted && queue.is_channel_in_flight(buzz_event.channel_id) {
let should_cancel = match config.multiple_event_handling {
MultipleEventHandling::Queue => false,
MultipleEventHandling::Interrupt => true,
@@ -1608,7 +1608,7 @@ async fn tokio_main() -> Result<()> {
}
};
if should_cancel {
cancel_in_flight_task(&mut pool, sprout_event.channel_id, CancelMode::Interrupt);
cancel_in_flight_task(&mut pool, buzz_event.channel_id, CancelMode::Interrupt);
}
}
// ── End mode gate ────────────────────────────────
@@ -1865,7 +1865,7 @@ async fn tokio_main() -> Result<()> {
// for the background task to finish, rather than aborting immediately (#40).
relay.shutdown().await;
tracing::info!("sprout-acp stopped");
tracing::info!("buzz-acp stopped");
Ok(())
}
@@ -2384,14 +2384,14 @@ fn default_heartbeat_prompt() -> String {
You have been awakened for a routine heartbeat. You have NO incoming messages or\n\
active channel context for this turn.\n\n\
Your tasks:\n\
1. Run `sprout feed get --types needs_action` to check for pending workflow approvals or\n\
1. Run `buzz feed get --types needs_action` to check for pending workflow approvals or\n\
high-priority requests addressed to you.\n\
2. Run `sprout feed get --types mentions` to check for unanswered @mentions.\n\
2. Run `buzz feed get --types mentions` to check for unanswered @mentions.\n\
3. If you find actionable items, address them using the appropriate CLI commands\n\
(e.g., `sprout workflows approve --token <UUID>`, `sprout messages send`,\n\
`sprout messages send --reply-to <event-id>`).\n\
(e.g., `buzz workflows approve --token <UUID>`, `buzz messages send`,\n\
`buzz messages send --reply-to <event-id>`).\n\
4. If there are no pending actions or mentions, end your turn immediately.\n\n\
Do not run `sprout channels list` or `sprout messages search` unless you have a specific reason.\n\
Do not run `buzz channels list` or `buzz messages search` unless you have a specific reason.\n\
Do not invent work — only act on items surfaced by the feed commands."
)
}
@@ -2499,7 +2499,7 @@ async fn spawn_and_init(
// ── run_models ─────────────────────────────────────────────────────────────────
/// `sprout-acp models` — spawn an agent, query its available models, exit.
/// `buzz-acp models` — spawn an agent, query its available models, exit.
///
/// Flow: spawn → initialize → session/new → print models → shutdown.
/// No relay connection, no MCP servers, no subscriptions. ~2-5s total.
@@ -2653,11 +2653,11 @@ fn build_mcp_servers(config: &Config) -> Vec<McpServer> {
env: {
let mut env = vec![
EnvVar {
name: "SPROUT_RELAY_URL".into(),
name: "BUZZ_RELAY_URL".into(),
value: config.relay_url.clone(),
},
EnvVar {
name: "SPROUT_PRIVATE_KEY".into(),
name: "BUZZ_PRIVATE_KEY".into(),
// bech32 encoding of a valid secret key is infallible.
// Panic here is correct: injecting a bogus secret would cause
// delayed, hard-to-diagnose agent failures downstream.
@@ -2668,12 +2668,12 @@ fn build_mcp_servers(config: &Config) -> Vec<McpServer> {
.expect("secret key bech32 encoding should never fail"),
},
];
// Forward SPROUT_AUTH_TAG (NIP-OA owner attestation credential)
// Forward BUZZ_AUTH_TAG (NIP-OA owner attestation credential)
// so the MCP server can attach it to every signed event.
if let Ok(auth_tag) = std::env::var("SPROUT_AUTH_TAG") {
if let Ok(auth_tag) = std::env::var("BUZZ_AUTH_TAG") {
if !auth_tag.is_empty() {
env.push(EnvVar {
name: "SPROUT_AUTH_TAG".into(),
name: "BUZZ_AUTH_TAG".into(),
value: auth_tag,
});
}
@@ -2833,7 +2833,7 @@ mod build_mcp_servers_tests {
kinds_override: None,
channels_override: None,
no_mention_filter: false,
config_path: std::path::PathBuf::from("./sprout-acp.toml"),
config_path: std::path::PathBuf::from("./buzz-acp.toml"),
context_message_limit: 12,
max_turns_per_session: 0,
presence_enabled: true,
@@ -2861,46 +2861,43 @@ mod build_mcp_servers_tests {
let names: Vec<&str> = server.env.iter().map(|e| e.name.as_str()).collect();
assert!(
names.contains(&"SPROUT_RELAY_URL"),
"missing SPROUT_RELAY_URL; got {names:?}"
names.contains(&"BUZZ_RELAY_URL"),
"missing BUZZ_RELAY_URL; got {names:?}"
);
assert!(
names.contains(&"SPROUT_PRIVATE_KEY"),
"missing SPROUT_PRIVATE_KEY; got {names:?}"
names.contains(&"BUZZ_PRIVATE_KEY"),
"missing BUZZ_PRIVATE_KEY; got {names:?}"
);
}
#[test]
fn session_new_mcp_server_forwards_sprout_auth_tag() {
fn session_new_mcp_server_forwards_buzz_auth_tag() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("SPROUT_AUTH_TAG", "test-attestation-tag");
std::env::set_var("BUZZ_AUTH_TAG", "test-attestation-tag");
let config = test_config();
let servers = build_mcp_servers(&config);
std::env::remove_var("SPROUT_AUTH_TAG");
std::env::remove_var("BUZZ_AUTH_TAG");
let server = &servers[0];
let auth_tag_env = server.env.iter().find(|e| e.name == "SPROUT_AUTH_TAG");
let auth_tag_env = server.env.iter().find(|e| e.name == "BUZZ_AUTH_TAG");
assert!(
auth_tag_env.is_some(),
"SPROUT_AUTH_TAG should be forwarded when set"
"BUZZ_AUTH_TAG should be forwarded when set"
);
assert_eq!(auth_tag_env.unwrap().value, "test-attestation-tag");
}
#[test]
fn session_new_mcp_server_skips_empty_sprout_auth_tag() {
fn session_new_mcp_server_skips_empty_buzz_auth_tag() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("SPROUT_AUTH_TAG", "");
std::env::set_var("BUZZ_AUTH_TAG", "");
let config = test_config();
let servers = build_mcp_servers(&config);
std::env::remove_var("SPROUT_AUTH_TAG");
std::env::remove_var("BUZZ_AUTH_TAG");
let server = &servers[0];
let has_auth_tag = server.env.iter().any(|e| e.name == "SPROUT_AUTH_TAG");
assert!(
!has_auth_tag,
"empty SPROUT_AUTH_TAG should not be forwarded"
);
let has_auth_tag = server.env.iter().any(|e| e.name == "BUZZ_AUTH_TAG");
assert!(!has_auth_tag, "empty BUZZ_AUTH_TAG should not be forwarded");
}
#[test]
@@ -1,3 +1,3 @@
fn main() -> anyhow::Result<()> {
sprout_acp::run()
buzz_acp::run()
}
@@ -20,7 +20,7 @@ const OBSERVER_BUFFER_CAP: usize = 1_000;
/// Best-effort metadata attached to observer events.
#[derive(Clone, Debug, Default)]
pub struct ObserverContext {
/// Sprout channel UUID for the current turn, when channel-scoped.
/// Buzz channel UUID for the current turn, when channel-scoped.
pub channel_id: Option<String>,
/// ACP session ID associated with the current turn, once known.
pub session_id: Option<String>,
@@ -63,7 +63,7 @@ pub struct ObserverEvent {
pub kind: String,
/// Pool slot index for the agent process that emitted the event.
pub agent_index: Option<usize>,
/// Sprout channel UUID for channel-scoped events.
/// Buzz channel UUID for channel-scoped events.
pub channel_id: Option<String>,
/// ACP session ID when known.
pub session_id: Option<String>,
@@ -220,7 +220,7 @@ pub struct PromptContext {
/// the per-session core engram fetch is skipped and `core_sections`
/// remains empty for every channel, so `format_prompt` renders no
/// `[Agent Memory — core]` section. On by default; disabled via
/// `--no-memory` / `SPROUT_ACP_NO_MEMORY`.
/// `--no-memory` / `BUZZ_ACP_NO_MEMORY`.
pub memory_enabled: bool,
}
@@ -760,11 +760,11 @@ pub async fn run_prompt_task(
// happens when a session is invalidated and recreated (see
// `SessionState::invalidate_channel`).
//
// Operator opt-out: `--no-memory` / `SPROUT_ACP_NO_MEMORY` disables the
// Operator opt-out: `--no-memory` / `BUZZ_ACP_NO_MEMORY` disables the
// NIP-AE injection path. By default we run the fetch and populate
// `state.core_sections`, so `format_prompt` renders the core section.
// When disabled we skip the fetch outright and leave `core_sections`
// empty. The `sprout mem` CLI and the relay's acceptance of
// empty. The `buzz mem` CLI and the relay's acceptance of
// kind:30174 engrams are unaffected.
if is_new_session && ctx.memory_enabled {
if let (PromptSource::Channel(cid), Some(owner_pk)) =
@@ -921,7 +921,7 @@ pub async fn run_prompt_task(
// When the batch is a single slash-command message (e.g. "@Eva /goal …"),
// `slash_command` holds the bare command. It is sent as the FIRST prompt
// content block so ACP connectors' slash-command detection
// (`prompt[0].text.startsWith("/")`) fires; the wrapped Sprout context
// (`prompt[0].text.startsWith("/")`) fires; the wrapped Buzz context
// follows as a second block.
let mut slash_command: Option<String> = None;
let prompt_text = if let Some(text) = prompt_text {
@@ -1002,7 +1002,7 @@ pub async fn run_prompt_task(
// ── Send the actual prompt ────────────────────────────────────────────
// Slash-command pass-through sends two text blocks: the bare command
// first (so connector detection fires), then the wrapped Sprout context.
// first (so connector detection fires), then the wrapped Buzz context.
let prompt_blocks: Vec<&str> = match slash_command {
Some(ref cmd) => vec![cmd.as_str(), prompt_text.as_str()],
None => vec![prompt_text.as_str()],
@@ -1309,7 +1309,7 @@ async fn fetch_channel_info(channel_id: Uuid, rest: &RestClient) -> Option<Promp
let d_tag = SingleLetterTag::lowercase(Alphabet::D);
let filter = nostr::Filter::new()
.kind(nostr::Kind::Custom(
sprout_core::kind::KIND_NIP29_GROUP_METADATA as u16,
buzz_core::kind::KIND_NIP29_GROUP_METADATA as u16,
))
.custom_tags(d_tag, [channel_id.to_string()]);
@@ -1564,8 +1564,8 @@ async fn fetch_thread_context(
let root_filter = nostr::Filter::new().id(nostr::EventId::from_hex(root_event_id).ok()?);
let replies_filter = nostr::Filter::new()
.kinds([
nostr::Kind::Custom(sprout_core::kind::KIND_STREAM_MESSAGE as u16),
nostr::Kind::Custom(sprout_core::kind::KIND_STREAM_MESSAGE_V2 as u16),
nostr::Kind::Custom(buzz_core::kind::KIND_STREAM_MESSAGE as u16),
nostr::Kind::Custom(buzz_core::kind::KIND_STREAM_MESSAGE_V2 as u16),
])
.custom_tags(e_tag, [root_event_id])
.custom_tags(h_tag, [ch_str.as_str()])
@@ -1612,8 +1612,8 @@ async fn fetch_dm_context(
let ch_str = channel_id.to_string();
let filter = nostr::Filter::new()
.kinds([
nostr::Kind::Custom(sprout_core::kind::KIND_STREAM_MESSAGE as u16),
nostr::Kind::Custom(sprout_core::kind::KIND_STREAM_MESSAGE_V2 as u16),
nostr::Kind::Custom(buzz_core::kind::KIND_STREAM_MESSAGE as u16),
nostr::Kind::Custom(buzz_core::kind::KIND_STREAM_MESSAGE_V2 as u16),
])
.custom_tags(h_tag, [ch_str.as_str()])
.limit(limit as usize);
@@ -1955,7 +1955,7 @@ fn pct_encode(s: &str) -> String {
/// Best-effort: add a reaction via a signed Nostr kind-7 event (NIP-25).
///
/// Builds a reaction event with `sprout_sdk::build_reaction`, signs it with
/// Builds a reaction event with `buzz_sdk::build_reaction`, signs it with
/// the keys already stored in `RestClient`, and submits via `POST /events`.
/// Returns immediately on timeout or any error — reactions are cosmetic.
pub(crate) async fn reaction_add(rest: &crate::relay::RestClient, event_id: &str, emoji: &str) {
@@ -1966,7 +1966,7 @@ pub(crate) async fn reaction_add(rest: &crate::relay::RestClient, event_id: &str
return;
}
};
let builder = match sprout_sdk::build_reaction(target_id, emoji) {
let builder = match buzz_sdk::build_reaction(target_id, emoji) {
Ok(b) => b,
Err(e) => {
tracing::warn!(event_id, emoji, "reaction add: build failed: {e}");
@@ -2047,7 +2047,7 @@ pub(crate) async fn reaction_remove(rest: &crate::relay::RestClient, event_id: &
return;
}
};
let builder = match sprout_sdk::build_remove_reaction(target_id) {
let builder = match buzz_sdk::build_remove_reaction(target_id) {
Ok(b) => b,
Err(e) => {
tracing::warn!(event_id, emoji, "reaction remove: build failed: {e}");
@@ -1,4 +1,4 @@
//! Event queue state machine for sprout-acp.
//! Event queue state machine for buzz-acp.
//!
//! Manages per-channel event queues with per-channel in-flight tracking.
//! When the harness is ready to prompt the agent, it flushes the channel with
@@ -587,7 +587,7 @@ pub struct ThreadTags {
///
/// NOTE: Only handles NIP-10 marker-based format (preferred). The deprecated
/// positional format (no markers, `["e", id, relay_url]`) is not supported —
/// Sprout always generates marker-based tags (see relay messages.rs:762-783).
/// Buzz always generates marker-based tags (see relay messages.rs:762-783).
pub fn parse_thread_tags(event: &Event) -> ThreadTags {
let mut root = None;
let mut reply = None;
@@ -633,7 +633,7 @@ pub fn parse_thread_tags(event: &Event) -> ThreadTags {
/// Extract a leading slash command from message content.
///
/// ACP connectors (claude-agent-acp, codex-acp) detect slash commands by
/// checking whether the **first** prompt content block starts with `/`. Sprout
/// checking whether the **first** prompt content block starts with `/`. Buzz
/// users must @mention an agent to reach it, so the wire content is typically
/// `"@Eva /goal ship it"`. This strips leading mention tokens — `@word`,
/// multi-word display names from `known_names`, and NIP-27 `nostr:npub1…` /
@@ -661,7 +661,7 @@ pub fn extract_slash_command(content: &str, known_names: &[&str]) -> Option<Stri
} else if let Some(after_at) = rest.strip_prefix('@') {
// Known display names first (longest match wins, case-insensitive,
// must end at whitespace or end-of-string), then a single-word
// token of the characters Sprout allows in plain @mentions.
// token of the characters Buzz allows in plain @mentions.
let name_len = names
.iter()
.find_map(|name| {
@@ -880,13 +880,13 @@ fn format_event_block(
/// Append a reply instruction when the agent is responding to a thread event.
///
/// Tells the agent to pass `--reply-to <event_id>` on every `sprout messages
/// Tells the agent to pass `--reply-to <event_id>` on every `buzz messages
/// send` call in this turn, and not to broadcast to the channel so replies
/// stay inside the thread.
fn append_reply_instruction(s: &mut String, event_id: &str) {
s.push_str(&format!(
"\nIMPORTANT: When responding, use `--reply-to {event_id}` \
on EVERY `sprout messages send` call in this turn. \
on EVERY `buzz messages send` call in this turn. \
Do not broadcast to the channel."
));
}
@@ -912,13 +912,13 @@ fn format_context_hints(
// DM replies use thread command because /messages excludes thread replies.
// DM non-replies use get for recent conversation.
let ctx_hint = if has_conversation_context && is_reply {
"Thread context included below. Use `sprout messages thread --channel <UUID> --event <ID>` for full history if truncated."
"Thread context included below. Use `buzz messages thread --channel <UUID> --event <ID>` for full history if truncated."
} else if has_conversation_context {
"Conversation context included below. Use `sprout messages get --channel <UUID>` for full history if truncated."
"Conversation context included below. Use `buzz messages get --channel <UUID>` for full history if truncated."
} else if is_reply {
"Use `sprout messages thread --channel <UUID> --event <ID>` to fetch the reply chain."
"Use `buzz messages thread --channel <UUID> --event <ID>` to fetch the reply chain."
} else {
"Use `sprout messages get --channel <UUID>` for conversation context."
"Use `buzz messages get --channel <UUID>` for conversation context."
};
let mut s = format!(
"[Context]\n\
@@ -941,9 +941,9 @@ fn format_context_hints(
s
} else if let Some(ref root) = thread_tags.root_event_id {
let ctx_hint = if has_conversation_context {
"Thread context included below. Use `sprout messages thread --channel <UUID> --event <ID>` for full history if truncated."
"Thread context included below. Use `buzz messages thread --channel <UUID> --event <ID>` for full history if truncated."
} else {
"Use `sprout messages thread --channel <UUID> --event <ID>` to fetch thread context."
"Use `buzz messages thread --channel <UUID> --event <ID>` to fetch thread context."
};
let mut s = format!(
"[Context]\n\
@@ -966,7 +966,7 @@ fn format_context_hints(
"[Context]\n\
Scope: channel\n\
Channel: {channel_display}\n\
Hint: Use `sprout messages get --channel <UUID>` for recent messages if needed."
Hint: Use `buzz messages get --channel <UUID>` for recent messages if needed."
)
}
}
@@ -1033,7 +1033,7 @@ pub fn prepend_base_prompt(base: &str, body: &str) -> String {
/// 1. `[System]\n{system_prompt}` — if system prompt is set
/// 2. `[Context]` — scope, channel name, and contextual hints for the agent
/// 3. `[Thread Context]` or `[Conversation Context]` — if fetched
/// 4. `[Event]` / `[Sprout events]` — the triggering event(s)
/// 4. `[Event]` / `[Buzz events]` — the triggering event(s)
pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> String {
// Scope is always derived from the LAST event in the batch — that's the
// one the agent is responding to. Thread/DM context is supplementary info
@@ -1115,7 +1115,7 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> String
)
} else {
format!(
"[Sprout event: {}]\n{}",
"[Buzz event: {}]\n{}",
be.prompt_tag,
format_event_block(batch.channel_id, args.channel_info, be, args.profile_lookup)
)
@@ -1127,7 +1127,7 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> String
batch.events.len()
)
} else {
format!("[Sprout events — {} events]", batch.events.len())
format!("[Buzz events — {} events]", batch.events.len())
};
let mut s = header;
for (i, be) in batch.events.iter().enumerate() {
@@ -1371,7 +1371,7 @@ mod tests {
// Should contain [Context] section before the event.
assert!(prompt.contains("[Context]"));
assert!(prompt.contains("Scope: channel"));
assert!(prompt.contains("[Sprout event: @mention]\n"));
assert!(prompt.contains("[Buzz event: @mention]\n"));
assert!(prompt.contains(&format!("Channel: {}", ch)));
assert!(prompt.contains(&format!("From: {}", npub)));
assert!(prompt.contains("Content: Hello @agent"));
@@ -1465,7 +1465,7 @@ mod tests {
let prompt = format_prompt(&batch, &FormatPromptArgs::default());
assert!(prompt.contains("[Context]"));
assert!(prompt.contains("[Sprout events — 3 events]"));
assert!(prompt.contains("[Buzz events — 3 events]"));
assert!(prompt.contains("--- Event 1 (tag-a) ---"));
assert!(prompt.contains("--- Event 2 (tag-b) ---"));
assert!(prompt.contains("--- Event 3 (tag-c) ---"));
@@ -2447,8 +2447,8 @@ mod tests {
);
// Hint should point to the thread command, not get.
assert!(
prompt.contains("sprout messages thread"),
"DM reply hint should mention `sprout messages thread`, got:\n{prompt}"
prompt.contains("buzz messages thread"),
"DM reply hint should mention `buzz messages thread`, got:\n{prompt}"
);
// Thread structural info should be present.
assert!(
@@ -2487,12 +2487,12 @@ mod tests {
);
assert!(prompt.contains("Scope: dm"));
assert!(
prompt.contains("sprout messages get"),
"DM non-reply hint should mention `sprout messages get`"
prompt.contains("buzz messages get"),
"DM non-reply hint should mention `buzz messages get`"
);
assert!(
!prompt.contains("sprout messages thread"),
"DM non-reply should NOT mention `sprout messages thread`"
!prompt.contains("buzz messages thread"),
"DM non-reply should NOT mention `buzz messages thread`"
);
}
@@ -1,6 +1,6 @@
//! Harness-side Sprout relay client.
//! Harness-side Buzz relay client.
//!
//! Connects to the Sprout relay via NIP-01 WebSocket, authenticates via NIP-42,
//! Connects to the Buzz relay via NIP-01 WebSocket, authenticates via NIP-42,
//! discovers channels via REST API, and streams events back to the harness main
//! loop. Also publishes ephemeral events (typing indicators) via the same
//! WebSocket connection.
@@ -9,7 +9,7 @@
//!
//! A background tokio task owns the WebSocket stream. It:
//! - Responds to Ping frames with Pong (preventing relay disconnect on long turns)
//! - Forwards `SproutEvent`s through an `mpsc` channel
//! - Forwards `BuzzEvent`s through an `mpsc` channel
//! - Handles reconnection with `since` filters to avoid event loss
//! - Responds to mid-session AUTH challenges
//! - Publishes ephemeral events (typing indicators) via `PublishEvent` commands
@@ -23,7 +23,7 @@ use std::time::Duration;
// ─── Named constants ──────────────────────────────────────────────────────────
/// Default capacity of the event channel from background task to harness.
/// Override with `SPROUT_ACP_EVENT_BUFFER` env var at startup.
/// Override with `BUZZ_ACP_EVENT_BUFFER` env var at startup.
const EVENT_CHANNEL_CAPACITY_DEFAULT: usize = 256;
/// Capacity of the command channel from harness to background task.
const CMD_CHANNEL_CAPACITY: usize = 64;
@@ -31,7 +31,7 @@ const CMD_CHANNEL_CAPACITY: usize = 64;
/// Read the event channel capacity from the environment, falling back to the
/// compiled-in default. Parsed once at call-site (connect time).
fn event_channel_capacity() -> usize {
std::env::var("SPROUT_ACP_EVENT_BUFFER")
std::env::var("BUZZ_ACP_EVENT_BUFFER")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.map(|v| v.max(1)) // mpsc::channel panics on capacity 0
@@ -61,13 +61,13 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
use std::time::Instant;
use futures_util::{SinkExt, StreamExt};
use nostr::{Event, EventBuilder, Keys, Kind, RelayUrl, Tag};
use serde_json::{json, Value};
use sprout_core::kind::{
use buzz_core::kind::{
KIND_AGENT_OBSERVER_FRAME, KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION,
KIND_TYPING_INDICATOR,
};
use futures_util::{SinkExt, StreamExt};
use nostr::{Event, EventBuilder, Keys, Kind, RelayUrl, Tag};
use serde_json::{json, Value};
use tokio::sync::mpsc;
use tokio::time::timeout;
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
@@ -294,7 +294,7 @@ impl RestClient {
/// Events the harness cares about.
#[derive(Debug, Clone)]
pub struct SproutEvent {
pub struct BuzzEvent {
/// Which channel this event belongs to.
pub channel_id: Uuid,
/// The underlying Nostr event.
@@ -408,14 +408,14 @@ type WsStream = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
/// Harness-side relay client.
///
/// Connects to the Sprout relay, authenticates via NIP-42, and streams
/// Connects to the Buzz relay, authenticates via NIP-42, and streams
/// matching events for subscribed channels.
///
/// A background tokio task owns the WebSocket connection and responds to
/// Ping frames, preventing disconnection during long agent turns.
pub struct HarnessRelay {
/// Receiver for events forwarded by the background task.
event_rx: mpsc::Receiver<Option<SproutEvent>>,
event_rx: mpsc::Receiver<Option<BuzzEvent>>,
/// Receiver for encrypted observer control events addressed to this agent.
observer_control_rx: Option<mpsc::Receiver<Event>>,
/// Sender for commands to the background task.
@@ -470,7 +470,7 @@ impl HarnessRelay {
// task so buffered messages aren't silently discarded.
let (ws, handshake_buffer) = do_connect(relay_url, keys, auth_tag.as_ref()).await?;
let (event_tx, event_rx) = mpsc::channel::<Option<SproutEvent>>(event_channel_capacity());
let (event_tx, event_rx) = mpsc::channel::<Option<BuzzEvent>>(event_channel_capacity());
let (observer_control_tx, observer_control_rx) =
mpsc::channel::<Event>(event_channel_capacity());
let (cmd_tx, cmd_rx) = mpsc::channel::<RelayCommand>(CMD_CHANNEL_CAPACITY);
@@ -526,7 +526,7 @@ impl HarnessRelay {
let p_tag = SingleLetterTag::lowercase(Alphabet::P);
let member_filter = nostr::Filter::new()
.kind(Kind::Custom(
sprout_core::kind::KIND_NIP29_GROUP_MEMBERS as u16,
buzz_core::kind::KIND_NIP29_GROUP_MEMBERS as u16,
))
.custom_tags(p_tag, [pk_hex.as_str()]);
let member_events = rest.query(&[member_filter]).await?;
@@ -563,7 +563,7 @@ impl HarnessRelay {
let d_values: Vec<String> = channel_uuids.iter().map(|u| u.to_string()).collect();
let meta_filter = nostr::Filter::new()
.kind(Kind::Custom(
sprout_core::kind::KIND_NIP29_GROUP_METADATA as u16,
buzz_core::kind::KIND_NIP29_GROUP_METADATA as u16,
))
.custom_tags(d_tag, d_values);
let meta_events = rest.query(&[meta_filter]).await?;
@@ -717,7 +717,7 @@ impl HarnessRelay {
///
/// Reads from the background task's event channel. Returns `None` on
/// connection loss — the caller should call [`reconnect`](Self::reconnect).
pub async fn next_event(&mut self) -> Option<SproutEvent> {
pub async fn next_event(&mut self) -> Option<BuzzEvent> {
// The background task sends `None` to signal connection loss.
self.event_rx.recv().await.flatten()
}
@@ -787,7 +787,7 @@ impl HarnessRelay {
message: &str,
thread_root: Option<&str>,
) -> Result<Event, RelayError> {
use sprout_core::kind::KIND_STREAM_MESSAGE;
use buzz_core::kind::KIND_STREAM_MESSAGE;
let h_tag = Tag::parse(["h", &channel_id.to_string()])
.map_err(|e| RelayError::EventBuild(e.to_string()))?;
@@ -1232,7 +1232,7 @@ async fn execute_connected_command(
async fn run_background_task(
mut ws: WsStream,
initial_handshake_buffer: std::collections::VecDeque<RelayMessage>,
event_tx: mpsc::Sender<Option<SproutEvent>>,
event_tx: mpsc::Sender<Option<BuzzEvent>>,
observer_control_tx: mpsc::Sender<Event>,
mut cmd_rx: mpsc::Receiver<RelayCommand>,
keys: Keys,
@@ -1632,7 +1632,7 @@ async fn run_background_task(
async fn handle_ws_message(
msg: Message,
ws: &mut WsStream,
event_tx: &mpsc::Sender<Option<SproutEvent>>,
event_tx: &mpsc::Sender<Option<BuzzEvent>>,
observer_control_tx: &mpsc::Sender<Event>,
state: &mut BgState,
keys: &Keys,
@@ -1688,7 +1688,7 @@ async fn handle_ws_message(
return true;
}
let ts = event.created_at.as_secs();
let sprout_event = SproutEvent {
let buzz_event = BuzzEvent {
channel_id: channel_uuid,
event: *event,
};
@@ -1702,7 +1702,7 @@ async fn handle_ws_message(
"event channel at ≥80% capacity — backpressure imminent"
);
}
match event_tx.try_send(Some(sprout_event)) {
match event_tx.try_send(Some(buzz_event)) {
Ok(()) => {
state.membership_last_seen =
Some(state.membership_last_seen.unwrap_or(0).max(ts));
@@ -1731,7 +1731,7 @@ async fn handle_ws_message(
let ts = event.created_at.as_secs();
let event_id_hex = event.id.to_hex();
if state.record_event(channel_id, &event) {
let sprout_event = SproutEvent {
let buzz_event = BuzzEvent {
channel_id,
event: *event,
};
@@ -1745,7 +1745,7 @@ async fn handle_ws_message(
"event channel at ≥80% capacity — backpressure imminent"
);
}
match event_tx.try_send(Some(sprout_event)) {
match event_tx.try_send(Some(buzz_event)) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
// Remove from dedup set so the replayed event
@@ -1936,7 +1936,7 @@ async fn handle_ws_message(
async fn process_handshake_buffer(
ws: &mut WsStream,
buffer: std::collections::VecDeque<RelayMessage>,
event_tx: &mpsc::Sender<Option<SproutEvent>>,
event_tx: &mpsc::Sender<Option<BuzzEvent>>,
observer_control_tx: &mpsc::Sender<Event>,
state: &mut BgState,
keys: &Keys,
@@ -2147,7 +2147,7 @@ async fn try_autonomous_reconnect(
keys: &Keys,
relay_url: &str,
agent_pubkey_hex: &str,
event_tx: &mpsc::Sender<Option<SproutEvent>>,
event_tx: &mpsc::Sender<Option<BuzzEvent>>,
observer_control_tx: &mpsc::Sender<Event>,
auth_tag: Option<&nostr::Tag>,
) -> ReconnectOutcome {
@@ -2250,7 +2250,7 @@ async fn wait_for_reconnect(
keys: &Keys,
relay_url: &str,
agent_pubkey_hex: &str,
event_tx: &mpsc::Sender<Option<SproutEvent>>,
event_tx: &mpsc::Sender<Option<BuzzEvent>>,
observer_control_tx: &mpsc::Sender<Event>,
skip_drain: bool,
auth_tag: Option<&nostr::Tag>,
@@ -1,20 +1,20 @@
[package]
name = "sprout-admin"
name = "buzz-admin"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Operator CLI for Sprout relay administration"
description = "Operator CLI for Buzz relay administration"
[[bin]]
name = "sprout-admin"
name = "buzz-admin"
path = "src/main.rs"
[dependencies]
sprout-db = { workspace = true }
sprout-core = { workspace = true }
sprout-auth = { workspace = true }
buzz-db = { workspace = true }
buzz-core = { workspace = true }
buzz-auth = { workspace = true }
nostr = { workspace = true }
tokio = { workspace = true }
serde_json = { workspace = true }
@@ -1,18 +1,18 @@
#![deny(unsafe_code)]
//! Sprout instance administration CLI.
//! Buzz instance administration CLI.
//!
//! In the pure Nostr architecture, API tokens no longer exist.
//! Admin operations are performed via signed Nostr events (NIP-43 relay admin commands).
//! This binary is retained as a placeholder for future admin tooling.
use anyhow::Result;
use buzz_db::{Db, DbConfig};
use clap::{Parser, Subcommand};
use nostr::Keys;
use sprout_db::{Db, DbConfig};
#[derive(Parser)]
#[command(name = "sprout-admin", about = "Sprout instance administration")]
#[command(name = "buzz-admin", about = "Buzz instance administration")]
struct Cli {
#[command(subcommand)]
command: Command,
@@ -41,7 +41,7 @@ enum Command {
/// clients can see those channels. Idempotent — safe to run multiple times.
ReconcileChannels {
/// Relay private key (hex) for signing events. Falls back to
/// SPROUT_RELAY_PRIVATE_KEY env var. If neither is set, generates
/// BUZZ_RELAY_PRIVATE_KEY env var. If neither is set, generates
/// an ephemeral key (events will be unverifiable after restart).
#[arg(long)]
relay_key: Option<String>,
@@ -57,7 +57,7 @@ async fn main() -> Result<()> {
let keys = Keys::generate();
println!("Public key: {}", keys.public_key().to_hex());
println!("Secret key: {}", keys.secret_key().display_secret());
println!("\nSet SPROUT_PRIVATE_KEY to the secret key to use this identity.");
println!("\nSet BUZZ_PRIVATE_KEY to the secret key to use this identity.");
}
Command::AddMember { pubkey, role } => {
let db = connect_db().await?;
@@ -93,7 +93,7 @@ async fn main() -> Result<()> {
async fn connect_db() -> Result<Db> {
let db_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://sprout:sprout_dev@localhost:5432/sprout".to_string());
.unwrap_or_else(|_| "postgres://buzz:buzz_dev@localhost:5432/buzz".to_string());
let db = Db::new(&DbConfig {
database_url: db_url,
..DbConfig::default()
@@ -103,15 +103,14 @@ async fn connect_db() -> Result<Db> {
}
async fn reconcile_channels(relay_key_arg: Option<String>) -> Result<()> {
use buzz_core::kind::KIND_NIP29_GROUP_ADMINS;
use buzz_db::event::EventQuery;
use nostr::{EventBuilder, Kind, Tag};
use sprout_core::kind::KIND_NIP29_GROUP_ADMINS;
use sprout_db::event::EventQuery;
let db = connect_db().await?;
// Resolve relay signing key: arg > env > ephemeral
let relay_keys = match relay_key_arg.or_else(|| std::env::var("SPROUT_RELAY_PRIVATE_KEY").ok())
{
let relay_keys = match relay_key_arg.or_else(|| std::env::var("BUZZ_RELAY_PRIVATE_KEY").ok()) {
Some(key_hex) => {
Keys::parse(&key_hex).map_err(|e| anyhow::anyhow!("invalid relay key: {e}"))?
}
@@ -122,7 +121,7 @@ async fn reconcile_channels(relay_key_arg: Option<String>) -> Result<()> {
k.public_key().to_hex()
);
eprintln!("Events signed with this key won't be verifiable after this run.");
eprintln!("Pass --relay-key or set SPROUT_RELAY_PRIVATE_KEY for production use.");
eprintln!("Pass --relay-key or set BUZZ_RELAY_PRIVATE_KEY for production use.");
k
}
};
@@ -1,5 +1,5 @@
[package]
name = "sprout-agent"
name = "buzz-agent"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -11,11 +11,11 @@ keywords = ["acp", "agent", "llm", "mcp", "minimal"]
categories = ["command-line-utilities", "web-programming"]
[lib]
name = "sprout_agent"
name = "buzz_agent"
path = "src/lib.rs"
[[bin]]
name = "sprout-agent"
name = "buzz-agent"
path = "src/main.rs"
# Test-only fake MCP server. Built unconditionally because cargo can't gate
@@ -1,16 +1,16 @@
# sprout-agent
# buzz-agent
> Minimal, unbreakable ACP-compliant LLM agent. Stdio in, tool calls out. Non-streaming. No persistence. No cleverness.
[ACP](https://agentclientprotocol.com) is the Agent Client Protocol — JSON-RPC 2.0 over stdio between a client (Zed, JetBrains, sprout-acp, …) and an agent. [MCP](https://modelcontextprotocol.io) is how the agent talks to its tools.
[ACP](https://agentclientprotocol.com) is the Agent Client Protocol — JSON-RPC 2.0 over stdio between a client (Zed, JetBrains, buzz-acp, …) and an agent. [MCP](https://modelcontextprotocol.io) is how the agent talks to its tools.
`sprout-agent` is the agent.
`buzz-agent` is the agent.
## What It Is
```
+--------+ stdio (JSON-RPC 2.0) +---------------+
| client | <----------------------> | sprout-agent |
| client | <----------------------> | buzz-agent |
+--------+ ACP frames +---------------+
│ │
│ │ rmcp (stdio)
@@ -35,26 +35,26 @@ The agent's **output is its tool calls**. Generated text is forwarded to the cli
```bash
# Build
cargo build --release -p sprout-agent
cargo build --release -p buzz-agent
# Run against Anthropic
SPROUT_AGENT_PROVIDER=anthropic \
BUZZ_AGENT_PROVIDER=anthropic \
ANTHROPIC_API_KEY=sk-ant-... \
ANTHROPIC_MODEL=claude-sonnet-4-5 \
./target/release/sprout-agent
./target/release/buzz-agent
# Or any OpenAI-compatible endpoint
SPROUT_AGENT_PROVIDER=openai \
BUZZ_AGENT_PROVIDER=openai \
OPENAI_COMPAT_API_KEY=sk-... \
OPENAI_COMPAT_MODEL=gpt-5 \
OPENAI_COMPAT_BASE_URL=https://api.openai.com/v1 \
./target/release/sprout-agent
./target/release/buzz-agent
# Or Databricks model serving via OAuth 2.0 PKCE
SPROUT_AGENT_PROVIDER=databricks \
BUZZ_AGENT_PROVIDER=databricks \
DATABRICKS_HOST=https://dbc-...cloud.databricks.com \
DATABRICKS_MODEL=goose-claude-4-6-sonnet \
./target/release/sprout-agent
./target/release/buzz-agent
```
That's the whole setup. The agent reads JSON-RPC frames from stdin, writes them to stdout, and logs to stderr.
@@ -73,7 +73,7 @@ A complete round-trip. Lines starting with `→` are client→agent (stdin); `
"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false},
"mcpCapabilities":{"http":false,"sse":false}
},
"agentInfo":{"name":"sprout-agent","version":"0.1.0"}
"agentInfo":{"name":"buzz-agent","version":"0.1.0"}
}}
// 2. Open a session. The client passes the MCP servers to spawn.
@@ -129,7 +129,7 @@ Everything is environment variables. No flags, no config files. (We are a subpro
| Variable | Default | Notes |
|---|---|---|
| `SPROUT_AGENT_PROVIDER` | — | `anthropic`, `openai`, or `databricks`. If unset, or if `anthropic`/`openai` is selected but its API key is missing, Databricks is auto-selected when `DATABRICKS_HOST` + `DATABRICKS_MODEL` are set. |
| `BUZZ_AGENT_PROVIDER` | — | `anthropic`, `openai`, or `databricks`. If unset, or if `anthropic`/`openai` is selected but its API key is missing, Databricks is auto-selected when `DATABRICKS_HOST` + `DATABRICKS_MODEL` are set. |
| `ANTHROPIC_API_KEY` | — | Required when provider=anthropic unless Databricks fallback is configured. |
| `ANTHROPIC_MODEL` | — | Required when provider=anthropic. |
| `ANTHROPIC_BASE_URL` | `https://api.anthropic.com` | |
@@ -141,25 +141,25 @@ Everything is environment variables. No flags, no config files. (We are a subpro
| `DATABRICKS_HOST` | — | Required when provider=databricks or when using Databricks fallback. |
| `DATABRICKS_MODEL` | — | Required when provider=databricks or when using Databricks fallback. |
| `DATABRICKS_TOKEN` | — | Optional static bearer escape hatch. If unset, Databricks uses browser OAuth + refresh cache. |
| `SPROUT_AGENT_SYSTEM_PROMPT` | built-in | Inline system prompt. |
| `SPROUT_AGENT_SYSTEM_PROMPT_FILE` | — | File path. Mutually exclusive with the above. |
| `SPROUT_AGENT_MAX_ROUNDS` | `0` | Tool-loop iteration cap. 0 = unlimited. |
| `SPROUT_AGENT_MAX_OUTPUT_TOKENS` | `32768` | Per LLM call. Headroom for large tool-call inputs (e.g. file writes via heredoc); Sonnet 4 / Opus 4 cap at 64K. |
| `SPROUT_AGENT_MAX_CONTEXT_TOKENS` | `200000` | Provider context window used by the handoff gate. |
| `SPROUT_AGENT_MAX_HANDOFFS` | `10` | Max context handoffs per session before falling back to truncation. |
| `SPROUT_AGENT_LLM_TIMEOUT_SECS` | `120` | |
| `SPROUT_AGENT_TOOL_TIMEOUT_SECS` | `660` | Per-tool call timeout in seconds |
| `SPROUT_AGENT_MAX_PARALLEL_TOOLS` | `8` | Max concurrent tool calls per turn (1 = sequential) |
| `SPROUT_AGENT_MAX_SESSIONS` | unlimited | Max concurrent ACP sessions. Sessions are cheap; default has no cap. |
| `SPROUT_AGENT_MAX_LINE_BYTES` | `4194304` | 4 MiB. Hard cap on inbound JSON-RPC frames. |
| `SPROUT_AGENT_MAX_HISTORY_BYTES` | `1048576` | 1 MiB. Old turns are evicted past this. |
| `BUZZ_AGENT_SYSTEM_PROMPT` | built-in | Inline system prompt. |
| `BUZZ_AGENT_SYSTEM_PROMPT_FILE` | — | File path. Mutually exclusive with the above. |
| `BUZZ_AGENT_MAX_ROUNDS` | `0` | Tool-loop iteration cap. 0 = unlimited. |
| `BUZZ_AGENT_MAX_OUTPUT_TOKENS` | `32768` | Per LLM call. Headroom for large tool-call inputs (e.g. file writes via heredoc); Sonnet 4 / Opus 4 cap at 64K. |
| `BUZZ_AGENT_MAX_CONTEXT_TOKENS` | `200000` | Provider context window used by the handoff gate. |
| `BUZZ_AGENT_MAX_HANDOFFS` | `10` | Max context handoffs per session before falling back to truncation. |
| `BUZZ_AGENT_LLM_TIMEOUT_SECS` | `120` | |
| `BUZZ_AGENT_TOOL_TIMEOUT_SECS` | `660` | Per-tool call timeout in seconds |
| `BUZZ_AGENT_MAX_PARALLEL_TOOLS` | `8` | Max concurrent tool calls per turn (1 = sequential) |
| `BUZZ_AGENT_MAX_SESSIONS` | unlimited | Max concurrent ACP sessions. Sessions are cheap; default has no cap. |
| `BUZZ_AGENT_MAX_LINE_BYTES` | `4194304` | 4 MiB. Hard cap on inbound JSON-RPC frames. |
| `BUZZ_AGENT_MAX_HISTORY_BYTES` | `1048576` | 1 MiB. Old turns are evicted past this. |
## Providers
`sprout-agent` speaks two HTTP dialects. Pick with `SPROUT_AGENT_PROVIDER`.
`buzz-agent` speaks two HTTP dialects. Pick with `BUZZ_AGENT_PROVIDER`.
| Provider | `SPROUT_AGENT_PROVIDER` | Endpoint (auto) | Tested with |
| Provider | `BUZZ_AGENT_PROVIDER` | Endpoint (auto) | Tested with |
|---|---|---|---|
| Anthropic | `anthropic` | `POST {base}/v1/messages` | claude-sonnet-4-5, claude-opus-4 |
| OpenAI | `openai` | `POST {base}/responses` | gpt-5, gpt-5-mini, o4-mini, gpt-4o |
@@ -170,7 +170,7 @@ Everything is environment variables. No flags, no config files. (We are a subpro
| Block Gateway | `openai` | `POST {base}/chat/completions` | gpt-5, claude |
| Databricks | `databricks` | `POST {host}/serving-endpoints/{model}/invocations` | goose-claude-4-6-sonnet |
If `SPROUT_AGENT_PROVIDER=anthropic` is selected without `ANTHROPIC_API_KEY`, or `SPROUT_AGENT_PROVIDER=openai` is selected without `OPENAI_COMPAT_API_KEY`, the agent automatically falls back to Databricks OAuth when `DATABRICKS_HOST` and `DATABRICKS_MODEL` are set. The same Databricks fallback applies when `SPROUT_AGENT_PROVIDER` is unset. Explicit Anthropic/OpenAI API keys always win.
If `BUZZ_AGENT_PROVIDER=anthropic` is selected without `ANTHROPIC_API_KEY`, or `BUZZ_AGENT_PROVIDER=openai` is selected without `OPENAI_COMPAT_API_KEY`, the agent automatically falls back to Databricks OAuth when `DATABRICKS_HOST` and `DATABRICKS_MODEL` are set. The same Databricks fallback applies when `BUZZ_AGENT_PROVIDER` is unset. Explicit Anthropic/OpenAI API keys always win.
`provider=openai` speaks two HTTP dialects: the [Responses API](https://platform.openai.com/docs/api-reference/responses) (`/v1/responses`, required for GPT-5 / o-series tool-calling on OpenAI's own service) and the [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) (`/chat/completions`, the broadly-supported OpenAI-compatible wire format).
@@ -219,19 +219,19 @@ The trust boundary is **the operator who launched the agent**. The harness, MCP
| MCP child env | Whitelist (`PATH`, `HOME`, `TERM`, `LANG`, `LC_ALL`, `TMPDIR`) plus what the client explicitly passes. Your `ANTHROPIC_API_KEY` does not leak into MCP children. |
| MCP child lifetime | Process group via `setpgid(0,0)` in `pre_exec`. On transport break or shutdown: `killpg(SIGKILL)`. Grandchildren die too. |
| Server poisoning | After a timeout or transport break, the offending server is marked dead. Future calls trigger a lazy restart with exponential backoff. Other servers keep working. |
| Frame size | `SPROUT_AGENT_MAX_LINE_BYTES` (default 4 MiB). Oversize → connection killed. |
| Frame size | `BUZZ_AGENT_MAX_LINE_BYTES` (default 4 MiB). Oversize → connection killed. |
| LLM response size | 16 MiB hard cap. Both `Content-Length` precheck and streaming-buffer cap. |
| Cancellation | `tokio::select! { biased; _ = cancel.changed() => ... }` at every loop boundary. Cancel always wins the race. |
| Session isolation | Unlimited concurrent sessions by default (configurable via `SPROUT_AGENT_MAX_SESSIONS`). One prompt per session at a time. Each session gets its own MCP servers. |
| Session isolation | Unlimited concurrent sessions by default (configurable via `BUZZ_AGENT_MAX_SESSIONS`). One prompt per session at a time. Each session gets its own MCP servers. |
| `tool_use ↔ tool_result` pairing | Encoded in the type system. Every `ToolCall` and `ToolResult` carries a `provider_id: String` (not `Option`). |
### Bounded Everything
| Limit | Default | Where |
|---|---|---|
| Inbound JSON-RPC frame | 4 MiB | `SPROUT_AGENT_MAX_LINE_BYTES` |
| Inbound JSON-RPC frame | 4 MiB | `BUZZ_AGENT_MAX_LINE_BYTES` |
| Single prompt | 1 MiB | `MAX_PROMPT_BYTES` |
| History window | 1 MiB | `SPROUT_AGENT_MAX_HISTORY_BYTES` |
| History window | 1 MiB | `BUZZ_AGENT_MAX_HISTORY_BYTES` |
| LLM response body | 16 MiB | `MAX_LLM_RESPONSE_BYTES` |
| LLM error body | 4 KiB | `MAX_LLM_ERROR_BODY_BYTES` |
| Tool result body | 256 KiB | `MAX_TOOL_RESULT_BYTES` |
@@ -240,9 +240,9 @@ The trust boundary is **the operator who launched the agent**. The harness, MCP
| Tool description bytes | 1 KiB | `MAX_DESCRIPTION_BYTES` |
| Tool schema bytes | 4 KiB | `MAX_SCHEMA_BYTES` (oversize → replaced with `{}`) |
| Tool calls per turn | 64 | `MAX_TOOL_CALLS_PER_TURN` |
| Loop rounds | 0 (unlimited) | `SPROUT_AGENT_MAX_ROUNDS` |
| LLM call timeout | 120 s | `SPROUT_AGENT_LLM_TIMEOUT_SECS` |
| Tool call timeout | 660 s | `SPROUT_AGENT_TOOL_TIMEOUT_SECS` |
| Loop rounds | 0 (unlimited) | `BUZZ_AGENT_MAX_ROUNDS` |
| LLM call timeout | 120 s | `BUZZ_AGENT_LLM_TIMEOUT_SECS` |
| Tool call timeout | 660 s | `BUZZ_AGENT_TOOL_TIMEOUT_SECS` |
## What This Is NOT
@@ -285,13 +285,13 @@ One reader, one writer, up to 8 concurrent prompt tasks (one per session).
## Building
```bash
cargo build --release -p sprout-agent
cargo build --release -p buzz-agent
```
## Testing
```bash
cargo test -p sprout-agent
cargo test -p buzz-agent
```
Test strategy is **real subprocess, no mocks**:

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

@@ -568,7 +568,7 @@ fn format_hook_output_body(hook: &str, server: &str, text: &str) -> String {
/// be unique per pair so the LLM wire format (which keys tool results by
/// id) stays valid across multiple objections in one session.
fn synthetic_hook_id(hook: &str, server: &str, ordinal: u64) -> String {
format!("sprout_hook_{hook}_{server}_{ordinal}")
format!("buzz_hook_{hook}_{server}_{ordinal}")
}
/// Append a synthetic Assistant tool-call + ToolResult pair for each hook
@@ -65,7 +65,7 @@ impl TokenSource for StaticTokenSource {
///
/// The `discovery_url` must return a JSON document with at least
/// `authorization_endpoint` and `token_endpoint` (RFC 8414). The
/// `cache_namespace` is the directory under `~/.config/sprout-agent/oauth/`
/// `cache_namespace` is the directory under `~/.config/buzz-agent/oauth/`
/// the token JSON lives in — separates providers' caches cleanly.
#[derive(Debug, Clone)]
pub struct PkceOAuthConfig {
@@ -74,7 +74,7 @@ pub struct PkceOAuthConfig {
pub scopes: Vec<String>,
pub cache_namespace: String,
/// When `Some`, the engine writes tokens here instead of
/// `~/.config/sprout-agent/oauth/<cache_namespace>/`. Production code
/// `~/.config/buzz-agent/oauth/<cache_namespace>/`. Production code
/// leaves this `None`. Integration tests use it to avoid stomping on
/// a shared `$HOME` when running in parallel.
pub cache_dir_override: Option<PathBuf>,
@@ -290,7 +290,7 @@ fn cache_path_for(cfg: &PkceOAuthConfig) -> Result<PathBuf, AgentError> {
.map_err(|_| AgentError::Llm("oauth cache: $HOME not set".into()))?;
PathBuf::from(home)
.join(".config")
.join("sprout-agent")
.join("buzz-agent")
.join("oauth")
.join(&cfg.cache_namespace)
}
@@ -392,9 +392,9 @@ async fn browser_pkce_flow(
}
match result {
Ok(_) => Html(
"<h2>Sprout: signed in</h2><p>You can close this window.</p>".to_string(),
"<h2>Buzz: signed in</h2><p>You can close this window.</p>".to_string(),
),
Err(e) => Html(format!("<h2>Sprout auth failed</h2><pre>{e}</pre>")),
Err(e) => Html(format!("<h2>Buzz auth failed</h2><pre>{e}</pre>")),
}
}
}),
@@ -525,7 +525,7 @@ mod tests {
cache_dir_override: None,
};
let p = cache_path_for(&cfg).unwrap();
assert!(p.to_string_lossy().contains("/sprout-agent/oauth/demo/"));
assert!(p.to_string_lossy().contains("/buzz-agent/oauth/demo/"));
assert!(p.extension().and_then(|s| s.to_str()) == Some("json"));
}
@@ -17,7 +17,7 @@ pub const HANDOFF_PROMPT_MAX_BYTES: usize = 32 * 1024;
pub const HANDOFF_MAX_TOOL_NAMES: usize = 20;
const DEFAULT_SYSTEM_PROMPT: &str =
"You are sprout-agent. Use the provided tools to act. Tool calls are your only output.";
"You are buzz-agent. Use the provided tools to act. Tool calls are your only output.";
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Provider {
@@ -61,7 +61,7 @@ pub struct Config {
/// handoff threshold for this budget, before the next request can exceed
/// the window and 400. Default 200_000 — matching Claude 4.x windows;
/// operators lower/raise it for other models. Set via
/// `SPROUT_AGENT_MAX_CONTEXT_TOKENS`.
/// `BUZZ_AGENT_MAX_CONTEXT_TOKENS`.
pub max_context_tokens: u64,
pub max_handoffs: usize,
pub max_parallel_tools: usize,
@@ -87,7 +87,7 @@ impl Config {
let databricks_host = env("DATABRICKS_HOST");
let databricks_model = env("DATABRICKS_MODEL");
let provider = resolve_provider(
env("SPROUT_AGENT_PROVIDER").as_deref(),
env("BUZZ_AGENT_PROVIDER").as_deref(),
env("ANTHROPIC_API_KEY").as_deref(),
env("OPENAI_COMPAT_API_KEY").as_deref(),
databricks_host.as_deref(),
@@ -97,7 +97,7 @@ impl Config {
// Universal model override — any provider will use this when its own
// model env var is absent. Useful for wrapper scripts that set a single
// var regardless of which provider is active.
let sprout_agent_model = env("SPROUT_AGENT_MODEL");
let buzz_agent_model = env("BUZZ_AGENT_MODEL");
// OPENAI_COMPAT_API is only read when provider=openai, so a stray
// bad value can't break an Anthropic-only deployment.
@@ -110,7 +110,7 @@ impl Config {
req("ANTHROPIC_API_KEY")?,
resolve_model(
env("ANTHROPIC_MODEL").as_deref(),
sprout_agent_model.as_deref(),
buzz_agent_model.as_deref(),
)
.ok_or_else(|| "config: ANTHROPIC_MODEL required".to_string())?,
env_or("ANTHROPIC_BASE_URL", "https://api.anthropic.com"),
@@ -120,7 +120,7 @@ impl Config {
req("OPENAI_COMPAT_API_KEY")?,
resolve_model(
env("OPENAI_COMPAT_MODEL").as_deref(),
sprout_agent_model.as_deref(),
buzz_agent_model.as_deref(),
)
.ok_or_else(|| "config: OPENAI_COMPAT_MODEL required".to_string())?,
env_or("OPENAI_COMPAT_BASE_URL", "https://api.openai.com/v1"),
@@ -128,15 +128,15 @@ impl Config {
),
Provider::Databricks => (
env("DATABRICKS_TOKEN").unwrap_or_default(),
resolve_model(databricks_model.as_deref(), sprout_agent_model.as_deref())
resolve_model(databricks_model.as_deref(), buzz_agent_model.as_deref())
.ok_or_else(|| "config: DATABRICKS_MODEL required".to_string())?,
databricks_host.ok_or_else(|| "config: DATABRICKS_HOST required".to_string())?,
OpenAiApi::Chat, // Databricks invocations is chat-shaped
),
};
let system_prompt = match (env("SPROUT_AGENT_SYSTEM_PROMPT"), env("SPROUT_AGENT_SYSTEM_PROMPT_FILE")) {
let system_prompt = match (env("BUZZ_AGENT_SYSTEM_PROMPT"), env("BUZZ_AGENT_SYSTEM_PROMPT_FILE")) {
(Some(_), Some(_)) => return Err(
"config: SPROUT_AGENT_SYSTEM_PROMPT and SPROUT_AGENT_SYSTEM_PROMPT_FILE are mutually exclusive".into()),
"config: BUZZ_AGENT_SYSTEM_PROMPT and BUZZ_AGENT_SYSTEM_PROMPT_FILE are mutually exclusive".into()),
(Some(s), _) => s,
(_, Some(p)) => std::fs::read_to_string(&p).map_err(|e| format!("config: read {p}: {e}"))?,
_ => DEFAULT_SYSTEM_PROMPT.to_owned(),
@@ -149,30 +149,27 @@ impl Config {
base_url,
anthropic_api_version: env_or("ANTHROPIC_API_VERSION", "2023-06-01"),
openai_api,
max_rounds: parse_env("SPROUT_AGENT_MAX_ROUNDS", 0)?,
max_output_tokens: parse_env("SPROUT_AGENT_MAX_OUTPUT_TOKENS", 32_768)?,
llm_timeout: Duration::from_secs(parse_env("SPROUT_AGENT_LLM_TIMEOUT_SECS", 120)?),
tool_timeout: Duration::from_secs(parse_env("SPROUT_AGENT_TOOL_TIMEOUT_SECS", 660)?),
max_rounds: parse_env("BUZZ_AGENT_MAX_ROUNDS", 0)?,
max_output_tokens: parse_env("BUZZ_AGENT_MAX_OUTPUT_TOKENS", 32_768)?,
llm_timeout: Duration::from_secs(parse_env("BUZZ_AGENT_LLM_TIMEOUT_SECS", 120)?),
tool_timeout: Duration::from_secs(parse_env("BUZZ_AGENT_TOOL_TIMEOUT_SECS", 660)?),
mcp_init_timeout: Duration::from_secs(parse_env(
"SPROUT_AGENT_MCP_INIT_TIMEOUT_SECS",
"BUZZ_AGENT_MCP_INIT_TIMEOUT_SECS",
30,
)?),
mcp_max_restart_attempts: parse_env("SPROUT_AGENT_MCP_RESTART_MAX_ATTEMPTS", 3u32)?,
mcp_restart_base_ms: parse_env("SPROUT_AGENT_MCP_RESTART_BASE_MS", 500u64)?,
mcp_restart_max_ms: parse_env("SPROUT_AGENT_MCP_RESTART_MAX_MS", 30_000u64)?,
max_sessions: parse_env("SPROUT_AGENT_MAX_SESSIONS", usize::MAX)?,
max_line_bytes: parse_env("SPROUT_AGENT_MAX_LINE_BYTES", 4 * 1024 * 1024)?,
max_history_bytes: parse_env("SPROUT_AGENT_MAX_HISTORY_BYTES", 16 * 1024 * 1024)?,
max_context_tokens: parse_env("SPROUT_AGENT_MAX_CONTEXT_TOKENS", 200_000u64)?,
max_handoffs: parse_env("SPROUT_AGENT_MAX_HANDOFFS", 10)?,
max_parallel_tools: parse_env("SPROUT_AGENT_MAX_PARALLEL_TOOLS", 8usize)?,
hook_timeout: Duration::from_millis(parse_env(
"SPROUT_AGENT_HOOK_TIMEOUT_MS",
2500u64,
)?),
stop_max_rejections: parse_env("SPROUT_AGENT_STOP_MAX_REJECTIONS", 3u32)?,
mcp_max_restart_attempts: parse_env("BUZZ_AGENT_MCP_RESTART_MAX_ATTEMPTS", 3u32)?,
mcp_restart_base_ms: parse_env("BUZZ_AGENT_MCP_RESTART_BASE_MS", 500u64)?,
mcp_restart_max_ms: parse_env("BUZZ_AGENT_MCP_RESTART_MAX_MS", 30_000u64)?,
max_sessions: parse_env("BUZZ_AGENT_MAX_SESSIONS", usize::MAX)?,
max_line_bytes: parse_env("BUZZ_AGENT_MAX_LINE_BYTES", 4 * 1024 * 1024)?,
max_history_bytes: parse_env("BUZZ_AGENT_MAX_HISTORY_BYTES", 16 * 1024 * 1024)?,
max_context_tokens: parse_env("BUZZ_AGENT_MAX_CONTEXT_TOKENS", 200_000u64)?,
max_handoffs: parse_env("BUZZ_AGENT_MAX_HANDOFFS", 10)?,
max_parallel_tools: parse_env("BUZZ_AGENT_MAX_PARALLEL_TOOLS", 8usize)?,
hook_timeout: Duration::from_millis(parse_env("BUZZ_AGENT_HOOK_TIMEOUT_MS", 2500u64)?),
stop_max_rejections: parse_env("BUZZ_AGENT_STOP_MAX_REJECTIONS", 3u32)?,
hook_servers: parse_hook_servers_env("MCP_HOOK_SERVERS"),
hints_enabled: parse_env("SPROUT_AGENT_NO_HINTS", 0u8)? == 0,
hints_enabled: parse_env("BUZZ_AGENT_NO_HINTS", 0u8)? == 0,
};
cfg.validate()?;
Ok(cfg)
@@ -184,51 +181,52 @@ impl Config {
const MIN_TIMEOUT: Duration = Duration::from_secs(1);
if self.max_output_tokens < 1 {
return Err("config: SPROUT_AGENT_MAX_OUTPUT_TOKENS must be >= 1".into());
return Err("config: BUZZ_AGENT_MAX_OUTPUT_TOKENS must be >= 1".into());
}
if self.max_context_tokens <= u64::from(self.max_output_tokens) {
return Err(format!(
"config: SPROUT_AGENT_MAX_CONTEXT_TOKENS ({}) must be > SPROUT_AGENT_MAX_OUTPUT_TOKENS ({}) — the context window must leave room for the response",
"config: BUZZ_AGENT_MAX_CONTEXT_TOKENS ({}) must be > BUZZ_AGENT_MAX_OUTPUT_TOKENS ({}) — the context window must leave room for the response",
self.max_context_tokens, self.max_output_tokens
));
}
if self.max_history_bytes < MIN_HISTORY_BYTES {
return Err(format!(
"config: SPROUT_AGENT_MAX_HISTORY_BYTES must be >= {MIN_HISTORY_BYTES}"
"config: BUZZ_AGENT_MAX_HISTORY_BYTES must be >= {MIN_HISTORY_BYTES}"
));
}
if self.max_history_bytes < MAX_PROMPT_BYTES {
return Err(format!(
"config: SPROUT_AGENT_MAX_HISTORY_BYTES ({}) must be >= MAX_PROMPT_BYTES ({MAX_PROMPT_BYTES})",
"config: BUZZ_AGENT_MAX_HISTORY_BYTES ({}) must be >= MAX_PROMPT_BYTES ({MAX_PROMPT_BYTES})",
self.max_history_bytes
));
}
if self.max_line_bytes < MIN_LINE_BYTES {
return Err(format!(
"config: SPROUT_AGENT_MAX_LINE_BYTES must be >= {MIN_LINE_BYTES}"
"config: BUZZ_AGENT_MAX_LINE_BYTES must be >= {MIN_LINE_BYTES}"
));
}
if self.llm_timeout < MIN_TIMEOUT {
return Err("config: SPROUT_AGENT_LLM_TIMEOUT_SECS must be >= 1".into());
return Err("config: BUZZ_AGENT_LLM_TIMEOUT_SECS must be >= 1".into());
}
if self.tool_timeout < MIN_TIMEOUT {
return Err("config: SPROUT_AGENT_TOOL_TIMEOUT_SECS must be >= 1".into());
return Err("config: BUZZ_AGENT_TOOL_TIMEOUT_SECS must be >= 1".into());
}
if self.mcp_init_timeout < MIN_TIMEOUT {
return Err("config: SPROUT_AGENT_MCP_INIT_TIMEOUT_SECS must be >= 1".into());
return Err("config: BUZZ_AGENT_MCP_INIT_TIMEOUT_SECS must be >= 1".into());
}
if self.max_parallel_tools < 1 {
return Err("config: SPROUT_AGENT_MAX_PARALLEL_TOOLS must be >= 1".into());
return Err("config: BUZZ_AGENT_MAX_PARALLEL_TOOLS must be >= 1".into());
}
if self.mcp_max_restart_attempts < 1 {
return Err("config: SPROUT_AGENT_MCP_RESTART_MAX_ATTEMPTS must be >= 1".into());
return Err("config: BUZZ_AGENT_MCP_RESTART_MAX_ATTEMPTS must be >= 1".into());
}
if self.mcp_restart_base_ms < 1 {
return Err("config: SPROUT_AGENT_MCP_RESTART_BASE_MS must be >= 1".into());
return Err("config: BUZZ_AGENT_MCP_RESTART_BASE_MS must be >= 1".into());
}
if self.mcp_restart_max_ms < self.mcp_restart_base_ms {
return Err(
"config: SPROUT_AGENT_MCP_RESTART_MAX_MS must be >= SPROUT_AGENT_MCP_RESTART_BASE_MS".into(),
"config: BUZZ_AGENT_MCP_RESTART_MAX_MS must be >= BUZZ_AGENT_MCP_RESTART_BASE_MS"
.into(),
);
}
Ok(())
@@ -298,13 +296,13 @@ fn resolve_provider(
),
"databricks" => Ok(Provider::Databricks),
_ => Err(format!(
"config: SPROUT_AGENT_PROVIDER={raw} not supported"
"config: BUZZ_AGENT_PROVIDER={raw} not supported"
)),
}
}
None if databricks_ready => Ok(Provider::Databricks),
None => Err(
"config: SPROUT_AGENT_PROVIDER required (or set DATABRICKS_HOST and DATABRICKS_MODEL for Databricks OAuth fallback)".into(),
"config: BUZZ_AGENT_PROVIDER required (or set DATABRICKS_HOST and DATABRICKS_MODEL for Databricks OAuth fallback)".into(),
),
}
}
@@ -592,13 +590,13 @@ mod tests {
assert!(err.contains("OPENAI_COMPAT_API_KEY required"));
let err =
resolve_provider(None, None, None, Some("https://dbc.example"), None).unwrap_err();
assert!(err.contains("SPROUT_AGENT_PROVIDER required"));
assert!(err.contains("BUZZ_AGENT_PROVIDER required"));
}
#[test]
fn resolve_provider_unsupported_error_preserves_user_casing() {
let err = resolve_provider(Some("OpenAIish"), None, None, None, None).unwrap_err();
assert!(err.contains("SPROUT_AGENT_PROVIDER=OpenAIish"));
assert!(err.contains("BUZZ_AGENT_PROVIDER=OpenAIish"));
}
#[test]
@@ -368,11 +368,11 @@ mod tests {
std::fs::write(cwd.join("AGENTS.md"), "Project-level hints.").unwrap();
let skill_dir = cwd.join(".agents/skills/sprout-cli");
let skill_dir = cwd.join(".agents/skills/buzz-cli");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: sprout-cli\ndescription: CLI reference for Sprout managed agents\n---\nUse `sprout` to manage agents.\n",
"---\nname: buzz-cli\ndescription: CLI reference for Buzz managed agents\n---\nUse `buzz` to manage agents.\n",
)
.unwrap();
@@ -392,12 +392,12 @@ mod tests {
"missing Available Skills"
);
assert!(
result.contains("sprout-cli: CLI reference for Sprout managed agents"),
result.contains("buzz-cli: CLI reference for Buzz managed agents"),
"missing skill bullet"
);
assert!(result.contains("### sprout-cli"), "missing skill header");
assert!(result.contains("### buzz-cli"), "missing skill header");
assert!(
result.contains("Use `sprout` to manage agents."),
result.contains("Use `buzz` to manage agents."),
"missing skill body"
);
}
@@ -73,7 +73,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
/// `sprout-agent auth <provider>` — run the interactive auth flow for a
/// `buzz-agent auth <provider>` — run the interactive auth flow for a
/// provider and persist the result, then exit. Today the only provider is
/// `databricks` (OAuth 2.0 PKCE). Reads `DATABRICKS_HOST` from env; needs
/// a browser on the machine.
@@ -95,13 +95,11 @@ async fn auth_subcommand(args: &[String]) -> Result<(), Box<dyn std::error::Erro
};
let src = auth::PkceOAuthTokenSource::new(pkce)?;
src.interactive_login().await?;
eprintln!(
"Authenticated. Token cached under ~/.config/sprout-agent/oauth/databricks/."
);
eprintln!("Authenticated. Token cached under ~/.config/buzz-agent/oauth/databricks/.");
Ok(())
}
Some(other) => Err(format!("auth: unknown provider {other:?}").into()),
None => Err("auth: provider required (try: sprout-agent auth databricks)".into()),
None => Err("auth: provider required (try: buzz-agent auth databricks)".into()),
}
}
@@ -229,7 +227,7 @@ async fn initialize(id: Value, params: Value, wire_tx: &WireSender) {
"promptCapabilities": { "image": false, "audio": false, "embeddedContext": false },
"mcpCapabilities": { "http": false, "sse": false },
},
"agentInfo": { "name": "sprout-agent", "version": env!("CARGO_PKG_VERSION") },
"agentInfo": { "name": "buzz-agent", "version": env!("CARGO_PKG_VERSION") },
}),
),
)
@@ -12,7 +12,7 @@ use crate::types::{
/// Databricks OAuth client_id — the public Databricks-published CLI client.
/// PKCE-only, no secret. Same identifier goose uses, so a user's browser
/// consent for `databricks-cli` covers sprout-agent too.
/// consent for `databricks-cli` covers buzz-agent too.
const DATABRICKS_CLIENT_ID: &str = "databricks-cli";
const DATABRICKS_OAUTH_SCOPES: &[&str] = &["all-apis", "offline_access"];
@@ -1,5 +1,5 @@
fn main() {
if let Err(e) = sprout_agent::run() {
if let Err(e) = buzz_agent::run() {
eprintln!("Error: {e}");
std::process::exit(1);
}
@@ -42,12 +42,12 @@ const PASSTHROUGH_ENV: &[&str] = &[
"GIT_ASKPASS",
"GIT_SSH_COMMAND",
"GIT_CONFIG_GLOBAL",
// Sprout identity — dev-mcp writes NOSTR_PRIVATE_KEY to a keyfile then
// removes it from its own env (children never see it). SPROUT_PRIVATE_KEY
// and SPROUT_RELAY_URL are kept for the sprout CLI.
// Buzz identity — dev-mcp writes NOSTR_PRIVATE_KEY to a keyfile then
// removes it from its own env (children never see it). BUZZ_PRIVATE_KEY
// and BUZZ_RELAY_URL are kept for the buzz CLI.
"NOSTR_PRIVATE_KEY",
"SPROUT_PRIVATE_KEY",
"SPROUT_RELAY_URL",
"BUZZ_PRIVATE_KEY",
"BUZZ_RELAY_URL",
];
type Client = RunningService<RoleClient, ()>;
@@ -83,7 +83,7 @@ pub fn classify(msg: &Value) -> Inbound {
params,
},
(Some(m), None) => Inbound::Notification { method: m, params },
// Bare responses (id present, no method) are unexpected — sprout-agent
// Bare responses (id present, no method) are unexpected — buzz-agent
// does not issue requests to the client. Ignore silently.
(None, Some(_)) => Inbound::Ignored,
(None, None) => Inbound::Invalid {
@@ -2,11 +2,11 @@
//!
//! No browser dance — we cover the silent-refresh and cache-hit paths
//! against a stubbed OIDC server (axum). The interactive browser flow is
//! exercised manually via the `sprout-agent auth databricks` subcommand
//! exercised manually via the `buzz-agent auth databricks` subcommand
//! (see `lib.rs::auth_subcommand`).
//!
//! The second test module (further down) is an ACP-level envelope
//! regression: it spawns the real `sprout-agent` binary with
//! regression: it spawns the real `buzz-agent` binary with
//! `DATABRICKS_TOKEN` set and a stub HTTP server, then asserts the wire
//! shape we send to Databricks.
@@ -17,9 +17,9 @@ use std::time::{SystemTime, UNIX_EPOCH};
use axum::extract::Form;
use axum::{routing::get, routing::post, Json, Router};
use buzz_agent::auth::{PkceOAuthConfig, PkceOAuthTokenSource, TokenSource};
use serde::Deserialize;
use serde_json::json;
use sprout_agent::auth::{PkceOAuthConfig, PkceOAuthTokenSource, TokenSource};
use tempfile::TempDir;
#[derive(Deserialize)]
@@ -211,7 +211,7 @@ async fn refreshed_token_is_persisted_to_disk() {
// ────────────────────────────────────────────────────────────────────────────
// ACP-level envelope regression test.
//
// Boots the real sprout-agent binary with `DATABRICKS_TOKEN` set (so the
// Boots the real buzz-agent binary with `DATABRICKS_TOKEN` set (so the
// OAuth dance is skipped) pointed at a stub HTTP server that captures every
// inbound request. Asserts the wire-level shape Databricks model serving
// requires: path is `/serving-endpoints/<model>/invocations`, Authorization
@@ -332,21 +332,21 @@ impl Drop for AgentHarness {
impl AgentHarness {
async fn spawn_databricks(base_url: &str, model: &str) -> Self {
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "databricks")
cmd.env("BUZZ_AGENT_PROVIDER", "databricks")
.env("DATABRICKS_HOST", base_url)
.env("DATABRICKS_MODEL", model)
.env("DATABRICKS_TOKEN", "test-bearer")
.env("SPROUT_AGENT_LLM_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_MAX_ROUNDS", "2")
.env("SPROUT_AGENT_MCP_INIT_TIMEOUT_SECS", "2")
.env("BUZZ_AGENT_LLM_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_MAX_ROUNDS", "2")
.env("BUZZ_AGENT_MCP_INIT_TIMEOUT_SECS", "2")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
let mut child = cmd.spawn().expect("spawn sprout-agent");
let mut child = cmd.spawn().expect("spawn buzz-agent");
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
Self {
@@ -1,4 +1,4 @@
//! Integration test: fake LLM HTTP server + sprout-agent subprocess.
//! Integration test: fake LLM HTTP server + buzz-agent subprocess.
//!
//! Drives the agent through the ACP wire protocol and verifies:
//! - initialize / session/new responses
@@ -70,20 +70,20 @@ struct Harness {
impl Harness {
async fn spawn(base_url: &str) -> Self {
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "fake-model")
.env("OPENAI_COMPAT_BASE_URL", base_url)
.env("SPROUT_AGENT_LLM_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_MAX_ROUNDS", "4")
.env("BUZZ_AGENT_LLM_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_MAX_ROUNDS", "4")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true);
let mut child = cmd.spawn().expect("spawn sprout-agent");
let mut child = cmd.spawn().expect("spawn buzz-agent");
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
Self {
@@ -174,7 +174,7 @@ async fn init_session(h: &mut Harness) -> String {
.await;
let r = h.recv().await;
assert_eq!(r["result"]["protocolVersion"], 1);
assert_eq!(r["result"]["agentInfo"]["name"], "sprout-agent");
assert_eq!(r["result"]["agentInfo"]["name"], "buzz-agent");
h.send("session/new", json!({"cwd":"/tmp","mcpServers":[]}))
.await;
let r = h.recv().await;
@@ -313,13 +313,13 @@ async fn rejects_oversized_line() {
// Set a tiny max line and send something larger; agent must abort with an
// io error and not OOM.
let url = spawn_fake_llm(vec![]).await;
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "fake-model")
.env("OPENAI_COMPAT_BASE_URL", &url)
.env("SPROUT_AGENT_MAX_LINE_BYTES", "256")
.env("BUZZ_AGENT_MAX_LINE_BYTES", "256")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
@@ -17,14 +17,14 @@ struct Harness {
impl Harness {
async fn spawn(extra: &[(&str, &str)]) -> Self {
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "fake-model")
.env("SPROUT_AGENT_LLM_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_MAX_ROUNDS", "4")
.env("BUZZ_AGENT_LLM_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_MAX_ROUNDS", "4")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
@@ -32,7 +32,7 @@ impl Harness {
for (k, v) in extra {
cmd.env(k, v);
}
let mut child = cmd.spawn().expect("spawn sprout-agent");
let mut child = cmd.spawn().expect("spawn buzz-agent");
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
Self {
@@ -186,7 +186,7 @@ async fn handshake(h: &mut Harness) -> String {
.await;
let init = h.recv_for_id(init_id).await;
assert_eq!(init["result"]["protocolVersion"], 1);
assert_eq!(init["result"]["agentInfo"]["name"], "sprout-agent");
assert_eq!(init["result"]["agentInfo"]["name"], "buzz-agent");
assert_eq!(
init["result"]["agentCapabilities"]["promptCapabilities"]["image"],
false
@@ -470,13 +470,13 @@ async fn test_concurrent_prompt_rejected() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oversized_line_kills_agent() {
let url = spawn_fake_llm(vec![]).await;
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "fake-model")
.env("OPENAI_COMPAT_BASE_URL", &url)
.env("SPROUT_AGENT_MAX_LINE_BYTES", "256")
.env("BUZZ_AGENT_MAX_LINE_BYTES", "256")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
@@ -94,16 +94,16 @@ struct Harness {
impl Harness {
async fn spawn_with_env(base_url: &str, extra: &[(&str, &str)]) -> Self {
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "fake-model")
.env("OPENAI_COMPAT_BASE_URL", base_url)
.env("SPROUT_AGENT_LLM_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_MAX_ROUNDS", "8")
.env("SPROUT_AGENT_MCP_INIT_TIMEOUT_SECS", "2");
.env("BUZZ_AGENT_LLM_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_MAX_ROUNDS", "8")
.env("BUZZ_AGENT_MCP_INIT_TIMEOUT_SECS", "2");
for (k, v) in extra {
cmd.env(k, v);
}
@@ -111,7 +111,7 @@ impl Harness {
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true);
let mut child = cmd.spawn().expect("spawn sprout-agent");
let mut child = cmd.spawn().expect("spawn buzz-agent");
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
Self {
@@ -199,7 +199,7 @@ async fn init_session(h: &mut Harness, cwd: &str) -> String {
async fn hints_loaded_from_cwd_agents_md() {
let tmp = tempfile::TempDir::new().unwrap();
let cwd = tmp.path();
let marker = "SPROUT_HINTS_MARKER_42";
let marker = "BUZZ_HINTS_MARKER_42";
std::fs::write(cwd.join("AGENTS.md"), marker).unwrap();
let llm = spawn_capturing_llm(vec![openai_text("done")]).await;
@@ -224,7 +224,7 @@ async fn hints_loaded_from_cwd_agents_md() {
h.shutdown().await;
}
/// SPROUT_AGENT_NO_HINTS=1 suppresses hint loading.
/// BUZZ_AGENT_NO_HINTS=1 suppresses hint loading.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn hints_suppressed_with_env_var() {
let tmp = tempfile::TempDir::new().unwrap();
@@ -233,7 +233,7 @@ async fn hints_suppressed_with_env_var() {
std::fs::write(cwd.join("AGENTS.md"), marker).unwrap();
let llm = spawn_capturing_llm(vec![openai_text("done")]).await;
let mut h = Harness::spawn_with_env(&llm.url, &[("SPROUT_AGENT_NO_HINTS", "1")]).await;
let mut h = Harness::spawn_with_env(&llm.url, &[("BUZZ_AGENT_NO_HINTS", "1")]).await;
let sid = init_session(&mut h, cwd.to_str().unwrap()).await;
let p = h
@@ -6,7 +6,7 @@
//! 2. accepts a POST to /responses, replies 200 with a Responses-shaped
//! JSON envelope.
//!
//! Spawns `sprout-agent` with `provider=openai` + `OPENAI_COMPAT_API=auto`
//! Spawns `buzz-agent` with `provider=openai` + `OPENAI_COMPAT_API=auto`
//! pointed at the fake server, drives one prompt through the ACP wire
//! protocol, and verifies the prompt completes with `stopReason=end_turn`
//! — which can only happen if the second (Responses) request succeeded.
@@ -127,24 +127,24 @@ fn spawn_fake_provider() -> (String, Arc<AtomicUsize>, Arc<AtomicUsize>) {
async fn openai_auto_upgrades_chat_to_responses_on_databricks_signal() {
let (base_url, chat_hits, resp_hits) = spawn_fake_provider();
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "gpt-5.5")
.env("OPENAI_COMPAT_BASE_URL", &base_url)
// No OPENAI_COMPAT_API — must default to "auto" so the upgrade
// path is enabled.
.env_remove("OPENAI_COMPAT_API")
.env("SPROUT_AGENT_LLM_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_MAX_ROUNDS", "4")
.env("SPROUT_AGENT_MCP_INIT_TIMEOUT_SECS", "2")
.env("BUZZ_AGENT_LLM_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_MAX_ROUNDS", "4")
.env("BUZZ_AGENT_MCP_INIT_TIMEOUT_SECS", "2")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true);
let mut child = cmd.spawn().expect("spawn sprout-agent");
let mut child = cmd.spawn().expect("spawn buzz-agent");
let mut stdin = child.stdin.take().unwrap();
let mut stdout = BufReader::new(child.stdout.take().unwrap());
@@ -97,16 +97,16 @@ struct Harness {
impl Harness {
async fn spawn_with_env(base_url: &str, extra: &[(&str, &str)]) -> Self {
let bin = env!("CARGO_BIN_EXE_sprout-agent");
let bin = env!("CARGO_BIN_EXE_buzz-agent");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("SPROUT_AGENT_PROVIDER", "openai")
cmd.env("BUZZ_AGENT_PROVIDER", "openai")
.env("OPENAI_COMPAT_API_KEY", "test")
.env("OPENAI_COMPAT_MODEL", "fake-model")
.env("OPENAI_COMPAT_BASE_URL", base_url)
.env("SPROUT_AGENT_LLM_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("SPROUT_AGENT_MAX_ROUNDS", "8")
.env("SPROUT_AGENT_MCP_INIT_TIMEOUT_SECS", "2");
.env("BUZZ_AGENT_LLM_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_TOOL_TIMEOUT_SECS", "5")
.env("BUZZ_AGENT_MAX_ROUNDS", "8")
.env("BUZZ_AGENT_MCP_INIT_TIMEOUT_SECS", "2");
for (k, v) in extra {
cmd.env(k, v);
}
@@ -114,7 +114,7 @@ impl Harness {
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true);
let mut child = cmd.spawn().expect("spawn sprout-agent");
let mut child = cmd.spawn().expect("spawn buzz-agent");
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
Self {
@@ -540,8 +540,8 @@ async fn history_budget_evicts_old_turns() {
let mut h = Harness::spawn_with_env(
&llm.url,
&[
("SPROUT_AGENT_MAX_HISTORY_BYTES", &BUDGET.to_string()),
("SPROUT_AGENT_MAX_HANDOFFS", "0"), // exercise truncation, not handoff
("BUZZ_AGENT_MAX_HISTORY_BYTES", &BUDGET.to_string()),
("BUZZ_AGENT_MAX_HANDOFFS", "0"), // exercise truncation, not handoff
],
)
.await;
@@ -777,7 +777,7 @@ async fn hook_stop_blocks_premature_end() {
&llm.url,
&[
("MCP_HOOK_SERVERS", "fake"),
("SPROUT_AGENT_STOP_MAX_REJECTIONS", "10"),
("BUZZ_AGENT_STOP_MAX_REJECTIONS", "10"),
],
)
.await;
@@ -858,7 +858,7 @@ async fn hook_stop_budget_exhausted() {
&llm.url,
&[
("MCP_HOOK_SERVERS", "fake"),
("SPROUT_AGENT_STOP_MAX_REJECTIONS", "1"),
("BUZZ_AGENT_STOP_MAX_REJECTIONS", "1"),
],
)
.await;
@@ -907,7 +907,7 @@ async fn hook_stop_consecutive_end_turn() {
&[
("MCP_HOOK_SERVERS", "fake"),
// Set high so we don't trip the budget instead.
("SPROUT_AGENT_STOP_MAX_REJECTIONS", "10"),
("BUZZ_AGENT_STOP_MAX_REJECTIONS", "10"),
],
)
.await;
@@ -1046,9 +1046,9 @@ async fn hook_post_compact_injects_after_handoff() {
&llm.url,
&[
("MCP_HOOK_SERVERS", "fake"),
("SPROUT_AGENT_MAX_HISTORY_BYTES", &(1024 * 1024).to_string()),
("BUZZ_AGENT_MAX_HISTORY_BYTES", &(1024 * 1024).to_string()),
// Allow at least one handoff.
("SPROUT_AGENT_MAX_HANDOFFS", "3"),
("BUZZ_AGENT_MAX_HANDOFFS", "3"),
],
)
.await;
@@ -1157,13 +1157,13 @@ async fn token_usage_over_budget_triggers_handoff() {
let mut h = Harness::spawn_with_env(
&llm.url,
&[
("SPROUT_AGENT_MAX_CONTEXT_TOKENS", "1000"),
("SPROUT_AGENT_MAX_OUTPUT_TOKENS", "100"),
("SPROUT_AGENT_MAX_HANDOFFS", "3"),
("BUZZ_AGENT_MAX_CONTEXT_TOKENS", "1000"),
("BUZZ_AGENT_MAX_OUTPUT_TOKENS", "100"),
("BUZZ_AGENT_MAX_HANDOFFS", "3"),
// Huge byte budget so the byte path can NOT be what fires — only
// the token gate can explain a handoff on these tiny prompts.
(
"SPROUT_AGENT_MAX_HISTORY_BYTES",
"BUZZ_AGENT_MAX_HISTORY_BYTES",
&(16 * 1024 * 1024).to_string(),
),
],
@@ -1238,13 +1238,13 @@ async fn stale_usage_plus_history_growth_triggers_handoff() {
let mut h = Harness::spawn_with_env(
&llm.url,
&[
("SPROUT_AGENT_MAX_CONTEXT_TOKENS", "10000"),
("SPROUT_AGENT_MAX_OUTPUT_TOKENS", "1000"),
("SPROUT_AGENT_MAX_HANDOFFS", "3"),
("BUZZ_AGENT_MAX_CONTEXT_TOKENS", "10000"),
("BUZZ_AGENT_MAX_OUTPUT_TOKENS", "1000"),
("BUZZ_AGENT_MAX_HANDOFFS", "3"),
// Huge byte budget so the None-path byte fallback can't be what
// fires — only the token-mode growth estimate can explain it.
(
"SPROUT_AGENT_MAX_HISTORY_BYTES",
"BUZZ_AGENT_MAX_HISTORY_BYTES",
&(16 * 1024 * 1024).to_string(),
),
],
@@ -1277,7 +1277,7 @@ async fn stale_usage_plus_history_growth_triggers_handoff() {
h.shutdown().await;
}
/// `_Stop` hook that takes longer than `SPROUT_AGENT_HOOK_TIMEOUT_MS`
/// `_Stop` hook that takes longer than `BUZZ_AGENT_HOOK_TIMEOUT_MS`
/// must be treated as no-objection (fail-open). Agent stops normally.
///
/// Note on server-kill-on-timeout: `call_hooks` calls `kill_server` on a
@@ -1298,7 +1298,7 @@ async fn hook_stop_timeout_failopen() {
&[
("MCP_HOOK_SERVERS", "fake"),
// Hook delay (3s) >> hook timeout (200ms) → fail-open.
("SPROUT_AGENT_HOOK_TIMEOUT_MS", "200"),
("BUZZ_AGENT_HOOK_TIMEOUT_MS", "200"),
],
)
.await;
@@ -1344,20 +1344,20 @@ async fn hook_stop_timeout_failopen() {
}
/// When a session is cancelled while a tool call is in-flight, the agent
/// sends `notifications/cancelled` to the MCP server. With sprout-dev-mcp,
/// sends `notifications/cancelled` to the MCP server. With buzz-dev-mcp,
/// this cancels the CancellationToken and kills the running shell process
/// group. We verify:
/// 1. The prompt completes in under 5s (not 60s).
/// 2. The `sleep 60` process is actually dead after cancel.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cancel_kills_inflight_tool_via_mcp_notification() {
// sprout-dev-mcp is a separate crate; locate its binary relative to
// the sprout-agent test binary (they share the same target dir).
let self_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_sprout-agent"));
let dev_mcp_bin = self_bin.parent().unwrap().join("sprout-dev-mcp");
// buzz-dev-mcp is a separate crate; locate its binary relative to
// the buzz-agent test binary (they share the same target dir).
let self_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_buzz-agent"));
let dev_mcp_bin = self_bin.parent().unwrap().join("buzz-dev-mcp");
if !dev_mcp_bin.exists() {
eprintln!(
"SKIP: sprout-dev-mcp not built at {}; run `cargo build -p sprout-dev-mcp` first",
"SKIP: buzz-dev-mcp not built at {}; run `cargo build -p buzz-dev-mcp` first",
dev_mcp_bin.display()
);
return;
@@ -1366,7 +1366,7 @@ async fn cancel_kills_inflight_tool_via_mcp_notification() {
// Use a unique marker (PID + timestamp) to avoid stale-file collisions.
let marker = format!(
"sprout_cancel_test_{}_{:x}",
"buzz_cancel_test_{}_{:x}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -1467,14 +1467,14 @@ async fn cancel_kills_inflight_tool_via_mcp_notification() {
}
/// Protocol-level test: verify that `notifications/cancelled` is sent to
/// any MCP server (not just sprout-dev-mcp) when a session is cancelled
/// any MCP server (not just buzz-dev-mcp) when a session is cancelled
/// during an in-flight tool call. Uses fake_mcp with FAKE_MCP_CANCEL_LOG
/// to capture the raw notification on stdin.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cancel_sends_notifications_cancelled_to_any_mcp_server() {
let cancel_log = std::env::temp_dir()
.join(format!(
"sprout_cancel_proto_{}_{:x}.log",
"buzz_cancel_proto_{}_{:x}.log",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -1,14 +1,14 @@
[package]
name = "sprout-audit"
name = "buzz-audit"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Hash-chain audit log for Sprout"
description = "Hash-chain audit log for Buzz"
[dependencies]
sprout-core = { workspace = true }
buzz-core = { workspace = true }
sqlx = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
@@ -3,7 +3,7 @@ use futures_util::FutureExt as _;
use sqlx::{Acquire, PgPool, Row};
use tracing::{debug, instrument, warn};
use sprout_core::kind::KIND_AUTH;
use buzz_core::kind::KIND_AUTH;
use crate::{
action::AuditAction,
@@ -13,7 +13,7 @@ use crate::{
schema::AUDIT_SCHEMA_SQL,
};
/// Advisory lock key derived from a stable hash of "sprout_audit".
/// Advisory lock key derived from a stable hash of "buzz_audit".
const AUDIT_LOCK_KEY: i64 = 0x5370_7275_7441_7564; // "SprutAud" as hex
/// Append-only audit log service backed by Postgres.
@@ -266,7 +266,7 @@ mod tests {
async fn test_pool() -> Option<PgPool> {
let url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://sprout:sprout_dev@localhost:5432/sprout".into());
.unwrap_or_else(|_| "postgres://buzz:buzz_dev@localhost:5432/buzz".into());
PgPool::connect(&url).await.ok()
}
@@ -1,18 +1,18 @@
[package]
name = "sprout-auth"
name = "buzz-auth"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Authentication and authorization for Sprout"
description = "Authentication and authorization for Buzz"
[features]
test-utils = []
dev = []
[dependencies]
sprout-core = { workspace = true }
buzz-core = { workspace = true }
nostr = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -1,7 +1,7 @@
//! Channel access enforcement.
//!
//! Defines [`ChannelAccessChecker`] so `sprout-auth` can enforce access
//! without depending on `sprout-db` directly.
//! Defines [`ChannelAccessChecker`] so `buzz-auth` can enforce access
//! without depending on `buzz-db` directly.
use std::collections::HashSet;
use std::future::Future;
@@ -14,9 +14,9 @@ use crate::scope::Scope;
/// Async trait for checking channel membership.
///
/// Implemented by the database layer (`sprout-db`) in production. The `sprout-auth`
/// Implemented by the database layer (`buzz-db`) in production. The `buzz-auth`
/// crate defines the trait so it can enforce access rules without a direct dependency
/// on `sprout-db`.
/// on `buzz-db`.
pub trait ChannelAccessChecker: Send + Sync {
/// Return the set of channel UUIDs accessible to `pubkey`.
fn accessible_channel_ids(
@@ -1,4 +1,4 @@
//! Error types for sprout-auth.
//! Error types for buzz-auth.
/// All errors that can occur during authentication and authorization.
///
@@ -1,6 +1,6 @@
#![deny(unsafe_code)]
#![warn(missing_docs)]
//! `sprout-auth` — Authentication and authorization for the Sprout relay.
//! `buzz-auth` — Authentication and authorization for the Buzz relay.
//!
//! ## Auth paths
//!
@@ -137,7 +137,7 @@ impl AuthService {
/// Derive a deterministic Nostr pubkey from a username string.
///
/// Uses `SHA-256("sprout-test-key:{username}")` as the secret key material.
/// Uses `SHA-256("buzz-test-key:{username}")` as the secret key material.
/// This matches the derivation used by the desktop's `set_test_identity` function,
/// allowing the relay to resolve usernames to Nostr pubkeys in dev mode.
///
@@ -151,7 +151,7 @@ impl AuthService {
#[cfg(any(test, feature = "dev"))]
pub fn derive_pubkey_from_username(username: &str) -> Result<nostr::PublicKey, AuthError> {
use sha2::{Digest, Sha256};
let seed = format!("sprout-test-key:{username}");
let seed = format!("buzz-test-key:{username}");
let hash: [u8; 32] = Sha256::digest(seed.as_bytes()).into();
let secret_key = nostr::SecretKey::from_slice(&hash)
.map_err(|e| AuthError::Internal(format!("key derivation failed: {e}")))?;
@@ -53,7 +53,7 @@ pub fn verify_nip42_event(
return Err(AuthError::InvalidSignature);
}
sprout_core::verify_event(event).map_err(|_| AuthError::InvalidSignature)?;
buzz_core::verify_event(event).map_err(|_| AuthError::InvalidSignature)?;
let challenge = event
.tags
@@ -14,7 +14,7 @@
//!
//! 1. Parse JSON into a `nostr::Event`
//! 2. Verify `kind == 27235` (`Kind::HttpAuth`)
//! 3. Verify Schnorr signature via `sprout_core::verify_event`
//! 3. Verify Schnorr signature via `buzz_core::verify_event`
//! 4. Verify `created_at` within ±60 seconds of server time
//! 5. Verify `["u", <url>]` tag matches `expected_url` (normalised: case-insensitive
//! scheme/host, trailing slash stripped)
@@ -71,7 +71,7 @@ pub fn verify_nip98_event(
}
// 3. Verify Schnorr signature (also verifies event ID hash).
sprout_core::verify_event(&event)
buzz_core::verify_event(&event)
.map_err(|_| AuthError::Nip98Invalid("invalid Schnorr signature".to_string()))?;
// 4. Verify created_at within ±60 seconds of now.
@@ -1,7 +1,7 @@
//! Rate limiting types and interface.
//!
//! Defines the [`RateLimiter`] trait. The Redis-backed implementation lives in
//! `sprout-relay` / `sprout-pubsub`. Fixed-window counter algorithm.
//! `buzz-relay` / `buzz-pubsub`. Fixed-window counter algorithm.
//!
//! ⚠️ Fixed windows allow up to 2× burst at boundaries. Upgrade to sliding
//! window or token bucket for strict limiting.
@@ -144,7 +144,7 @@ impl Default for RateLimitConfig {
/// Async rate-limiting interface.
///
/// The Redis-backed production implementation lives in `sprout-relay` / `sprout-pubsub`.
/// The Redis-backed production implementation lives in `buzz-relay` / `buzz-pubsub`.
/// A no-op `AlwaysAllowRateLimiter` is provided for unit tests.
///
/// ⚠️ The fixed-window algorithm used by the Redis implementation allows up to 2×
@@ -171,18 +171,18 @@ pub trait RateLimiter: Send + Sync {
) -> impl std::future::Future<Output = Result<RateLimitResult, AuthError>> + Send;
}
/// Redis key for pubkey-based rate limit: `sprout:ratelimit:<hex>:<suffix>`
/// Redis key for pubkey-based rate limit: `buzz:ratelimit:<hex>:<suffix>`
pub fn rate_limit_key(pubkey: &PublicKey, limit_type: &LimitType) -> String {
format!(
"sprout:ratelimit:{}:{}",
"buzz:ratelimit:{}:{}",
pubkey.to_hex(),
limit_type.key_suffix()
)
}
/// Redis key for IP-based rate limit: `sprout:ratelimit:ip:<ip>:conn`
/// Redis key for IP-based rate limit: `buzz:ratelimit:ip:<ip>:conn`
pub fn ip_rate_limit_key(ip: &IpAddr) -> String {
format!("sprout:ratelimit:ip:{}:conn", ip)
format!("buzz:ratelimit:ip:{}:conn", ip)
}
/// Always-allow rate limiter for unit tests.
@@ -221,17 +221,14 @@ mod tests {
fn rate_limit_key_format() {
let keys = Keys::generate();
let key = rate_limit_key(&keys.public_key(), &LimitType::Messages);
assert!(key.starts_with("sprout:ratelimit:"));
assert!(key.starts_with("buzz:ratelimit:"));
assert!(key.ends_with(":msg"));
}
#[test]
fn ip_rate_limit_key_format() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(
ip_rate_limit_key(&ip),
"sprout:ratelimit:ip:192.168.1.1:conn"
);
assert_eq!(ip_rate_limit_key(&ip), "buzz:ratelimit:ip:192.168.1.1:conn");
}
#[tokio::test]
@@ -1,22 +1,22 @@
[package]
name = "sprout-cli"
name = "buzz-cli"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Agent-first CLI for Sprout relay"
description = "Agent-first CLI for Buzz relay"
[lib]
name = "sprout_cli"
name = "buzz_cli"
path = "src/lib.rs"
[[bin]]
name = "sprout"
name = "buzz"
path = "src/main.rs"
[dependencies]
# CLI argument parsing — derive macros + env var support (SPROUT_API_TOKEN auto-wired)
# CLI argument parsing — derive macros + env var support (BUZZ_API_TOKEN auto-wired)
clap = { version = "4", features = ["derive", "env"] }
# HTTP client — async REST calls to the relay
@@ -32,15 +32,15 @@ serde_json = { workspace = true }
# Structured error types with exit code mapping
thiserror = { workspace = true }
# Nostr event signing — used in `sprout auth`, auto-mint, and signed event writes
# Nostr event signing — used in `buzz auth`, auto-mint, and signed event writes
nostr = { workspace = true }
# UUID parsing for validate_uuid + event building
uuid = { workspace = true }
# Typed event builders for all write operations
sprout-sdk = { workspace = true }
sprout-core = { workspace = true }
buzz-sdk = { workspace = true }
buzz-core = { workspace = true }
# Base64 encoding — NIP-98 event serialization for Authorization header
base64 = "0.22"
@@ -61,7 +61,7 @@ infer = "0.19"
url = { workspace = true }
# Persona pack parsing, validation, and resolution
sprout-persona = { path = "../sprout-persona" }
buzz-persona = { path = "../buzz-persona" }
# WebSocket client — ephemeral event publish (kind:20001 is WS-only on the relay)
sprout-ws-client = { path = "../sprout-ws-client" }
buzz-ws-client = { path = "../buzz-ws-client" }
@@ -1,23 +1,23 @@
# Sprout CLI
# Buzz CLI
Agent-first command-line interface for Sprout relay. JSON in, JSON out.
Agent-first command-line interface for Buzz relay. JSON in, JSON out.
## Install
```bash
cargo install --path crates/sprout-cli
cargo install --path crates/buzz-cli
```
## Authentication
| Env Var | Mode | Use Case |
|---------|------|----------|
| `SPROUT_PRIVATE_KEY` | NIP-98 Schnorr signature | Agents with a keypair |
| `BUZZ_PRIVATE_KEY` | NIP-98 Schnorr signature | Agents with a keypair |
```bash
# Private key identity (NIP-98 signed requests)
export SPROUT_PRIVATE_KEY="nsec1..."
sprout channels list
export BUZZ_PRIVATE_KEY="nsec1..."
buzz channels list
```
## Usage
@@ -26,56 +26,56 @@ All output is JSON on stdout. Errors are JSON on stderr. Exit codes: 0=ok, 1=use
```bash
# Set relay URL (defaults to http://localhost:3000)
export SPROUT_RELAY_URL="https://relay.example.com"
export BUZZ_RELAY_URL="https://relay.example.com"
# Messages
sprout messages send --channel <uuid> --content "Hello"
sprout messages send --channel <uuid> --content "Reply" --reply-to <event-id> --broadcast
sprout messages send --channel <uuid> --content - < message.md # read body from stdin
sprout messages get --channel <uuid> --limit 20
sprout messages thread --channel <uuid> --event <event-id>
sprout messages search --query "architecture"
sprout messages edit --event <event-id> --content "Updated text"
sprout messages delete --event <event-id>
buzz messages send --channel <uuid> --content "Hello"
buzz messages send --channel <uuid> --content "Reply" --reply-to <event-id> --broadcast
buzz messages send --channel <uuid> --content - < message.md # read body from stdin
buzz messages get --channel <uuid> --limit 20
buzz messages thread --channel <uuid> --event <event-id>
buzz messages search --query "architecture"
buzz messages edit --event <event-id> --content "Updated text"
buzz messages delete --event <event-id>
# Diffs
sprout messages send-diff --channel <uuid> --diff - --repo https://github.com/org/repo --commit abc123 < diff.patch
buzz messages send-diff --channel <uuid> --diff - --repo https://github.com/org/repo --commit abc123 < diff.patch
# Channels
sprout channels list
sprout channels create --name "my-channel" --type stream --visibility open
sprout channels join --channel <uuid>
sprout channels topic --channel <uuid> --topic "New topic"
buzz channels list
buzz channels create --name "my-channel" --type stream --visibility open
buzz channels join --channel <uuid>
buzz channels topic --channel <uuid> --topic "New topic"
# Reactions
sprout reactions add --event <event-id> --emoji "👍"
sprout reactions get --event <event-id>
buzz reactions add --event <event-id> --emoji "👍"
buzz reactions get --event <event-id>
# Users & Presence
sprout users get # your own profile
sprout users get --pubkey <hex> # single user
sprout users get --pubkey <hex> --pubkey <hex> # batch (max 200)
sprout users set-presence --status online
buzz users get # your own profile
buzz users get --pubkey <hex> # single user
buzz users get --pubkey <hex> --pubkey <hex> # batch (max 200)
buzz users set-presence --status online
# DMs
sprout dms open --pubkey <hex>
sprout dms list
buzz dms open --pubkey <hex>
buzz dms list
# Workflows
sprout workflows list --channel <uuid>
sprout workflows trigger --workflow <uuid>
sprout workflows approve --token <uuid>
sprout workflows approve --token <uuid> --approved false --note "needs revision"
buzz workflows list --channel <uuid>
buzz workflows trigger --workflow <uuid>
buzz workflows approve --token <uuid>
buzz workflows approve --token <uuid> --approved false --note "needs revision"
# Forum
sprout messages vote --event <event-id> --direction up
buzz messages vote --event <event-id> --direction up
# Canvas
sprout canvas get --channel <uuid>
sprout canvas set --channel <uuid> --content "# Welcome"
buzz canvas get --channel <uuid>
buzz canvas set --channel <uuid> --content "# Welcome"
# Pipe to jq
sprout channels list | jq '.[].name'
buzz channels list | jq '.[].name'
```
## 54 Subcommands across 12 Groups
@@ -140,9 +140,9 @@ sprout channels list | jq '.[].name'
## Architecture
```
sprout <group> <subcommand> [flags]
buzz <group> <subcommand> [flags]
│
├─ main.rs ──▶ commands/*.rs ──▶ client.rs ──▶ Sprout Relay REST API
├─ main.rs ──▶ commands/*.rs ──▶ client.rs ──▶ Buzz Relay REST API
│ (clap) (handlers) (reqwest)
│
├─ validate.rs (UUID, hex, content size, percent-encode)
@@ -1,4 +1,4 @@
# sprout-cli Live Testing Guide
# buzz-cli Live Testing Guide
Manual testing runbook for verifying every CLI command against a local relay.
An agent or developer follows this step by step, running each command and
@@ -12,9 +12,9 @@ Docker services running and healthy:
```bash
docker compose ps
# sprout-postgres healthy
# sprout-redis healthy
# sprout-typesense healthy
# buzz-postgres healthy
# buzz-redis healthy
# buzz-typesense healthy
```
If not running: `just setup` from the repo root.
@@ -26,10 +26,10 @@ Tools: `jq`, `curl`, Rust toolchain.
## 2. Build the CLI
```bash
cargo build -p sprout-cli
cargo build -p buzz-cli
```
Use `cargo run -p sprout-cli --` or the built binary at `target/debug/sprout`.
Use `cargo run -p buzz-cli --` or the built binary at `target/debug/buzz`.
---
@@ -38,9 +38,9 @@ Use `cargo run -p sprout-cli --` or the built binary at `target/debug/sprout`.
In a separate terminal:
```bash
cd REPOS/sprout-nostr
cd REPOS/buzz-nostr
set -a && source .env && set +a
cargo run -p sprout-relay
cargo run -p buzz-relay
```
Verify:
@@ -50,33 +50,33 @@ curl -s http://localhost:3000/_liveness
# "ok" or 200 status
```
The `.env` should have `SPROUT_REQUIRE_AUTH_TOKEN=false` for local dev.
The `.env` should have `BUZZ_REQUIRE_AUTH_TOKEN=false` for local dev.
---
## 4. Mint Test Credentials
### Option A: sprout-admin (full scopes including admin)
### Option A: buzz-admin (full scopes including admin)
This mints a token with all CLI-relevant scopes (including `admin:channels`)
via direct DB access. Use this for testing admin operations (archive,
delete-channel, add/remove-channel-member).
```bash
DATABASE_URL=postgres://sprout:sprout_dev@localhost:5432/sprout \
cargo run -p sprout-admin -- mint-token \
DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz \
cargo run -p buzz-admin -- mint-token \
--name "cli-test" \
--scopes "messages:read,messages:write,channels:read,channels:write,users:read,users:write,files:read,files:write,admin:channels"
```
This generates a keypair and prints:
- **Private key (nsec)** — save for `SPROUT_PRIVATE_KEY` testing
- **Private key (nsec)** — save for `BUZZ_PRIVATE_KEY` testing
Export:
```bash
export SPROUT_RELAY_URL="http://localhost:3000"
export SPROUT_PRIVATE_KEY="nsec1..." # from the mint output
export BUZZ_RELAY_URL="http://localhost:3000"
export BUZZ_PRIVATE_KEY="nsec1..." # from the mint output
```
### Scope reference
@@ -98,10 +98,10 @@ export SPROUT_PRIVATE_KEY="nsec1..." # from the mint output
## 5. Unit Tests
```bash
cargo test -p sprout-cli
# Expected: see cargo test -p sprout-cli for current count
cargo test -p buzz-cli
# Expected: see cargo test -p buzz-cli for current count
cargo clippy -p sprout-cli -- -D warnings
cargo clippy -p buzz-cli -- -D warnings
# Expected: zero warnings
```
@@ -117,59 +117,59 @@ earlier ones create resources that later ones need.
```bash
# channels create (stream)
sprout channels create --name "test-stream" --type stream --visibility open \
buzz channels create --name "test-stream" --type stream --visibility open \
--description "CLI test channel" | jq .
# Save the channel ID:
CHANNEL_ID=$(sprout channels create --name "test-cli" --type stream --visibility open | jq -r '.channel_id')
CHANNEL_ID=$(buzz channels create --name "test-cli" --type stream --visibility open | jq -r '.channel_id')
# Expected: {"event_id":"...","accepted":true,"message":"...","channel_id":"<uuid>"}
# channels create (forum) — needed for messages vote later
FORUM_ID=$(sprout channels create --name "test-forum" --type forum --visibility open | jq -r '.channel_id')
FORUM_ID=$(buzz channels create --name "test-forum" --type forum --visibility open | jq -r '.channel_id')
# channels list
sprout channels list | jq .
buzz channels list | jq .
# Expected: [{"channel_id":"...","name":"...","description":"...","created_at":N}]
sprout channels list --visibility open | jq .
sprout channels list --member | jq .
buzz channels list --visibility open | jq .
buzz channels list --member | jq .
# channels get
sprout channels get --channel "$CHANNEL_ID" | jq .
buzz channels get --channel "$CHANNEL_ID" | jq .
# Expected: {"channel_id":"...","name":"...","description":"...","created_at":N,"pubkey":"..."} or null
# channels update
sprout channels update --channel "$CHANNEL_ID" --name "test-cli-updated" \
buzz channels update --channel "$CHANNEL_ID" --name "test-cli-updated" \
--description "Updated" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# channels topic
sprout channels topic --channel "$CHANNEL_ID" --topic "Test topic" | jq .
buzz channels topic --channel "$CHANNEL_ID" --topic "Test topic" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# channels purpose
sprout channels purpose --channel "$CHANNEL_ID" --purpose "Testing" | jq .
buzz channels purpose --channel "$CHANNEL_ID" --purpose "Testing" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# channels join (may already be a member from create)
sprout channels join --channel "$CHANNEL_ID" | jq .
buzz channels join --channel "$CHANNEL_ID" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# channels leave
# NOTE: Fails with 400 "cannot remove the last owner" if this identity is the
# sole owner (which it is after channels create). To test leave successfully,
# first add-member a second pubkey as owner. The relay enforces ≥1 owner.
sprout channels leave --channel "$CHANNEL_ID" | jq .
buzz channels leave --channel "$CHANNEL_ID" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."} (or 400 if last owner)
# Re-join so we can send messages
sprout channels join --channel "$CHANNEL_ID" | jq .
buzz channels join --channel "$CHANNEL_ID" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# channels archive (requires admin:channels scope)
sprout channels archive --channel "$CHANNEL_ID" | jq .
buzz channels archive --channel "$CHANNEL_ID" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# channels unarchive
sprout channels unarchive --channel "$CHANNEL_ID" | jq .
buzz channels unarchive --channel "$CHANNEL_ID" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
```
@@ -177,13 +177,13 @@ sprout channels unarchive --channel "$CHANNEL_ID" | jq .
```bash
# canvas set
sprout canvas set --channel "$CHANNEL_ID" --content "# Test Canvas" | jq .
buzz canvas set --channel "$CHANNEL_ID" --content "# Test Canvas" | jq .
# canvas set from stdin
echo "# Canvas from stdin" | sprout canvas set --channel "$CHANNEL_ID" --content - | jq .
echo "# Canvas from stdin" | buzz canvas set --channel "$CHANNEL_ID" --content - | jq .
# canvas get
sprout canvas get --channel "$CHANNEL_ID"
buzz canvas get --channel "$CHANNEL_ID"
# Expected: raw markdown string, or: null
```
@@ -191,44 +191,44 @@ sprout canvas get --channel "$CHANNEL_ID"
```bash
# messages send
MSG=$(sprout messages send --channel "$CHANNEL_ID" --content "Hello from CLI test" | jq .)
MSG=$(buzz messages send --channel "$CHANNEL_ID" --content "Hello from CLI test" | jq .)
echo "$MSG"
EVENT_ID=$(echo "$MSG" | jq -r '.event_id')
# messages send with reply + broadcast
REPLY=$(sprout messages send --channel "$CHANNEL_ID" --content "Reply" \
REPLY=$(buzz messages send --channel "$CHANNEL_ID" --content "Reply" \
--reply-to "$EVENT_ID" --broadcast | jq .)
echo "$REPLY"
REPLY_ID=$(echo "$REPLY" | jq -r '.event_id')
# messages send with mentions — @name in content is auto-resolved, no flag needed
sprout messages send --channel "$CHANNEL_ID" --content "Hey @someone" | jq .
buzz messages send --channel "$CHANNEL_ID" --content "Hey @someone" | jq .
# messages send with NIP-27 nostr:npub1… inline mention — auto-resolved to p-tag
sprout messages send --channel "$CHANNEL_ID" \
buzz messages send --channel "$CHANNEL_ID" \
--content "Check with nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg on this" | jq .
# messages send from stdin — safe path for content with shell metacharacters
# (backticks, $vars, code blocks) that would otherwise be expanded by the shell.
echo 'Body with `backticks` and $vars stays literal.' \
| sprout messages send --channel "$CHANNEL_ID" --content - | jq .
| buzz messages send --channel "$CHANNEL_ID" --content - | jq .
# messages get
sprout messages get --channel "$CHANNEL_ID" | jq .
sprout messages get --channel "$CHANNEL_ID" --limit 5 | jq .
buzz messages get --channel "$CHANNEL_ID" | jq .
buzz messages get --channel "$CHANNEL_ID" --limit 5 | jq .
# messages thread
sprout messages thread --channel "$CHANNEL_ID" --event "$EVENT_ID" | jq .
buzz messages thread --channel "$CHANNEL_ID" --event "$EVENT_ID" | jq .
# messages search
sprout messages search --query "Hello" | jq .
sprout messages search --query "CLI test" --limit 5 | jq .
buzz messages search --query "Hello" | jq .
buzz messages search --query "CLI test" --limit 5 | jq .
# messages edit
sprout messages edit --event "$EVENT_ID" --content "Edited by CLI test" | jq .
buzz messages edit --event "$EVENT_ID" --content "Edited by CLI test" | jq .
# messages delete
sprout messages delete --event "$REPLY_ID" | jq .
buzz messages delete --event "$REPLY_ID" | jq .
```
### 6.4 Diff Messages
@@ -239,14 +239,14 @@ echo '--- a/foo.rs
+++ b/foo.rs
@@ -1,3 +1,3 @@
-fn old() {}
+fn new() {}' | sprout messages send-diff \
+fn new() {}' | buzz messages send-diff \
--channel "$CHANNEL_ID" \
--diff - \
--repo "https://github.com/example/repo" \
--commit "abcdef1234567890abcdef1234567890abcdef12" | jq .
# messages send-diff with metadata
echo "diff content" | sprout messages send-diff \
echo "diff content" | buzz messages send-diff \
--channel "$CHANNEL_ID" \
--diff - \
--repo "https://github.com/example/repo" \
@@ -256,7 +256,7 @@ echo "diff content" | sprout messages send-diff \
--description "Refactored main" | jq .
# messages send-diff with branch + PR metadata
echo "diff content" | sprout messages send-diff \
echo "diff content" | buzz messages send-diff \
--channel "$CHANNEL_ID" \
--diff - \
--repo "https://github.com/example/repo" \
@@ -271,40 +271,40 @@ echo "diff content" | sprout messages send-diff \
```bash
# Send a message to react to
REACT_MSG=$(sprout messages send --channel "$CHANNEL_ID" --content "React to this")
REACT_MSG=$(buzz messages send --channel "$CHANNEL_ID" --content "React to this")
REACT_ID=$(echo "$REACT_MSG" | jq -r '.event_id')
# reactions add
sprout reactions add --event "$REACT_ID" --emoji "👍" | jq .
buzz reactions add --event "$REACT_ID" --emoji "👍" | jq .
# reactions get
sprout reactions get --event "$REACT_ID" | jq .
buzz reactions get --event "$REACT_ID" | jq .
# Expected: {"reactions":[{"emoji":"...","count":N,"pubkeys":["..."]}]}
# reactions remove
sprout reactions remove --event "$REACT_ID" --emoji "👍" | jq .
buzz reactions remove --event "$REACT_ID" --emoji "👍" | jq .
```
### 6.6 DMs
```bash
# dms list
sprout dms list | jq .
buzz dms list | jq .
# Expected: [{"dm_id":"...","participants":["..."],"created_at":N}]
# dms open (needs a real pubkey — use your own or a test one)
# Get your own pubkey first:
MY_PUBKEY=$(sprout users get | jq -r '.[0].pubkey // empty')
MY_PUBKEY=$(buzz users get | jq -r '.[0].pubkey // empty')
echo "My pubkey: $MY_PUBKEY"
# dms open with a synthetic pubkey (relay will create the user)
DM_RESULT=$(sprout dms open --pubkey "0000000000000000000000000000000000000000000000000000000000000001")
DM_RESULT=$(buzz dms open --pubkey "0000000000000000000000000000000000000000000000000000000000000001")
echo "$DM_RESULT" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"...","dm_id":"<uuid>"}
DM_ID=$(echo "$DM_RESULT" | jq -r '.dm_id')
# dms add-member (requires messages:write scope — NOT admin:channels)
sprout dms add-member --channel "$DM_ID" \
buzz dms add-member --channel "$DM_ID" \
--pubkey "0000000000000000000000000000000000000000000000000000000000000002" | jq .
```
@@ -312,25 +312,25 @@ sprout dms add-member --channel "$DM_ID" \
```bash
# users get — own profile (0 pubkeys)
sprout users get | jq .
buzz users get | jq .
# Expected: [{...profile...}] — always returns an array, even for single results
# users get — single pubkey
sprout users get --pubkey "$MY_PUBKEY" | jq .
buzz users get --pubkey "$MY_PUBKEY" | jq .
# users get — batch (2+ pubkeys)
sprout users get --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
buzz users get --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
# users set-profile
sprout users set-profile --name "CLI Test Agent" --about "Testing sprout-cli" | jq .
buzz users set-profile --name "CLI Test Agent" --about "Testing buzz-cli" | jq .
# users presence
sprout users presence --pubkeys "$MY_PUBKEY" | jq .
buzz users presence --pubkeys "$MY_PUBKEY" | jq .
# users set-presence
sprout users set-presence --status online | jq .
sprout users set-presence --status away | jq .
sprout users set-presence --status offline | jq .
buzz users set-presence --status online | jq .
buzz users set-presence --status away | jq .
buzz users set-presence --status offline | jq .
# Note: set-presence may fail — kind:20001 is ephemeral and rejected by the HTTP bridge
```
@@ -338,16 +338,16 @@ sprout users set-presence --status offline | jq .
```bash
# channels add-member
sprout channels add-member --channel "$CHANNEL_ID" \
buzz channels add-member --channel "$CHANNEL_ID" \
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" \
--role member | jq .
# channels members
sprout channels members --channel "$CHANNEL_ID" | jq .
buzz channels members --channel "$CHANNEL_ID" | jq .
# Expected: [{"pubkey":"...","role":"..."}]
# channels remove-member
sprout channels remove-member --channel "$CHANNEL_ID" \
buzz channels remove-member --channel "$CHANNEL_ID" \
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" | jq .
```
@@ -358,7 +358,7 @@ sprout channels remove-member --channel "$CHANNEL_ID" \
# NOTE: trigger uses `on:` tag (serde internally tagged enum).
# Valid triggers: message_posted, reaction_added, diff_posted, schedule, webhook
# Steps use `action:` tag: send_message, send_dm, set_channel_topic, add_reaction, etc.
WF=$(sprout workflows create --channel "$CHANNEL_ID" \
WF=$(buzz workflows create --channel "$CHANNEL_ID" \
--yaml 'name: test-wf
trigger:
on: webhook
@@ -370,14 +370,14 @@ echo "$WF"
WF_ID=$(echo "$WF" | jq -r '.workflow_id')
# workflows list
sprout workflows list --channel "$CHANNEL_ID" | jq .
buzz workflows list --channel "$CHANNEL_ID" | jq .
# workflows get
sprout workflows get --workflow "$WF_ID" | jq .
buzz workflows get --workflow "$WF_ID" | jq .
# Expected: {"workflow_id":"...","content":"<yaml>","created_at":N,"pubkey":"..."} or null
# workflows update (requires --channel)
sprout workflows update --channel "$CHANNEL_ID" --workflow "$WF_ID" \
buzz workflows update --channel "$CHANNEL_ID" --workflow "$WF_ID" \
--yaml 'name: test-wf-updated
trigger:
on: webhook
@@ -390,28 +390,28 @@ steps:
# NOTE: May return 400 "workflow not found" — the relay indexes workflow
# definitions into a DB table asynchronously. If the definition event hasn't
# been indexed yet, the trigger handler won't find it.
sprout workflows trigger --workflow "$WF_ID" | jq .
buzz workflows trigger --workflow "$WF_ID" | jq .
# workflows runs
sprout workflows runs --workflow "$WF_ID" | jq .
buzz workflows runs --workflow "$WF_ID" | jq .
# Expected: [] — relay stores runs in DB, not as Nostr events; empty is normal
# workflows approve — requires a workflow run waiting for approval
# This is hard to test ad-hoc without a workflow that has an approval gate.
# Test the validation instead:
sprout workflows approve --token "00000000-0000-0000-0000-000000000000" 2>&1 || true
buzz workflows approve --token "00000000-0000-0000-0000-000000000000" 2>&1 || true
# Should fail with relay error (token not found), not a validation error
# To test the deny path: sprout workflows approve --token <UUID> --approved false
# To test the deny path: buzz workflows approve --token <UUID> --approved false
# workflows delete
sprout workflows delete --workflow "$WF_ID" | jq .
buzz workflows delete --workflow "$WF_ID" | jq .
```
### 6.10 Feed
```bash
sprout feed get | jq .
sprout feed get --limit 5 | jq .
buzz feed get | jq .
buzz feed get --limit 5 | jq .
# Expected: [{id,pubkey,kind,content,created_at,tags}] — sig-stripped, sorted newest-first
```
@@ -419,16 +419,16 @@ sprout feed get --limit 5 | jq .
```bash
# Send a forum post (kind 45001) to the forum channel
FORUM_POST=$(sprout messages send --channel "$FORUM_ID" \
FORUM_POST=$(buzz messages send --channel "$FORUM_ID" \
--content "Forum post for vote testing" --kind 45001 | jq .)
echo "$FORUM_POST"
FORUM_EVENT_ID=$(echo "$FORUM_POST" | jq -r '.event_id')
# messages vote (up)
sprout messages vote --event "$FORUM_EVENT_ID" --direction up | jq .
buzz messages vote --event "$FORUM_EVENT_ID" --direction up | jq .
# messages vote (down)
sprout messages vote --event "$FORUM_EVENT_ID" --direction down | jq .
buzz messages vote --event "$FORUM_EVENT_ID" --direction down | jq .
```
### 6.12 Notes (NIP-23 long-form, kind:30023)
@@ -439,34 +439,34 @@ not JSON — except `get`/`ls`, which emit JSON.
```bash
# set (first publish — --title required, body from stdin)
cat <<'EOF' | sprout notes set --name dco-check --title "DCO Check" \
cat <<'EOF' | buzz notes set --name dco-check --title "DCO Check" \
--summary "How we verify DCO" --tag dco --tag ci --content -
Run `git log --format='%(trailers:key=Signed-off-by)'` ...
EOF
# → prints event_id / naddr / coordinate / slug / title
# set (edit — omit --title to carry it forward; published_at preserved)
echo "Updated body." | sprout notes set --name dco-check --content -
echo "Updated body." | buzz notes set --name dco-check --content -
# get by name (own author resolves directly; cross-author #d query otherwise)
sprout notes get --name dco-check | jq .
sprout notes get --name dco-check --content-only
buzz notes get --name dco-check | jq .
buzz notes get --name dco-check --content-only
# get by naddr (exact coordinate; paste the naddr from a set/get above)
sprout notes get --naddr "$NADDR" | jq .
buzz notes get --naddr "$NADDR" | jq .
# ls (own by default; --author all across the team; --tag filters)
sprout notes ls | jq .
sprout notes ls --tag dco | jq .
sprout notes ls --author all --limit 10 | jq .
buzz notes ls | jq .
buzz notes ls --tag dco | jq .
buzz notes ls --author all --limit 10 | jq .
# rm (NIP-09 a-tag deletion; subsequent get must 404)
sprout notes rm --name dco-check
buzz notes rm --name dco-check
# → prints deleted <coordinate> / deletion <event-id>
sprout notes get --name dco-check # exits non-zero: not found
buzz notes get --name dco-check # exits non-zero: not found
# rm of a slug you never published → NotFound, no kind:5 emitted
sprout notes rm --name does-not-exist # exits non-zero
buzz notes rm --name does-not-exist # exits non-zero
```
---
@@ -477,37 +477,37 @@ Verify the CLI produces correct JSON on stderr and correct exit codes.
```bash
# Exit 1: Invalid UUID
sprout channels get --channel "not-a-uuid" 2>&1; echo "exit: $?"
buzz channels get --channel "not-a-uuid" 2>&1; echo "exit: $?"
# stderr: {"error":"user_error","message":"invalid UUID: not-a-uuid"}
# exit: 1
# Exit 1: Invalid hex64
sprout messages delete --event "not-hex" 2>&1; echo "exit: $?"
buzz messages delete --event "not-hex" 2>&1; echo "exit: $?"
# stderr: {"error":"user_error","message":"must be a 64-character hex string: not-hex"}
# exit: 1
# Exit 1: Invalid --type value (clap validates the enum — multi-line error)
sprout channels create --name x --type invalid --visibility open 2>&1; echo "exit: $?"
buzz channels create --name x --type invalid --visibility open 2>&1; echo "exit: $?"
# stderr: {"error":"user_error","message":"error: invalid value 'invalid' for '--type <CHANNEL_TYPE>'\n [possible values: stream, forum]\n..."}
# exit: 1
# Exit 1: Invalid --direction value
sprout messages vote --event "$(printf '0%.0s' {1..64})" \
buzz messages vote --event "$(printf '0%.0s' {1..64})" \
--direction sideways 2>&1; echo "exit: $?"
# exit: 1
# Exit 1: Empty body guard
sprout users set-profile 2>&1; echo "exit: $?"
buzz users set-profile 2>&1; echo "exit: $?"
# exit: 1 (at least one field required)
# Exit 3: No auth configured
env -u SPROUT_PRIVATE_KEY \
cargo run -p sprout-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"auth error: SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
env -u BUZZ_PRIVATE_KEY \
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
# exit: 3
# Not-found returns null, not an error (exit 0)
sprout channels get --channel "00000000-0000-0000-0000-000000000000"
buzz channels get --channel "00000000-0000-0000-0000-000000000000"
# stdout: null
# exit: 0
```
@@ -519,14 +519,14 @@ sprout channels get --channel "00000000-0000-0000-0000-000000000000"
Test authentication.
```bash
# Private key (SPROUT_PRIVATE_KEY)
SPROUT_PRIVATE_KEY="nsec1..." sprout channels list | jq .
# Private key (BUZZ_PRIVATE_KEY)
BUZZ_PRIVATE_KEY="nsec1..." buzz channels list | jq .
# Should succeed
# No auth → exit 3
env -u SPROUT_PRIVATE_KEY \
cargo run -p sprout-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"auth error: SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
env -u BUZZ_PRIVATE_KEY \
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
# exit: 3
```
@@ -536,8 +536,8 @@ env -u SPROUT_PRIVATE_KEY \
```bash
# Delete test channels
sprout channels delete --channel "$CHANNEL_ID" | jq .
sprout channels delete --channel "$FORUM_ID" | jq .
buzz channels delete --channel "$CHANNEL_ID" | jq .
buzz channels delete --channel "$FORUM_ID" | jq .
```
---
@@ -117,12 +117,12 @@ fn sign_nip98(
}
// ---------------------------------------------------------------------------
// SproutClient
// BuzzClient
// ---------------------------------------------------------------------------
pub struct SproutClient {
pub struct BuzzClient {
http: reqwest::Client,
relay_url: String, // base URL, no trailing slash, e.g. "https://relay.sprout.place"
relay_url: String, // base URL, no trailing slash, e.g. "https://relay.buzz.place"
keys: Keys,
/// Optional NIP-OA auth tag injected into every signed event.
auth_tag: Option<Tag>,
@@ -130,7 +130,7 @@ pub struct SproutClient {
auth_tag_json: Option<String>,
}
impl SproutClient {
impl BuzzClient {
pub fn new(
relay_url: String,
keys: Keys,
@@ -290,12 +290,12 @@ impl SproutClient {
/// Publish an ephemeral event via WebSocket with NIP-42 authentication.
///
/// The relay rejects ephemeral kinds (20000–29999) over HTTP. Delegates to
/// `sprout_ws_client::publish_event` which handles connect, NIP-42 auth,
/// `buzz_ws_client::publish_event` which handles connect, NIP-42 auth,
/// EVENT send, OK wait, and graceful close.
pub async fn publish_ephemeral_event(&self, event: nostr::Event) -> Result<String, CliError> {
let ws_url = to_ws_url(&self.relay_url);
let ok =
sprout_ws_client::publish_event(&ws_url, event, &self.keys, self.auth_tag.as_ref(), 10)
buzz_ws_client::publish_event(&ws_url, event, &self.keys, self.auth_tag.as_ref(), 10)
.await
.map_err(|e| CliError::Other(e.to_string()))?;
@@ -440,9 +440,9 @@ impl SproutClient {
.map(|s| s.to_string())
})
.unwrap_or(body);
if status == 403 && std::env::var("SPROUT_AUTH_TAG").is_ok() {
if status == 403 && std::env::var("BUZZ_AUTH_TAG").is_ok() {
let message = format!(
"{message} (SPROUT_AUTH_TAG is set — it may be stale or revoked; try unsetting it)"
"{message} (BUZZ_AUTH_TAG is set — it may be stale or revoked; try unsetting it)"
);
return Err(CliError::Relay {
status,
@@ -463,7 +463,7 @@ impl SproutClient {
// ---------------------------------------------------------------------------
/// Normalize a relay URL: ws:// → http://, wss:// → https://, strip trailing slash.
/// SPROUT_RELAY_URL may be ws/wss (copied from MCP config).
/// BUZZ_RELAY_URL may be ws/wss (copied from MCP config).
pub fn normalize_relay_url(url: &str) -> String {
url.replace("wss://", "https://")
.replace("ws://", "http://")
@@ -2,7 +2,7 @@ use uuid::Uuid;
use crate::client::{
extract_d_tag, extract_p_tags, extract_tag_value, normalize_write_response,
print_create_response, SproutClient,
print_create_response, BuzzClient,
};
use crate::error::CliError;
use crate::validate::{parse_uuid, read_or_stdin, validate_hex64, validate_uuid};
@@ -21,7 +21,7 @@ fn extract_channel_metadata(e: &serde_json::Value) -> serde_json::Value {
}
pub async fn cmd_list_channels(
client: &SproutClient,
client: &BuzzClient,
visibility: Option<&str>,
member: Option<bool>,
limit: Option<u32>,
@@ -117,7 +117,7 @@ pub async fn cmd_list_channels(
/// (private channels they're not a member of), so we just post-filter the
/// returned events by name and project them into a stable JSON shape.
pub async fn cmd_search_channels(
client: &SproutClient,
client: &BuzzClient,
query: &str,
exact: bool,
include_archived: bool,
@@ -195,7 +195,7 @@ impl ChannelSummary {
"d" => channel_id = val.map(str::to_string),
"name" => name = val.map(str::to_string),
"t" => channel_type = val.map(str::to_string),
// NIP-29 emits both `private` and `public` (Sprout adds the latter).
// NIP-29 emits both `private` and `public` (Buzz adds the latter).
// The presence of either tag is the source of truth; tag value is unused.
"private" => visibility = Some("private".to_string()),
"public" => visibility = Some("public".to_string()),
@@ -229,7 +229,7 @@ fn name_matches(name: &str, needle_lower: &str, exact: bool) -> bool {
}
}
pub async fn cmd_get_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_get_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
validate_uuid(channel_id)?;
let filter = serde_json::json!({
"kinds": [39000],
@@ -250,7 +250,7 @@ pub async fn cmd_get_channel(client: &SproutClient, channel_id: &str) -> Result<
}
pub async fn cmd_list_channel_members(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
) -> Result<(), CliError> {
validate_uuid(channel_id)?;
@@ -267,7 +267,7 @@ pub async fn cmd_list_channel_members(
Ok(())
}
pub async fn cmd_get_canvas(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_get_canvas(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
validate_uuid(channel_id)?;
let filter = serde_json::json!({
"kinds": [40100],
@@ -292,7 +292,7 @@ pub async fn cmd_get_canvas(client: &SproutClient, channel_id: &str) -> Result<(
// ---------------------------------------------------------------------------
pub async fn cmd_create_channel(
client: &SproutClient,
client: &BuzzClient,
name: &str,
channel_type: &str,
visibility: &str,
@@ -318,17 +318,17 @@ pub async fn cmd_create_channel(
let channel_uuid = Uuid::new_v4();
let vis = match visibility {
"open" => sprout_sdk::Visibility::Open,
"private" => sprout_sdk::Visibility::Private,
"open" => buzz_sdk::Visibility::Open,
"private" => buzz_sdk::Visibility::Private,
_ => unreachable!(),
};
let ct = match channel_type {
"stream" => sprout_sdk::ChannelKind::Stream,
"forum" => sprout_sdk::ChannelKind::Forum,
"stream" => buzz_sdk::ChannelKind::Stream,
"forum" => buzz_sdk::ChannelKind::Forum,
_ => unreachable!(),
};
let builder =
sprout_sdk::build_create_channel(channel_uuid, name, Some(vis), Some(ct), description)
buzz_sdk::build_create_channel(channel_uuid, name, Some(vis), Some(ct), description)
.map_err(|e| CliError::Other(format!("build_create_channel failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -338,7 +338,7 @@ pub async fn cmd_create_channel(
}
pub async fn cmd_update_channel(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
name: Option<&str>,
description: Option<&str>,
@@ -350,7 +350,7 @@ pub async fn cmd_update_channel(
}
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_update_channel(channel_uuid, name, description, None, None)
let builder = buzz_sdk::build_update_channel(channel_uuid, name, description, None, None)
.map_err(|e| CliError::Other(format!("build_update_channel failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -360,13 +360,13 @@ pub async fn cmd_update_channel(
}
pub async fn cmd_set_channel_topic(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
topic: &str,
) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_set_topic(channel_uuid, topic)
let builder = buzz_sdk::build_set_topic(channel_uuid, topic)
.map_err(|e| CliError::Other(format!("build_set_topic failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -376,13 +376,13 @@ pub async fn cmd_set_channel_topic(
}
pub async fn cmd_set_channel_purpose(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
purpose: &str,
) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_set_purpose(channel_uuid, purpose)
let builder = buzz_sdk::build_set_purpose(channel_uuid, purpose)
.map_err(|e| CliError::Other(format!("build_set_purpose failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -391,10 +391,10 @@ pub async fn cmd_set_channel_purpose(
Ok(())
}
pub async fn cmd_join_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_join_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_join(channel_uuid)
let builder = buzz_sdk::build_join(channel_uuid)
.map_err(|e| CliError::Other(format!("build_join failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -403,10 +403,10 @@ pub async fn cmd_join_channel(client: &SproutClient, channel_id: &str) -> Result
Ok(())
}
pub async fn cmd_leave_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_leave_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_leave(channel_uuid)
let builder = buzz_sdk::build_leave(channel_uuid)
.map_err(|e| CliError::Other(format!("build_leave failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -415,10 +415,10 @@ pub async fn cmd_leave_channel(client: &SproutClient, channel_id: &str) -> Resul
Ok(())
}
pub async fn cmd_archive_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_archive_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_archive(channel_uuid)
let builder = buzz_sdk::build_archive(channel_uuid)
.map_err(|e| CliError::Other(format!("build_archive failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -427,13 +427,10 @@ pub async fn cmd_archive_channel(client: &SproutClient, channel_id: &str) -> Res
Ok(())
}
pub async fn cmd_unarchive_channel(
client: &SproutClient,
channel_id: &str,
) -> Result<(), CliError> {
pub async fn cmd_unarchive_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_unarchive(channel_uuid)
let builder = buzz_sdk::build_unarchive(channel_uuid)
.map_err(|e| CliError::Other(format!("build_unarchive failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -442,10 +439,10 @@ pub async fn cmd_unarchive_channel(
Ok(())
}
pub async fn cmd_delete_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_delete_channel(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_delete_channel(channel_uuid)
let builder = buzz_sdk::build_delete_channel(channel_uuid)
.map_err(|e| CliError::Other(format!("build_delete_channel failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -455,7 +452,7 @@ pub async fn cmd_delete_channel(client: &SproutClient, channel_id: &str) -> Resu
}
pub async fn cmd_add_channel_member(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
pubkey: &str,
role: Option<&str>,
@@ -465,18 +462,18 @@ pub async fn cmd_add_channel_member(
let typed_role = match role {
None => None,
Some("owner") => Some(sprout_sdk::MemberRole::Owner),
Some("admin") => Some(sprout_sdk::MemberRole::Admin),
Some("member") => Some(sprout_sdk::MemberRole::Member),
Some("guest") => Some(sprout_sdk::MemberRole::Guest),
Some("bot") => Some(sprout_sdk::MemberRole::Bot),
Some("owner") => Some(buzz_sdk::MemberRole::Owner),
Some("admin") => Some(buzz_sdk::MemberRole::Admin),
Some("member") => Some(buzz_sdk::MemberRole::Member),
Some("guest") => Some(buzz_sdk::MemberRole::Guest),
Some("bot") => Some(buzz_sdk::MemberRole::Bot),
Some(other) => {
return Err(CliError::Usage(format!(
"--role must be owner/admin/member/guest/bot (got: {other})"
)))
}
};
let builder = sprout_sdk::build_add_member(channel_uuid, pubkey, typed_role)
let builder = buzz_sdk::build_add_member(channel_uuid, pubkey, typed_role)
.map_err(|e| CliError::Other(format!("build_add_member failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -486,14 +483,14 @@ pub async fn cmd_add_channel_member(
}
pub async fn cmd_remove_channel_member(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
pubkey: &str,
) -> Result<(), CliError> {
validate_hex64(pubkey)?;
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_remove_member(channel_uuid, pubkey)
let builder = buzz_sdk::build_remove_member(channel_uuid, pubkey)
.map_err(|e| CliError::Other(format!("build_remove_member failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -503,7 +500,7 @@ pub async fn cmd_remove_channel_member(
}
/// Set the channel addition policy — sign and submit a kind:10100 (agent profile) event.
pub async fn cmd_set_add_policy(client: &SproutClient, policy: &str) -> Result<(), CliError> {
pub async fn cmd_set_add_policy(client: &BuzzClient, policy: &str) -> Result<(), CliError> {
match policy {
"anyone" | "owner_only" | "nobody" => {}
_ => {
@@ -516,7 +513,7 @@ pub async fn cmd_set_add_policy(client: &SproutClient, policy: &str) -> Result<(
let content = serde_json::json!({ "channel_add_policy": policy }).to_string();
use nostr::{EventBuilder, Kind};
let builder = EventBuilder::new(
Kind::Custom(sprout_sdk::kind::KIND_AGENT_PROFILE as u16),
Kind::Custom(buzz_sdk::kind::KIND_AGENT_PROFILE as u16),
&content,
)
.tags([]);
@@ -528,14 +525,14 @@ pub async fn cmd_set_add_policy(client: &SproutClient, policy: &str) -> Result<(
}
pub async fn cmd_set_canvas(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
content: &str,
) -> Result<(), CliError> {
let content = read_or_stdin(content)?;
let channel_uuid = parse_uuid(channel_id)?;
let builder = sprout_sdk::build_set_canvas(channel_uuid, &content)
let builder = buzz_sdk::build_set_canvas(channel_uuid, &content)
.map_err(|e| CliError::Other(format!("build_set_canvas failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -550,7 +547,7 @@ pub async fn cmd_set_canvas(
pub async fn dispatch(
cmd: crate::ChannelsCmd,
client: &SproutClient,
client: &BuzzClient,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
use crate::ChannelsCmd;
@@ -614,7 +611,7 @@ pub async fn dispatch(
}
}
pub async fn dispatch_canvas(cmd: crate::CanvasCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch_canvas(cmd: crate::CanvasCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::CanvasCmd;
match cmd {
CanvasCmd::Get { channel } => cmd_get_canvas(client, &channel).await,
@@ -635,7 +632,7 @@ mod tests {
fn from_event_extracts_known_tags() {
let ev = event(json!([
["d", "11111111-1111-1111-1111-111111111111"],
["name", "sprout-chat-composer"],
["name", "buzz-chat-composer"],
["t", "stream"],
["public"],
["about", "About text"],
@@ -644,7 +641,7 @@ mod tests {
]));
let s = ChannelSummary::from_event(&ev).expect("parse");
assert_eq!(s.channel_id, "11111111-1111-1111-1111-111111111111");
assert_eq!(s.name, "sprout-chat-composer");
assert_eq!(s.name, "buzz-chat-composer");
assert_eq!(s.channel_type.as_deref(), Some("stream"));
assert_eq!(s.visibility.as_deref(), Some("public"));
assert!(!s.archived);
@@ -704,14 +701,14 @@ mod tests {
#[test]
fn name_matches_substring_case_insensitive() {
assert!(name_matches("Sprout-Chat-Composer", "composer", false));
assert!(name_matches("Sprout-Chat-Composer", "sprout", false));
assert!(name_matches("Buzz-Chat-Composer", "composer", false));
assert!(name_matches("Buzz-Chat-Composer", "buzz", false));
assert!(!name_matches("design", "composer", false));
}
#[test]
fn name_matches_exact_case_insensitive() {
assert!(name_matches("Sprout", "sprout", true));
assert!(!name_matches("Sprout-Chat", "sprout", true));
assert!(name_matches("Buzz", "buzz", true));
assert!(!name_matches("Buzz-Chat", "buzz", true));
}
}
@@ -1,11 +1,11 @@
use uuid::Uuid;
use crate::client::{extract_d_tag, normalize_write_response, SproutClient};
use crate::client::{extract_d_tag, normalize_write_response, BuzzClient};
use crate::error::CliError;
use crate::validate::{parse_uuid, sdk_err, validate_hex64};
/// List DM conversations by querying kind:41001 (relay-confirmed DMs) filtered by our pubkey.
pub async fn cmd_list_dms(client: &SproutClient, limit: Option<u32>) -> Result<(), CliError> {
pub async fn cmd_list_dms(client: &BuzzClient, limit: Option<u32>) -> Result<(), CliError> {
let my_pk = client.keys().public_key().to_hex();
let limit = limit.unwrap_or(50).min(200);
let filter = serde_json::json!({
@@ -48,7 +48,7 @@ pub async fn cmd_list_dms(client: &SproutClient, limit: Option<u32>) -> Result<(
}
/// Open a DM with one or more users — sign and submit a kind:41010 event with a d-tag.
pub async fn cmd_open_dm(client: &SproutClient, pubkeys: &[String]) -> Result<(), CliError> {
pub async fn cmd_open_dm(client: &BuzzClient, pubkeys: &[String]) -> Result<(), CliError> {
if pubkeys.is_empty() || pubkeys.len() > 8 {
return Err(CliError::Usage("--pubkey: must provide 1-8 pubkeys".into()));
}
@@ -93,14 +93,14 @@ pub async fn cmd_open_dm(client: &SproutClient, pubkeys: &[String]) -> Result<()
}
/// Hide a DM channel — sign and submit a kind:41012 event with h-tag.
pub async fn cmd_hide_dm(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_hide_dm(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
use nostr::{EventBuilder, Kind, Tag};
let tags = vec![Tag::parse(["h", &channel_uuid.to_string()])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
let builder =
EventBuilder::new(Kind::Custom(sprout_sdk::kind::KIND_DM_HIDE as u16), "").tags(tags);
EventBuilder::new(Kind::Custom(buzz_sdk::kind::KIND_DM_HIDE as u16), "").tags(tags);
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
@@ -110,14 +110,14 @@ pub async fn cmd_hide_dm(client: &SproutClient, channel_id: &str) -> Result<(),
/// Add a member to a DM group — sign and submit a kind:41011 event.
pub async fn cmd_add_dm_member(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
pubkey: &str,
) -> Result<(), CliError> {
let channel_uuid = parse_uuid(channel_id)?;
validate_hex64(pubkey)?;
let builder = sprout_sdk::build_dm_add_member(channel_uuid, pubkey).map_err(sdk_err)?;
let builder = buzz_sdk::build_dm_add_member(channel_uuid, pubkey).map_err(sdk_err)?;
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
@@ -129,7 +129,7 @@ pub async fn cmd_add_dm_member(
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::DmsCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::DmsCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::DmsCmd;
match cmd {
DmsCmd::List { limit } => cmd_list_dms(client, limit).await,
@@ -1,12 +1,12 @@
use std::io::Read;
use crate::client::{normalize_write_response, SproutClient};
use crate::client::{normalize_write_response, BuzzClient};
use crate::error::CliError;
use sprout_sdk::CustomEmoji;
use buzz_sdk::CustomEmoji;
/// d-tag for a member's own custom emoji set (kind:30030). Mirrors the SDK
/// constant; the workspace palette is the union of every member's own set.
const CUSTOM_EMOJI_SET_D_TAG: &str = sprout_sdk::CUSTOM_EMOJI_SET_D_TAG;
const CUSTOM_EMOJI_SET_D_TAG: &str = buzz_sdk::CUSTOM_EMOJI_SET_D_TAG;
/// Custom emoji entry in CLI output.
#[derive(Debug, serde::Serialize)]
@@ -59,10 +59,10 @@ fn union_custom_emoji(events: &[serde_json::Value]) -> Vec<EmojiEntry> {
}
/// List the workspace custom emoji palette: the union of every member's
/// own kind:30030 set (d=`sprout:custom-emoji`).
async fn cmd_list(client: &SproutClient) -> Result<(), CliError> {
/// own kind:30030 set (d=`buzz:custom-emoji`).
async fn cmd_list(client: &BuzzClient) -> Result<(), CliError> {
let filter = serde_json::json!({
"kinds": [sprout_sdk::kind::KIND_EMOJI_SET],
"kinds": [buzz_sdk::kind::KIND_EMOJI_SET],
"#d": [CUSTOM_EMOJI_SET_D_TAG],
});
let raw = client.query(&filter).await?;
@@ -76,10 +76,10 @@ async fn cmd_list(client: &SproutClient) -> Result<(), CliError> {
/// Fetch the caller's own current custom emoji set (latest kind:30030 under
/// the d-tag, authored by the caller). Empty when none published yet.
async fn fetch_own_emoji(client: &SproutClient) -> Result<Vec<CustomEmoji>, CliError> {
async fn fetch_own_emoji(client: &BuzzClient) -> Result<Vec<CustomEmoji>, CliError> {
let me = client.keys().public_key().to_hex();
let filter = serde_json::json!({
"kinds": [sprout_sdk::kind::KIND_EMOJI_SET],
"kinds": [buzz_sdk::kind::KIND_EMOJI_SET],
"#d": [CUSTOM_EMOJI_SET_D_TAG],
"authors": [me],
"limit": 1,
@@ -101,8 +101,8 @@ async fn fetch_own_emoji(client: &SproutClient) -> Result<Vec<CustomEmoji>, CliE
}
/// Publish the caller's own (replaced) kind:30030 set, signed as the caller.
async fn publish_own_set(client: &SproutClient, emojis: &[CustomEmoji]) -> Result<(), CliError> {
let builder = sprout_sdk::build_custom_emoji_set(emojis)
async fn publish_own_set(client: &BuzzClient, emojis: &[CustomEmoji]) -> Result<(), CliError> {
let builder = buzz_sdk::build_custom_emoji_set(emojis)
.map_err(|e| CliError::Other(format!("build_custom_emoji_set failed: {e}")))?;
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
@@ -111,8 +111,8 @@ async fn publish_own_set(client: &SproutClient, emojis: &[CustomEmoji]) -> Resul
}
/// Add/update a shortcode in the caller's own set (read-modify-write).
async fn cmd_set(client: &SproutClient, shortcode: &str, url: &str) -> Result<(), CliError> {
let normalized = sprout_sdk::normalize_custom_emoji_shortcode(shortcode)
async fn cmd_set(client: &BuzzClient, shortcode: &str, url: &str) -> Result<(), CliError> {
let normalized = buzz_sdk::normalize_custom_emoji_shortcode(shortcode)
.map_err(|e| CliError::Other(format!("invalid shortcode: {e}")))?;
let mut emojis = fetch_own_emoji(client).await?;
emojis.retain(|e| e.shortcode != normalized);
@@ -124,8 +124,8 @@ async fn cmd_set(client: &SproutClient, shortcode: &str, url: &str) -> Result<()
}
/// Remove a shortcode from the caller's own set (read-modify-write).
async fn cmd_rm(client: &SproutClient, shortcode: &str) -> Result<(), CliError> {
let normalized = sprout_sdk::normalize_custom_emoji_shortcode(shortcode)
async fn cmd_rm(client: &BuzzClient, shortcode: &str) -> Result<(), CliError> {
let normalized = buzz_sdk::normalize_custom_emoji_shortcode(shortcode)
.map_err(|e| CliError::Other(format!("invalid shortcode: {e}")))?;
let mut emojis = fetch_own_emoji(client).await?;
let before = emojis.len();
@@ -181,7 +181,7 @@ fn write_output(output: &str, file: Option<&str>) -> Result<(), CliError> {
/// Export custom emojis to stdout or a file.
async fn cmd_export(
client: &SproutClient,
client: &BuzzClient,
file: Option<&str>,
scope: &crate::EmojiScope,
) -> Result<(), CliError> {
@@ -202,7 +202,7 @@ async fn cmd_export(
}
crate::EmojiScope::Workspace => {
let filter = serde_json::json!({
"kinds": [sprout_sdk::kind::KIND_EMOJI_SET],
"kinds": [buzz_sdk::kind::KIND_EMOJI_SET],
"#d": [CUSTOM_EMOJI_SET_D_TAG],
});
let raw = client.query(&filter).await?;
@@ -218,7 +218,7 @@ async fn cmd_export(
/// Import custom emojis from stdin or a file into the caller's own set.
async fn cmd_import(
client: &SproutClient,
client: &BuzzClient,
file: Option<&str>,
replace: bool,
dry_run: bool,
@@ -247,7 +247,7 @@ async fn cmd_import(
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| CliError::Usage(format!("emojis[{i}]: missing \"url\" field")))?;
let normalized = sprout_sdk::normalize_custom_emoji_shortcode(shortcode)
let normalized = buzz_sdk::normalize_custom_emoji_shortcode(shortcode)
.map_err(|e| CliError::Usage(format!("emojis[{i}]: invalid shortcode: {e}")))?;
import_entries.push(CustomEmoji {
shortcode: normalized,
@@ -298,7 +298,7 @@ async fn cmd_import(
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::EmojiCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::EmojiCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::EmojiCmd;
match cmd {
EmojiCmd::List => cmd_list(client).await,
@@ -322,14 +322,14 @@ mod tests {
let events = vec![
serde_json::json!({
"tags": [
["d", "sprout:custom-emoji"],
["d", "buzz:custom-emoji"],
["emoji", "zort", "https://example.com/zort.png"],
["emoji", "narf", "https://example.com/narf.png"]
]
}),
serde_json::json!({
"tags": [
["d", "sprout:custom-emoji"],
["d", "buzz:custom-emoji"],
// exact duplicate (same shortcode+url) — collapses
["emoji", "narf", "https://example.com/narf.png"],
// same shortcode, different url — both kept (distinct pair)
@@ -1,13 +1,13 @@
use std::cmp::Reverse;
use crate::client::{normalize_events, SproutClient};
use crate::client::{normalize_events, BuzzClient};
use crate::error::CliError;
const VALID_FEED_TYPES: &[&str] = &["mentions", "needs_action", "activity", "agent_activity"];
/// Get activity feed — query events mentioning our pubkey (via p-tag).
pub async fn cmd_get_feed(
client: &SproutClient,
client: &BuzzClient,
since: Option<i64>,
limit: Option<u32>,
types: Option<&str>,
@@ -70,7 +70,7 @@ pub async fn cmd_get_feed(
pub async fn dispatch(
cmd: crate::FeedCmd,
client: &SproutClient,
client: &BuzzClient,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
use crate::FeedCmd;
@@ -1,15 +1,15 @@
//! `sprout mem` — agent-side engram management (NIP-AE).
//! `buzz mem` — agent-side engram management (NIP-AE).
//!
//! Subcommands:
//! - `sprout mem ls` — list non-tombstoned memories
//! - `sprout mem get <slug>` — print the value to stdout
//! - `sprout mem hash <slug>` — print sha256(value) hex
//! - `sprout mem set <slug> <value|-> ` — write a value (use `-` for stdin)
//! - `sprout mem patch <slug>` — apply a unified diff to the current value
//! - `sprout mem rm <slug>` — publish a tombstone
//! - `buzz mem ls` — list non-tombstoned memories
//! - `buzz mem get <slug>` — print the value to stdout
//! - `buzz mem hash <slug>` — print sha256(value) hex
//! - `buzz mem set <slug> <value|-> ` — write a value (use `-` for stdin)
//! - `buzz mem patch <slug>` — apply a unified diff to the current value
//! - `buzz mem rm <slug>` — publish a tombstone
//!
//! By default, the caller's `SPROUT_PRIVATE_KEY` is the agent's nsec. The
//! agent's owner pubkey is resolved from `SPROUT_AUTH_TAG` (NIP-OA attestation)
//! By default, the caller's `BUZZ_PRIVATE_KEY` is the agent's nsec. The
//! agent's owner pubkey is resolved from `BUZZ_AUTH_TAG` (NIP-OA attestation)
//! or the `--owner` flag. Read commands also support owner-side recovery via
//! `--agent <pubkey>`: the CLI identity is treated as the owner and decrypts
//! the agent's engrams through the same agent↔owner NIP-44 conversation key.
@@ -19,25 +19,25 @@ use std::time::SystemTime;
use sha2::{Digest, Sha256};
use nostr::PublicKey;
use sprout_core::engram::{
use buzz_core::engram::{
self, conversation_key, d_tag, normalize_slug, select_head, validate_and_decrypt, Body, Listing,
};
use sprout_core::kind::KIND_AGENT_ENGRAM;
use buzz_core::kind::KIND_AGENT_ENGRAM;
use nostr::PublicKey;
use crate::client::SproutClient;
use crate::client::BuzzClient;
use crate::error::CliError;
/// Resolve the agent's owner pubkey: explicit `--owner` flag wins, otherwise
/// fall back to the NIP-OA `auth_tag` (which carries owner pubkey in slot 1).
fn resolve_owner(client: &SproutClient, owner_flag: Option<&str>) -> Result<PublicKey, CliError> {
fn resolve_owner(client: &BuzzClient, owner_flag: Option<&str>) -> Result<PublicKey, CliError> {
if let Some(s) = owner_flag {
return PublicKey::from_hex(s)
.map_err(|e| CliError::Usage(format!("--owner must be a 64-hex pubkey: {e}")));
}
let tag = client.auth_tag_owner_hex().ok_or_else(|| {
CliError::Usage(
"owner pubkey required (set SPROUT_AUTH_TAG with a NIP-OA attestation or pass --owner)"
"owner pubkey required (set BUZZ_AUTH_TAG with a NIP-OA attestation or pass --owner)"
.into(),
)
})?;
@@ -48,11 +48,11 @@ fn resolve_owner(client: &SproutClient, owner_flag: Option<&str>) -> Result<Publ
/// Resolve the read perspective for `mem ls/get/hash`.
///
/// Normal agent-side reads use the CLI identity as the agent and resolve the
/// owner from `--owner` / SPROUT_AUTH_TAG. Owner-side recovery passes
/// owner from `--owner` / BUZZ_AUTH_TAG. Owner-side recovery passes
/// `--agent <pubkey>`; the CLI identity is then the owner and the supplied
/// pubkey is the agent author to query/decrypt.
fn resolve_reader(
client: &SproutClient,
client: &BuzzClient,
owner_flag: Option<&str>,
agent_flag: Option<&str>,
) -> Result<(PublicKey, PublicKey, PublicKey), CliError> {
@@ -90,7 +90,7 @@ fn now_secs() -> u64 {
/// `message` field starts with `"duplicate:"` when the write was rejected
/// as already-superseded by a later head (NIP-33 LWW). In that case we
/// surface a `Conflict` so callers don't lie about success.
async fn submit_engram(client: &SproutClient, event: nostr::Event) -> Result<(), CliError> {
async fn submit_engram(client: &BuzzClient, event: nostr::Event) -> Result<(), CliError> {
let raw = client.submit_event(event).await?;
let parsed: serde_json::Value = serde_json::from_str(&raw)
.map_err(|e| CliError::Other(format!("relay response is not JSON: {e} ({raw})")))?;
@@ -134,7 +134,7 @@ fn parse_events(json: &str) -> Result<Vec<nostr::Event>, CliError> {
/// Fetch the head event for `slug`, returning `(Option<Event>, Option<Body>)`.
async fn fetch_head(
client: &SproutClient,
client: &BuzzClient,
agent: &PublicKey,
owner: &PublicKey,
slug: &str,
@@ -185,9 +185,9 @@ async fn fetch_head(
Ok((Some(head), body))
}
/// `sprout mem ls` — list non-tombstoned memory entries.
/// `buzz mem ls` — list non-tombstoned memory entries.
pub async fn cmd_ls(
client: &SproutClient,
client: &BuzzClient,
owner_flag: Option<&str>,
agent_flag: Option<&str>,
json: bool,
@@ -271,11 +271,11 @@ pub async fn cmd_ls(
Ok(())
}
/// `sprout mem get <slug>` — print value (memory) or profile (core) to stdout.
/// `buzz mem get <slug>` — print value (memory) or profile (core) to stdout.
///
/// Exit codes: 0 on found, 1 on absent or tombstoned.
pub async fn cmd_get(
client: &SproutClient,
client: &BuzzClient,
raw_slug: &str,
owner_flag: Option<&str>,
agent_flag: Option<&str>,
@@ -291,7 +291,7 @@ pub async fn cmd_get(
Err(CliError::NotFound(format!("tombstoned: {slug}")))
}
Some(Body::Memory { value: Some(v), .. }) => {
// Raw stdout, no trailing newline — round-trips with `sprout mem set foo -`.
// Raw stdout, no trailing newline — round-trips with `buzz mem set foo -`.
std::io::stdout()
.write_all(v.as_bytes())
.map_err(|e| CliError::Other(e.to_string()))
@@ -302,7 +302,7 @@ pub async fn cmd_get(
}
}
/// `sprout mem set <slug> <value|->` — write a value or core profile.
/// `buzz mem set <slug> <value|->` — write a value or core profile.
///
/// Pass `-` to read the value from stdin.
///
@@ -312,7 +312,7 @@ pub async fn cmd_get(
/// otherwise commit an empty value — silently destroying the slug.
/// A literal `""` positional argument is still accepted (explicit intent).
pub async fn cmd_set(
client: &SproutClient,
client: &BuzzClient,
raw_slug: &str,
raw_value: &str,
owner_flag: Option<&str>,
@@ -339,7 +339,7 @@ pub async fn cmd_set(
if buf.is_empty() && !allow_empty {
return Err(CliError::Usage(
"refusing to write empty value from stdin (an upstream pipeline step likely \
failed). Pass --allow-empty to confirm, or use `sprout mem rm <slug>` to \
failed). Pass --allow-empty to confirm, or use `buzz mem rm <slug>` to \
tombstone."
.into(),
));
@@ -482,7 +482,7 @@ fn verify_hunks_at_declared_position(
/// Used by `mem hash` and `mem patch` — they both need "the value or fail".
/// Returns `(head_event, value)` so the caller can preserve monotonic ordering.
async fn fetch_value(
client: &SproutClient,
client: &BuzzClient,
agent: &PublicKey,
owner: &PublicKey,
slug: &str,
@@ -499,14 +499,14 @@ async fn fetch_value(
}
}
/// `sprout mem hash <slug>` — print sha256(value) in hex to stdout.
/// `buzz mem hash <slug>` — print sha256(value) in hex to stdout.
///
/// The output is a 64-character hex digest followed by a newline (line-
/// oriented for shell use). Use this to capture a base-hash before editing,
/// then pass it to `sprout mem patch --base-hash <hex>` to make the edit
/// then pass it to `buzz mem patch --base-hash <hex>` to make the edit
/// safe against concurrent writes.
pub async fn cmd_hash(
client: &SproutClient,
client: &BuzzClient,
raw_slug: &str,
owner_flag: Option<&str>,
agent_flag: Option<&str>,
@@ -519,7 +519,7 @@ pub async fn cmd_hash(
Ok(())
}
/// `sprout mem patch <slug>` — apply a unified diff to the current value.
/// `buzz mem patch <slug>` — apply a unified diff to the current value.
///
/// Reads a unified diff from stdin (or `--patch-file <path>`), fetches the
/// current head, applies the diff with **strict context matching** (no
@@ -536,7 +536,7 @@ pub async fn cmd_hash(
/// can chain edits.
#[allow(clippy::too_many_arguments)]
pub async fn cmd_patch(
client: &SproutClient,
client: &BuzzClient,
raw_slug: &str,
patch_path: Option<&str>,
base_hash: Option<&str>,
@@ -558,7 +558,7 @@ pub async fn cmd_patch(
}
(None, false) => {
return Err(CliError::Usage(
"missing --base-hash <hex> (run `sprout mem hash <slug>` to get it). \
"missing --base-hash <hex> (run `buzz mem hash <slug>` to get it). \
Pass --no-base-hash to skip this check at your own risk."
.into(),
));
@@ -659,7 +659,7 @@ pub async fn cmd_patch(
if new_value.is_empty() && !allow_empty {
return Err(CliError::Usage(
"refusing to write empty value (patch result is empty). \
Pass --allow-empty to confirm, or use `sprout mem rm <slug>` to tombstone."
Pass --allow-empty to confirm, or use `buzz mem rm <slug>` to tombstone."
.into(),
));
}
@@ -697,14 +697,14 @@ pub async fn cmd_patch(
Ok(())
}
/// `sprout mem rm <slug>` — publish a tombstone (`value: null`).
/// `buzz mem rm <slug>` — publish a tombstone (`value: null`).
///
/// `rm core` writes a tombstone-shaped body, but a core tombstone has no
/// well-defined semantics in NIP-AE (the spec only defines tombstones for
/// memory entries). We refuse it and tell the operator to overwrite `core`
/// with an empty profile instead.
pub async fn cmd_rm(
client: &SproutClient,
client: &BuzzClient,
raw_slug: &str,
owner_flag: Option<&str>,
) -> Result<(), CliError> {
@@ -712,7 +712,7 @@ pub async fn cmd_rm(
normalize_slug(raw_slug).map_err(|e| CliError::Usage(format!("invalid slug: {e}")))?;
if slug == engram::CORE_SLUG {
return Err(CliError::Usage(
"core cannot be tombstoned; overwrite it with `sprout mem set core ''` instead".into(),
"core cannot be tombstoned; overwrite it with `buzz mem set core ''` instead".into(),
));
}
let owner = resolve_owner(client, owner_flag)?;
@@ -738,7 +738,7 @@ pub async fn cmd_rm(
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::MemCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::MemCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::MemCmd;
match cmd {
MemCmd::Ls { owner, agent, json } => {
@@ -789,8 +789,8 @@ mod tests {
// verify base-hash from the shell. Hard-coded vectors from the NIST and
// common quick-check inputs.
fn test_client(keys: nostr::Keys) -> SproutClient {
SproutClient::new("http://127.0.0.1:9".into(), keys, None, None).unwrap()
fn test_client(keys: nostr::Keys) -> BuzzClient {
BuzzClient::new("http://127.0.0.1:9".into(), keys, None, None).unwrap()
}
#[test]
@@ -1,14 +1,14 @@
use buzz_sdk::{DiffMeta, ThreadRef, VoteDirection};
use nostr::PublicKey;
use sprout_sdk::{DiffMeta, ThreadRef, VoteDirection};
use uuid::Uuid;
use crate::client::{normalize_events, normalize_write_response, SproutClient};
use crate::client::{normalize_events, normalize_write_response, BuzzClient};
use crate::error::CliError;
use crate::validate::{
infer_language, parse_event_id, parse_uuid, read_or_stdin, truncate_diff,
validate_content_size, validate_hex64, validate_uuid, MAX_DIFF_BYTES,
};
use sprout_sdk::mentions::{
use buzz_sdk::mentions::{
extract_at_mentions_with_known, extract_nostr_uris, merge_mentions, strip_code_regions,
MENTION_CAP,
};
@@ -60,7 +60,7 @@ fn find_root_from_tags(tags: &serde_json::Value) -> Option<String> {
///
/// Ensures CLI-sent replies thread correctly using the same NIP-10 logic.
async fn resolve_thread_ref(
client: &SproutClient,
client: &BuzzClient,
parent_event_id: &str,
) -> Result<ThreadRef, CliError> {
let parent_eid = parse_event_id(parent_event_id)?;
@@ -90,7 +90,7 @@ async fn resolve_thread_ref(
/// Resolve the channel UUID for an event by querying for it via POST /query.
/// Extracts the `h` tag value from the returned event's tags.
async fn resolve_channel_id(client: &SproutClient, event_id: &str) -> Result<Uuid, CliError> {
async fn resolve_channel_id(client: &BuzzClient, event_id: &str) -> Result<Uuid, CliError> {
let filter = serde_json::json!({
"ids": [event_id]
});
@@ -130,7 +130,7 @@ async fn resolve_channel_id(client: &SproutClient, event_id: &str) -> Result<Uui
/// for multi-word matching. On any I/O or parse failure, returns an empty
/// vec — auto-tagging is best-effort and must never block a send.
async fn resolve_content_mentions(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
content: &str,
) -> Vec<String> {
@@ -205,7 +205,7 @@ async fn resolve_content_mentions(
/// Fetch raw events for `filter` via the relay's `/query` endpoint.
/// Returns `None` on any I/O or parse failure.
async fn fetch_events(
client: &SproutClient,
client: &BuzzClient,
filter: &serde_json::Value,
) -> Option<Vec<serde_json::Value>> {
let raw = client.query(filter).await.ok()?;
@@ -215,7 +215,7 @@ async fn fetch_events(
/// Extract member pubkeys (the `p` tag values) from a single 39002 event.
async fn fetch_member_pubkeys(
client: &SproutClient,
client: &BuzzClient,
filter: &serde_json::Value,
) -> Option<Vec<String>> {
let events = fetch_events(client, filter).await?;
@@ -269,7 +269,7 @@ fn format_events(normalized: &str, format: &crate::OutputFormat) -> String {
}
pub async fn cmd_get_messages(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
limit: Option<u32>,
before: Option<i64>,
@@ -310,7 +310,7 @@ pub async fn cmd_get_messages(
}
pub async fn cmd_get_thread(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
event_id: &str,
limit: Option<u32>,
@@ -346,7 +346,7 @@ pub async fn cmd_get_thread(
}
pub async fn cmd_search(
client: &SproutClient,
client: &BuzzClient,
query: &str,
limit: Option<u32>,
format: &crate::OutputFormat,
@@ -378,7 +378,7 @@ pub struct SendMessageParams {
}
pub async fn cmd_send_message(
client: &SproutClient,
client: &BuzzClient,
mut p: SendMessageParams,
) -> Result<(), CliError> {
// Allow '-' to read content from stdin. This keeps callers from having to
@@ -436,14 +436,14 @@ pub async fn cmd_send_message(
let builder = match p.kind {
Some(45001) => {
sprout_sdk::build_forum_post(channel_uuid, &final_content, &mention_refs, &media_tags)
buzz_sdk::build_forum_post(channel_uuid, &final_content, &mention_refs, &media_tags)
.map_err(|e| CliError::Other(format!("build_forum_post failed: {e}")))?
}
Some(45003) => {
let tr = thread_ref.as_ref().ok_or_else(|| {
CliError::Usage("--reply-to is required for forum comments (kind 45003)".into())
})?;
sprout_sdk::build_forum_comment(
buzz_sdk::build_forum_comment(
channel_uuid,
&final_content,
tr,
@@ -452,7 +452,7 @@ pub async fn cmd_send_message(
)
.map_err(|e| CliError::Other(format!("build_forum_comment failed: {e}")))?
}
None | Some(9) => sprout_sdk::build_message(
None | Some(9) => buzz_sdk::build_message(
channel_uuid,
&final_content,
thread_ref.as_ref(),
@@ -490,10 +490,7 @@ pub struct SendDiffParams {
pub reply_to: Option<String>,
}
pub async fn cmd_send_diff_message(
client: &SproutClient,
p: SendDiffParams,
) -> Result<(), CliError> {
pub async fn cmd_send_diff_message(client: &BuzzClient, p: SendDiffParams) -> Result<(), CliError> {
if let Some(r) = &p.reply_to {
validate_hex64(r)?;
}
@@ -556,7 +553,7 @@ pub async fn cmd_send_diff_message(
};
let builder =
sprout_sdk::build_diff_message(channel_uuid, &diff, &diff_meta, thread_ref.as_ref())
buzz_sdk::build_diff_message(channel_uuid, &diff, &diff_meta, thread_ref.as_ref())
.map_err(|e| CliError::Other(format!("build_diff_message failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -566,14 +563,14 @@ pub async fn cmd_send_diff_message(
Ok(())
}
pub async fn cmd_delete_message(client: &SproutClient, event_id: &str) -> Result<(), CliError> {
pub async fn cmd_delete_message(client: &BuzzClient, event_id: &str) -> Result<(), CliError> {
validate_hex64(event_id)?;
// Resolve channel_id from the event's h-tag
let channel_uuid = resolve_channel_id(client, event_id).await?;
let target_eid = parse_event_id(event_id)?;
let builder = sprout_sdk::build_delete_message(channel_uuid, target_eid)
let builder = buzz_sdk::build_delete_message(channel_uuid, target_eid)
.map_err(|e| CliError::Other(format!("build_delete_message failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -585,7 +582,7 @@ pub async fn cmd_delete_message(client: &SproutClient, event_id: &str) -> Result
/// Edit a message you previously sent.
pub async fn cmd_edit_message(
client: &SproutClient,
client: &BuzzClient,
event_id: &str,
content: &str,
) -> Result<(), CliError> {
@@ -596,7 +593,7 @@ pub async fn cmd_edit_message(
let channel_uuid = resolve_channel_id(client, event_id).await?;
let target_eid = parse_event_id(event_id)?;
let builder = sprout_sdk::build_edit(channel_uuid, target_eid, content)
let builder = buzz_sdk::build_edit(channel_uuid, target_eid, content)
.map_err(|e| CliError::Other(format!("build_edit failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -608,7 +605,7 @@ pub async fn cmd_edit_message(
/// Vote on a forum post or comment.
pub async fn cmd_vote_on_post(
client: &SproutClient,
client: &BuzzClient,
event_id: &str,
direction: &str,
) -> Result<(), CliError> {
@@ -627,7 +624,7 @@ pub async fn cmd_vote_on_post(
let channel_uuid = resolve_channel_id(client, event_id).await?;
let target_eid = parse_event_id(event_id)?;
let builder = sprout_sdk::build_vote(channel_uuid, target_eid, vote_dir)
let builder = buzz_sdk::build_vote(channel_uuid, target_eid, vote_dir)
.map_err(|e| CliError::Other(format!("build_vote failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -643,7 +640,7 @@ pub async fn cmd_vote_on_post(
pub async fn dispatch(
cmd: crate::MessagesCmd,
client: &SproutClient,
client: &BuzzClient,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
use crate::MessagesCmd;
@@ -738,10 +735,10 @@ pub async fn dispatch(
#[cfg(test)]
mod tests {
use super::{find_root_from_tags, parse_member_pubkeys};
use serde_json::json;
use sprout_sdk::mentions::{
use buzz_sdk::mentions::{
extract_at_mentions_with_known, extract_at_names, match_names_to_profiles, MentionProfile,
};
use serde_json::json;
const ID_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const ID_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@@ -825,7 +822,7 @@ mod tests {
// events the relay returns, the CLI's parse + match wiring produces
// the right pubkeys. The async I/O wrapper around them is one
// straight line; the pure stages it composes are exercised here and
// in sprout-sdk.
// in buzz-sdk.
/// End-to-end (sans I/O): body text → extracted names → matched
/// member pubkeys, using realistic 39002 + kind:0 event JSON.
@@ -1,4 +1,4 @@
//! `sprout notes` — NIP-23 long-form editable notes (kind:30023).
//! `buzz notes` — NIP-23 long-form editable notes (kind:30023).
//!
//! Skill-sharing knowledge base for the team. Notes are parameterized-replaceable
//! events keyed by `(kind=30023, pubkey, d-tag)`; the `d` tag is the human slug.
@@ -30,7 +30,7 @@ use std::time::SystemTime;
use nostr::{Event, EventBuilder, Kind, PublicKey, Tag, Timestamp, ToBech32};
use crate::client::SproutClient;
use crate::client::BuzzClient;
use crate::error::CliError;
use crate::validate::validate_hex64;
@@ -177,7 +177,7 @@ fn parse_events(json: &str) -> Result<Vec<Event>, CliError> {
/// `NoteSnapshot::from_event` on the result for carry-forward, and `rm` only
/// needs its presence to decide first-publish vs. update / deletable-or-not.
/// (Quinn's option (b) — single tag-parser, isolated.)
pub async fn fetch_own_note(client: &SproutClient, slug: &str) -> Result<Option<Event>, CliError> {
pub async fn fetch_own_note(client: &BuzzClient, slug: &str) -> Result<Option<Event>, CliError> {
let me = client.keys().public_key();
let filter = serde_json::json!({
"kinds": [KIND_LONG_FORM],
@@ -196,7 +196,7 @@ pub async fn fetch_own_note(client: &SproutClient, slug: &str) -> Result<Option<
/// Cross-author `#d` lookup for `get --name`. The relay pushes the `#d`
/// filter into SQL for NIP-33 kinds (`req.rs`), so this is a single
/// indexed query, not a fan-out.
pub async fn fetch_by_slug(client: &SproutClient, slug: &str) -> Result<Vec<Event>, CliError> {
pub async fn fetch_by_slug(client: &BuzzClient, slug: &str) -> Result<Vec<Event>, CliError> {
let filter = serde_json::json!({
"kinds": [KIND_LONG_FORM],
"#d": [slug],
@@ -217,10 +217,7 @@ pub async fn fetch_by_slug(client: &SproutClient, slug: &str) -> Result<Vec<Even
/// - 64-hex pubkey → parsed directly.
/// - anything else → treated as a petname / display name, searched against
/// kind:0 profiles. Exact-one match required; ambiguity is a hard error.
pub async fn resolve_author(
client: &SproutClient,
author_flag: &str,
) -> Result<PublicKey, CliError> {
pub async fn resolve_author(client: &BuzzClient, author_flag: &str) -> Result<PublicKey, CliError> {
if author_flag == "me" {
return Ok(client.keys().public_key());
}
@@ -378,7 +375,7 @@ fn snapshots_from_events(events: Vec<Event>) -> Result<Vec<NoteSnapshot>, CliErr
}
async fn fetch_by_coord(
client: &SproutClient,
client: &BuzzClient,
coord: &nostr::nips::nip01::Coordinate,
) -> Result<Option<Event>, CliError> {
let filter = serde_json::json!({
@@ -524,7 +521,7 @@ pub const SET_STDIN_MAX_BYTES: usize = 1024 * 1024;
// ---------------------------------------------------------------------------
pub async fn cmd_set(
client: &SproutClient,
client: &BuzzClient,
slug: &str,
title: Option<&str>,
summary: Option<&str>,
@@ -649,7 +646,7 @@ fn validate_get_args(naddr: bool, name: bool, author: bool, latest: bool) -> Res
}
pub async fn cmd_get(
client: &SproutClient,
client: &BuzzClient,
naddr: Option<&str>,
name: Option<&str>,
author: Option<&str>,
@@ -708,7 +705,7 @@ pub async fn cmd_get(
}
pub async fn cmd_ls(
client: &SproutClient,
client: &BuzzClient,
author: Option<&str>,
tag: Option<&str>,
limit: Option<u32>,
@@ -753,7 +750,7 @@ pub fn build_rm_event(coord: &nostr::nips::nip01::Coordinate) -> Result<EventBui
Ok(EventBuilder::new(Kind::EventDeletion, "").tags(vec![a_tag]))
}
pub async fn cmd_rm(client: &SproutClient, slug: &str) -> Result<(), CliError> {
pub async fn cmd_rm(client: &BuzzClient, slug: &str) -> Result<(), CliError> {
// Read-before-write: only the author can delete their own note, and we
// want a clear "nothing to delete" signal rather than emitting a kind:5
// for a coordinate that was never published.
@@ -788,7 +785,7 @@ pub async fn cmd_rm(client: &SproutClient, slug: &str) -> Result<(), CliError> {
Ok(())
}
pub async fn dispatch(cmd: crate::NotesCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::NotesCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::NotesCmd;
match cmd {
NotesCmd::Set {
@@ -1,4 +1,4 @@
//! `sprout pack` subcommands — local persona pack operations.
//! `buzz pack` subcommands — local persona pack operations.
//!
//! These commands operate on local pack directories. No relay connection needed.
@@ -6,7 +6,7 @@ use std::path::Path;
use crate::error::CliError;
/// Run `sprout pack validate <path>`.
/// Run `buzz pack validate <path>`.
///
/// Calls `validate_pack()` from the persona crate, prints diagnostics,
/// and exits with the appropriate code:
@@ -21,14 +21,14 @@ pub fn cmd_validate(path: &str) -> Result<(), CliError> {
return Err(CliError::Usage(format!("not a directory: {path}")));
}
let report = sprout_persona::validate::validate_pack(pack_dir);
let report = buzz_persona::validate::validate_pack(pack_dir);
for diag in &report.diagnostics {
match diag {
sprout_persona::validate::ValidationDiagnostic::Error(msg) => {
buzz_persona::validate::ValidationDiagnostic::Error(msg) => {
eprintln!(" ERROR: {msg}");
}
sprout_persona::validate::ValidationDiagnostic::Warning(msg) => {
buzz_persona::validate::ValidationDiagnostic::Warning(msg) => {
eprintln!(" WARN: {msg}");
}
}
@@ -45,7 +45,7 @@ pub fn cmd_validate(path: &str) -> Result<(), CliError> {
Ok(())
}
/// Run `sprout pack inspect <path>`.
/// Run `buzz pack inspect <path>`.
///
/// Loads and resolves a pack, then pretty-prints a summary of each persona's
/// effective configuration.
@@ -59,7 +59,7 @@ pub fn cmd_inspect(path: &str) -> Result<(), CliError> {
}
// Resolve the pack — shows fully effective config (post-merge, post-split).
let pack = sprout_persona::resolve::resolve_pack(pack_dir)
let pack = buzz_persona::resolve::resolve_pack(pack_dir)
.map_err(|e| CliError::Other(format!("failed to resolve pack: {e}")))?;
// Header
@@ -2,12 +2,12 @@ use std::collections::HashMap;
use nostr::EventId;
use crate::client::{normalize_write_response, SproutClient};
use crate::client::{normalize_write_response, BuzzClient};
use crate::error::CliError;
use crate::validate::validate_hex64;
pub async fn cmd_add_reaction(
client: &SproutClient,
client: &BuzzClient,
event_id: &str,
emoji: &str,
emoji_url: Option<&str>,
@@ -17,10 +17,10 @@ pub async fn cmd_add_reaction(
EventId::parse(event_id).map_err(|e| CliError::Usage(format!("invalid event ID: {e}")))?;
let builder = if let Some(url) = emoji_url {
sprout_sdk::build_custom_emoji_reaction(target_eid, emoji, url)
buzz_sdk::build_custom_emoji_reaction(target_eid, emoji, url)
.map_err(|e| CliError::Other(format!("build_custom_emoji_reaction failed: {e}")))?
} else {
sprout_sdk::build_reaction(target_eid, emoji)
buzz_sdk::build_reaction(target_eid, emoji)
.map_err(|e| CliError::Other(format!("build_reaction failed: {e}")))?
};
@@ -32,7 +32,7 @@ pub async fn cmd_add_reaction(
}
pub async fn cmd_remove_reaction(
client: &SproutClient,
client: &BuzzClient,
event_id: &str,
emoji: &str,
) -> Result<(), CliError> {
@@ -67,7 +67,7 @@ pub async fn cmd_remove_reaction(
let reaction_eid = EventId::parse(reaction_event_id)
.map_err(|e| CliError::Other(format!("invalid reaction event ID: {e}")))?;
let builder = sprout_sdk::build_remove_reaction(reaction_eid)
let builder = buzz_sdk::build_remove_reaction(reaction_eid)
.map_err(|e| CliError::Other(format!("build_remove_reaction failed: {e}")))?;
let event = client.sign_event(builder)?;
@@ -77,7 +77,7 @@ pub async fn cmd_remove_reaction(
Ok(())
}
pub async fn cmd_get_reactions(client: &SproutClient, event_id: &str) -> Result<(), CliError> {
pub async fn cmd_get_reactions(client: &BuzzClient, event_id: &str) -> Result<(), CliError> {
validate_hex64(event_id)?;
let filter = serde_json::json!({
"kinds": [7],
@@ -128,7 +128,7 @@ pub async fn cmd_get_reactions(client: &SproutClient, event_id: &str) -> Result<
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::ReactionsCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::ReactionsCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::ReactionsCmd;
match cmd {
ReactionsCmd::Add {
@@ -1,4 +1,4 @@
use crate::client::SproutClient;
use crate::client::BuzzClient;
use crate::error::CliError;
use crate::validate::validate_repo_id;
@@ -7,7 +7,7 @@ use crate::validate::validate_repo_id;
// ---------------------------------------------------------------------------
pub async fn cmd_create_repo(
client: &SproutClient,
client: &BuzzClient,
repo_id: &str,
name: Option<&str>,
description: Option<&str>,
@@ -20,7 +20,7 @@ pub async fn cmd_create_repo(
let clone_refs: Vec<&str> = clone_urls.iter().map(|s| s.as_str()).collect();
let relay_refs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect();
let builder = sprout_sdk::build_repo_announcement(
let builder = buzz_sdk::build_repo_announcement(
repo_id,
name,
description,
@@ -41,7 +41,7 @@ pub async fn cmd_create_repo(
// ---------------------------------------------------------------------------
pub async fn cmd_get_repo(
client: &SproutClient,
client: &BuzzClient,
repo_id: &str,
owner: Option<&str>,
) -> Result<(), CliError> {
@@ -69,7 +69,7 @@ pub async fn cmd_get_repo(
// ---------------------------------------------------------------------------
pub async fn cmd_list_repos(
client: &SproutClient,
client: &BuzzClient,
owner: Option<&str>,
limit: Option<u32>,
) -> Result<(), CliError> {
@@ -100,7 +100,7 @@ pub async fn cmd_list_repos(
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::ReposCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::ReposCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::ReposCmd;
match cmd {
ReposCmd::Create {
@@ -1,15 +1,15 @@
use nostr::{EventBuilder, Kind, Tag};
use serde::Deserialize;
use sprout_sdk::kind::{
use buzz_sdk::kind::{
KIND_BOOKMARK_LIST, KIND_BOOKMARK_SET, KIND_FOLLOW_SET, KIND_MUTE_LIST,
KIND_NIP65_RELAY_LIST_METADATA, KIND_PIN_LIST,
};
use nostr::{EventBuilder, Kind, Tag};
use serde::Deserialize;
use crate::client::{normalize_write_response, SproutClient};
use crate::client::{normalize_write_response, BuzzClient};
use crate::error::CliError;
use crate::validate::{parse_event_id, validate_hex64};
/// A single contact entry (CLI-local, not from sprout-sdk).
/// A single contact entry (CLI-local, not from buzz-sdk).
#[derive(Debug, Deserialize)]
pub struct ContactEntry {
pub pubkey: String,
@@ -20,7 +20,7 @@ pub struct ContactEntry {
}
pub async fn cmd_publish_note(
client: &SproutClient,
client: &BuzzClient,
content: &str,
reply_to: Option<&str>,
) -> Result<(), CliError> {
@@ -30,7 +30,7 @@ pub async fn cmd_publish_note(
let reply_id = reply_to.map(parse_event_id).transpose()?;
let builder = sprout_sdk::build_note(content, reply_id)
let builder = buzz_sdk::build_note(content, reply_id)
.map_err(|e| CliError::Other(format!("build error: {e}")))?;
let event = client.sign_event(builder)?;
@@ -41,7 +41,7 @@ pub async fn cmd_publish_note(
}
pub async fn cmd_set_contact_list(
client: &SproutClient,
client: &BuzzClient,
contacts_json: &str,
) -> Result<(), CliError> {
let entries: Vec<ContactEntry> = serde_json::from_str(contacts_json)
@@ -58,7 +58,7 @@ pub async fn cmd_set_contact_list(
})
.collect();
let builder = sprout_sdk::build_contact_list(&contacts)
let builder = buzz_sdk::build_contact_list(&contacts)
.map_err(|e| CliError::Other(format!("build error: {e}")))?;
let event = client.sign_event(builder)?;
@@ -69,7 +69,7 @@ pub async fn cmd_set_contact_list(
}
/// Get a single event by ID via POST /query.
pub async fn cmd_get_event(client: &SproutClient, event_id: &str) -> Result<(), CliError> {
pub async fn cmd_get_event(client: &BuzzClient, event_id: &str) -> Result<(), CliError> {
validate_hex64(event_id)?;
let filter = serde_json::json!({
"ids": [event_id]
@@ -81,7 +81,7 @@ pub async fn cmd_get_event(client: &SproutClient, event_id: &str) -> Result<(),
/// Get user notes (kind:1) by author pubkey.
pub async fn cmd_get_user_notes(
client: &SproutClient,
client: &BuzzClient,
pubkey: &str,
limit: Option<u32>,
before: Option<i64>,
@@ -112,7 +112,7 @@ pub async fn cmd_get_user_notes(
}
/// Get a user's contact list (kind:3) by pubkey.
pub async fn cmd_get_contact_list(client: &SproutClient, pubkey: &str) -> Result<(), CliError> {
pub async fn cmd_get_contact_list(client: &BuzzClient, pubkey: &str) -> Result<(), CliError> {
validate_hex64(pubkey)?;
let filter = serde_json::json!({
"kinds": [3],
@@ -160,7 +160,7 @@ fn has_d_tag(tags: &[Tag]) -> bool {
}
pub async fn cmd_set_list(
client: &SproutClient,
client: &BuzzClient,
kind: u16,
tags_json: &str,
content: &str,
@@ -182,7 +182,7 @@ pub async fn cmd_set_list(
}
pub async fn cmd_get_list(
client: &SproutClient,
client: &BuzzClient,
pubkey: &str,
kind: u32,
d_tag: Option<&str>,
@@ -212,7 +212,7 @@ pub async fn cmd_get_list(
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::SocialCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::SocialCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::SocialCmd;
match cmd {
SocialCmd::PublishNote { content, reply_to } => {
@@ -1,7 +1,7 @@
use crate::client::SproutClient;
use crate::client::BuzzClient;
use crate::error::CliError;
pub async fn dispatch(cmd: crate::UploadCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::UploadCmd, client: &BuzzClient) -> Result<(), CliError> {
match cmd {
crate::UploadCmd::File { file } => {
let desc = client.upload_file(&file).await?;
@@ -1,8 +1,8 @@
use crate::client::{normalize_write_response, SproutClient};
use crate::client::{normalize_write_response, BuzzClient};
use crate::error::CliError;
use crate::validate::validate_hex64;
// TODO(phase-4): Replace raw nostr::EventBuilder usage in cmd_set_presence with sprout-sdk builder
// TODO(phase-4): Replace raw nostr::EventBuilder usage in cmd_set_presence with buzz-sdk builder
/// Get user profiles (kind:0 metadata events).
///
@@ -10,7 +10,7 @@ use crate::validate::validate_hex64;
/// - 1+ pubkeys → query those users' profiles
/// - --name "foo" → NIP-50 search on kind:0, then client-side filter
pub async fn cmd_get_users(
client: &SproutClient,
client: &BuzzClient,
pubkeys: &[String],
name: Option<&str>,
format: &crate::OutputFormat,
@@ -79,7 +79,7 @@ pub async fn cmd_get_users(
/// Search for users by display name via NIP-50 full-text search on kind:0 profiles.
/// Returns [] if the relay does not implement NIP-50 search.
async fn search_by_name(
client: &SproutClient,
client: &BuzzClient,
query: &str,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
@@ -148,7 +148,7 @@ async fn search_by_name(
}
pub async fn cmd_set_profile(
client: &SproutClient,
client: &BuzzClient,
display_name: Option<&str>,
avatar_url: Option<&str>,
about: Option<&str>,
@@ -197,7 +197,7 @@ pub async fn cmd_set_profile(
.map(|s| s.to_string())
});
let builder = sprout_sdk::build_profile(
let builder = buzz_sdk::build_profile(
merged_name.as_deref(),
None, // `name` field (username) — not exposed by CLI
merged_picture.as_deref(),
@@ -216,7 +216,7 @@ pub async fn cmd_set_profile(
/// Fetch the current user's profile metadata via POST /query (kind:0).
/// Returns the parsed content JSON object, or an empty object if no profile exists.
async fn fetch_current_profile(
client: &SproutClient,
client: &BuzzClient,
) -> Result<serde_json::Map<String, serde_json::Value>, CliError> {
let my_pk = client.keys().public_key().to_hex();
let filter = serde_json::json!({
@@ -244,7 +244,7 @@ async fn fetch_current_profile(
}
/// Get presence status for users — query kind:40902 presence snapshot events.
pub async fn cmd_get_presence(client: &SproutClient, pubkeys_csv: &str) -> Result<(), CliError> {
pub async fn cmd_get_presence(client: &BuzzClient, pubkeys_csv: &str) -> Result<(), CliError> {
let pubkeys: Vec<&str> = pubkeys_csv
.split(',')
.map(|s| s.trim())
@@ -281,8 +281,8 @@ pub async fn cmd_get_presence(client: &SproutClient, pubkeys_csv: &str) -> Resul
/// Kind 20001 is ephemeral and only accepted via WebSocket connections. This
/// method connects to the relay over WS, performs NIP-42 authentication, and
/// publishes the event directly — bypassing the HTTP bridge.
pub async fn cmd_set_presence(client: &SproutClient, status: &str) -> Result<(), CliError> {
let builder = sprout_sdk::build_presence_update(status).map_err(crate::validate::sdk_err)?;
pub async fn cmd_set_presence(client: &BuzzClient, status: &str) -> Result<(), CliError> {
let builder = buzz_sdk::build_presence_update(status).map_err(crate::validate::sdk_err)?;
let event = client.sign_event(builder)?;
let resp = client.publish_ephemeral_event(event).await?;
@@ -296,7 +296,7 @@ pub async fn cmd_set_presence(client: &SproutClient, status: &str) -> Result<(),
pub async fn dispatch(
cmd: crate::UsersCmd,
client: &SproutClient,
client: &BuzzClient,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
use crate::UsersCmd;
@@ -1,17 +1,17 @@
use sha2::{Digest, Sha256};
use crate::client::{extract_d_tag, normalize_write_response, print_create_response, SproutClient};
use crate::client::{extract_d_tag, normalize_write_response, print_create_response, BuzzClient};
use crate::error::CliError;
use crate::validate::{parse_uuid, read_or_stdin, sdk_err, validate_uuid};
// TODO(phase-4): Replace raw nostr::EventBuilder usage with sprout-sdk builder functions
// TODO(phase-4): Replace raw nostr::EventBuilder usage with buzz-sdk builder functions
// ---------------------------------------------------------------------------
// Read commands — POST /query
// ---------------------------------------------------------------------------
/// List workflows in a channel — query kind:30620 workflow definition events.
pub async fn cmd_list_workflows(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
pub async fn cmd_list_workflows(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
validate_uuid(channel_id)?;
let filter = serde_json::json!({
"kinds": [30620],
@@ -36,7 +36,7 @@ pub async fn cmd_list_workflows(client: &SproutClient, channel_id: &str) -> Resu
}
/// Get a single workflow definition.
pub async fn cmd_get_workflow(client: &SproutClient, workflow_id: &str) -> Result<(), CliError> {
pub async fn cmd_get_workflow(client: &BuzzClient, workflow_id: &str) -> Result<(), CliError> {
validate_uuid(workflow_id)?;
let filter = serde_json::json!({
"kinds": [30620],
@@ -65,7 +65,7 @@ pub async fn cmd_get_workflow(client: &SproutClient, workflow_id: &str) -> Resul
/// This command will return an empty array until the relay adds event emission
/// or a dedicated REST endpoint for run history.
pub async fn cmd_get_workflow_runs(
client: &SproutClient,
client: &BuzzClient,
workflow_id: &str,
limit: Option<u32>,
) -> Result<(), CliError> {
@@ -101,7 +101,7 @@ pub async fn cmd_get_workflow_runs(
/// Create a workflow — sign and submit a kind:30620 event.
pub async fn cmd_create_workflow(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
yaml: &str,
) -> Result<(), CliError> {
@@ -109,7 +109,7 @@ pub async fn cmd_create_workflow(
let yaml_definition = read_or_stdin(yaml)?;
let workflow_id = uuid::Uuid::new_v4();
let builder = sprout_sdk::build_workflow_def(channel_uuid, workflow_id, &yaml_definition)
let builder = buzz_sdk::build_workflow_def(channel_uuid, workflow_id, &yaml_definition)
.map_err(sdk_err)?;
let event = client.sign_event(builder)?;
@@ -120,7 +120,7 @@ pub async fn cmd_create_workflow(
/// Update a workflow — sign and submit an updated kind:30620 event with same d-tag.
pub async fn cmd_update_workflow(
client: &SproutClient,
client: &BuzzClient,
channel_id: &str,
workflow_id: &str,
yaml: &str,
@@ -129,7 +129,7 @@ pub async fn cmd_update_workflow(
let wf_uuid = parse_uuid(workflow_id)?;
let yaml_definition = read_or_stdin(yaml)?;
let builder = sprout_sdk::build_workflow_update(channel_uuid, wf_uuid, &yaml_definition)
let builder = buzz_sdk::build_workflow_update(channel_uuid, wf_uuid, &yaml_definition)
.map_err(sdk_err)?;
let event = client.sign_event(builder)?;
@@ -139,12 +139,12 @@ pub async fn cmd_update_workflow(
}
/// Delete a workflow — sign and submit a kind:5 deletion event.
pub async fn cmd_delete_workflow(client: &SproutClient, workflow_id: &str) -> Result<(), CliError> {
pub async fn cmd_delete_workflow(client: &BuzzClient, workflow_id: &str) -> Result<(), CliError> {
let wf_uuid = parse_uuid(workflow_id)?;
let keys = client.keys();
let builder =
sprout_sdk::build_workflow_delete(&keys.public_key().to_hex(), wf_uuid).map_err(sdk_err)?;
buzz_sdk::build_workflow_delete(&keys.public_key().to_hex(), wf_uuid).map_err(sdk_err)?;
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
@@ -157,7 +157,7 @@ pub async fn cmd_delete_workflow(client: &SproutClient, workflow_id: &str) -> Re
/// When `inputs` is provided, it is parsed as a JSON object and used as the
/// event content (MCP parity). When omitted, the event content is `{}`.
pub async fn cmd_trigger_workflow(
client: &SproutClient,
client: &BuzzClient,
workflow_id: &str,
inputs: Option<&str>,
) -> Result<(), CliError> {
@@ -176,7 +176,7 @@ pub async fn cmd_trigger_workflow(
let tags = vec![Tag::parse(["d", &wf_uuid.to_string()])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
let builder = EventBuilder::new(
Kind::Custom(sprout_sdk::kind::KIND_WORKFLOW_TRIGGER as u16),
Kind::Custom(buzz_sdk::kind::KIND_WORKFLOW_TRIGGER as u16),
&content,
)
.tags(tags);
@@ -184,7 +184,7 @@ pub async fn cmd_trigger_workflow(
let resp = client.submit_event(event).await?;
println!("{}", normalize_write_response(&resp));
} else {
let builder = sprout_sdk::build_workflow_trigger(wf_uuid).map_err(sdk_err)?;
let builder = buzz_sdk::build_workflow_trigger(wf_uuid).map_err(sdk_err)?;
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
println!("{}", normalize_write_response(&resp));
@@ -194,7 +194,7 @@ pub async fn cmd_trigger_workflow(
/// Approve or deny a workflow step — sign and submit a kind:46030 (grant) or 46031 (deny) event.
pub async fn cmd_approve_step(
client: &SproutClient,
client: &BuzzClient,
approval_token: &str,
approved: bool,
note: Option<&str>,
@@ -206,7 +206,7 @@ pub async fn cmd_approve_step(
// The relay expects d-tag = hex(SHA256(token)), not the raw token UUID.
let token_hash = hex::encode(Sha256::digest(approval_token.as_bytes()));
let builder =
sprout_sdk::build_workflow_approval(&token_hash, approved, content).map_err(sdk_err)?;
buzz_sdk::build_workflow_approval(&token_hash, approved, content).map_err(sdk_err)?;
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
@@ -218,7 +218,7 @@ pub async fn cmd_approve_step(
// Dispatch
// ---------------------------------------------------------------------------
pub async fn dispatch(cmd: crate::WorkflowsCmd, client: &SproutClient) -> Result<(), CliError> {
pub async fn dispatch(cmd: crate::WorkflowsCmd, client: &BuzzClient) -> Result<(), CliError> {
use crate::WorkflowsCmd;
match cmd {
WorkflowsCmd::List { channel } => cmd_list_workflows(client, &channel).await,
@@ -18,16 +18,16 @@ pub enum CliError {
#[error("auth error: {0}")]
Auth(String),
/// Nostr key error (NIP-98 signing in `sprout auth`)
/// Nostr key error (NIP-98 signing in `buzz auth`)
#[error("key error: {0}")]
Key(String),
/// Relay accepted the event but reported it as superseded by a newer
/// head — used by `sprout mem` set/rm to surface NIP-33 LWW conflicts.
/// head — used by `buzz mem` set/rm to surface NIP-33 LWW conflicts.
#[error("conflict: {0}")]
Conflict(String),
/// Requested resource was absent or tombstoned (e.g. `sprout mem get`
/// Requested resource was absent or tombstoned (e.g. `buzz mem get`
/// for a slug with no head).
#[error("{0}")]
NotFound(String),
@@ -4,18 +4,18 @@ mod error;
mod validate;
use clap::{Parser, Subcommand};
use client::SproutClient;
use client::BuzzClient;
use error::CliError;
use nostr::Keys;
/// Run the Sprout CLI from raw arguments (including `argv[0]`).
/// Run the Buzz CLI from raw arguments (including `argv[0]`).
///
/// Returns a process exit code (0 = success).
///
/// # Example
///
/// ```ignore
/// let code = sprout_cli::run_from_args(std::env::args()).await;
/// let code = buzz_cli::run_from_args(std::env::args()).await;
/// std::process::exit(code);
/// ```
pub async fn run_from_args<I, S>(args: I) -> i32
@@ -51,15 +51,15 @@ where
#[derive(Parser)]
#[command(
name = "sprout",
about = "Sprout CLI — interact with a Sprout relay",
name = "buzz",
about = "Buzz CLI — interact with a Buzz relay",
long_about = "\
Sprout CLI — interact with a Sprout relay
Buzz CLI — interact with a Buzz relay
Configuration (flags override env vars):
SPROUT_RELAY_URL Relay base URL [default: http://localhost:3000]
SPROUT_PRIVATE_KEY Nostr private key (hex or nsec) [required]
SPROUT_AUTH_TAG NIP-OA auth tag JSON [optional]
BUZZ_RELAY_URL Relay base URL [default: http://localhost:3000]
BUZZ_PRIVATE_KEY Nostr private key (hex or nsec) [required]
BUZZ_AUTH_TAG NIP-OA auth tag JSON [optional]
The 'pack' subcommand runs locally and does not require a relay connection.
@@ -67,20 +67,16 @@ Exit codes: 0=ok 1=bad input 2=relay/network error 3=auth error 4=other 5=w
Errors are JSON on stderr: {\"error\": \"<category>\", \"message\": \"<detail>\"}"
)]
struct Cli {
/// Relay URL (http:// or https://). Overrides SPROUT_RELAY_URL env var.
#[arg(
long,
env = "SPROUT_RELAY_URL",
default_value = "http://localhost:3000"
)]
/// Relay URL (http:// or https://). Overrides BUZZ_RELAY_URL env var.
#[arg(long, env = "BUZZ_RELAY_URL", default_value = "http://localhost:3000")]
relay: String,
/// Nostr private key (hex or nsec). This is the CLI's identity.
#[arg(long, env = "SPROUT_PRIVATE_KEY")]
#[arg(long, env = "BUZZ_PRIVATE_KEY")]
private_key: Option<String>,
/// NIP-OA auth tag JSON (owner attestation). Injected into every signed event.
#[arg(long, env = "SPROUT_AUTH_TAG")]
#[arg(long, env = "BUZZ_AUTH_TAG")]
auth_tag: Option<String>,
/// Output format: 'json' (default, full fields) or 'compact' (reduced fields).
@@ -230,10 +226,10 @@ enum Cmd {
pub enum MessagesCmd {
/// Send a message to a channel
#[command(
after_help = "Examples:\n sprout messages send --channel <UUID> --content \"hello\"\n sprout messages send --channel <UUID> --content \"@alice check this\"\n echo \"hello from stdin\" | sprout messages send --channel <UUID> --content -"
after_help = "Examples:\n buzz messages send --channel <UUID> --content \"hello\"\n buzz messages send --channel <UUID> --content \"@alice check this\"\n echo \"hello from stdin\" | buzz messages send --channel <UUID> --content -"
)]
Send {
/// Channel UUID (from 'sprout channels list')
/// Channel UUID (from 'buzz channels list')
#[arg(long)]
channel: String,
/// Message text — supports @mentions and markdown. Use '-' to read from stdin.
@@ -308,7 +304,7 @@ pub enum MessagesCmd {
},
/// Retrieve messages from a channel
#[command(
after_help = "Examples:\n sprout messages get --channel <UUID>\n sprout messages get --channel <UUID> --limit 50 --kinds 1,1984"
after_help = "Examples:\n buzz messages get --channel <UUID>\n buzz messages get --channel <UUID> --limit 50 --kinds 1,1984"
)]
Get {
/// Channel UUID
@@ -370,7 +366,7 @@ pub enum MessagesCmd {
pub enum ChannelsCmd {
/// List channels visible to the current identity
#[command(
after_help = "Examples:\n sprout channels list\n sprout channels list --visibility open"
after_help = "Examples:\n buzz channels list\n buzz channels list --visibility open"
)]
List {
/// Filter by visibility
@@ -391,7 +387,7 @@ pub enum ChannelsCmd {
},
/// Search channels by human-readable name
#[command(
after_help = "Examples:\n sprout channels search --query composer\n sprout channels search --query sprout-chat-composer --exact\n sprout channels search --query design --include-archived"
after_help = "Examples:\n buzz channels search --query composer\n buzz channels search --query buzz-chat-composer --exact\n buzz channels search --query design --include-archived"
)]
Search {
/// Search query (case-insensitive substring of channel name)
@@ -409,7 +405,7 @@ pub enum ChannelsCmd {
},
/// Create a new channel
#[command(
after_help = "Examples:\n sprout channels create --name general --type stream --visibility open\n sprout channels create --name design --type forum --visibility open --description \"Design discussions\""
after_help = "Examples:\n buzz channels create --name general --type stream --visibility open\n buzz channels create --name design --type forum --visibility open --description \"Design discussions\""
)]
Create {
/// Channel name
@@ -755,7 +751,7 @@ pub enum WorkflowsCmd {
},
/// Trigger a workflow run
#[command(
after_help = "Examples:\n sprout workflows trigger --workflow <UUID>\n sprout workflows trigger --workflow <UUID> --inputs '{\"key\":\"value\"}'"
after_help = "Examples:\n buzz workflows trigger --workflow <UUID>\n buzz workflows trigger --workflow <UUID> --inputs '{\"key\":\"value\"}'"
)]
Trigger {
/// Workflow UUID
@@ -776,7 +772,7 @@ pub enum WorkflowsCmd {
},
/// Approve or deny a workflow step
#[command(
after_help = "Examples:\n sprout workflows approve --token <UUID>\n sprout workflows approve --token <UUID> --approved false --note \"needs revision\""
after_help = "Examples:\n buzz workflows approve --token <UUID>\n buzz workflows approve --token <UUID> --approved false --note \"needs revision\""
)]
Approve {
/// The approval token UUID (from the approval request)
@@ -905,7 +901,7 @@ pub enum NotesCmd {
/// title is carried forward when `--title` is omitted, and `--title ""`
/// explicitly clears it.
#[command(
after_help = "Examples:\n echo '# Hello' | sprout notes set --name hello --title 'Hello' --content -\n sprout notes set --name hello --tag onboarding --content - < draft.md"
after_help = "Examples:\n echo '# Hello' | buzz notes set --name hello --title 'Hello' --content -\n buzz notes set --name hello --tag onboarding --content - < draft.md"
)]
Set {
/// Slug — becomes the `d` tag. `[a-z0-9._-]{1,80}`.
@@ -1040,12 +1036,12 @@ pub enum UploadCmd {
// Mem subcommands (NIP-AE)
// ---------------------------------------------------------------------------
/// Subcommands for `sprout mem`.
/// Subcommands for `buzz mem`.
#[derive(Subcommand)]
pub enum MemCmd {
/// List non-tombstoned memory entries
Ls {
/// Owner pubkey (hex). Overrides SPROUT_AUTH_TAG.
/// Owner pubkey (hex). Overrides BUZZ_AUTH_TAG.
#[arg(long)]
owner: Option<String>,
/// Agent pubkey (hex) to read as this key's owner.
@@ -1096,8 +1092,8 @@ pub enum MemCmd {
#[arg(long)]
patch_file: Option<String>,
/// sha256 hex digest (lowercase) of the value the patch was generated
/// against. Hashes the exact UTF-8 bytes returned by `sprout mem get`,
/// not normalized lines. Run `sprout mem hash <slug>` to capture this
/// against. Hashes the exact UTF-8 bytes returned by `buzz mem get`,
/// not normalized lines. Run `buzz mem hash <slug>` to capture this
/// before editing.
#[arg(long)]
base_hash: Option<String>,
@@ -1127,7 +1123,7 @@ pub enum MemCmd {
// Pack subcommands (local, no relay connection needed)
// ---------------------------------------------------------------------------
/// Subcommands for `sprout pack`.
/// Subcommands for `buzz pack`.
#[derive(Subcommand)]
pub enum PackCmd {
/// Validate a persona pack directory
@@ -1160,19 +1156,19 @@ async fn run(cli: Cli) -> Result<(), CliError> {
// Auth: private key is required for all relay operations.
// The keypair IS the identity — no tokens, no other auth.
let private_key_str = cli.private_key.ok_or_else(|| {
CliError::Auth("SPROUT_PRIVATE_KEY is required (use --private-key or set env var)".into())
CliError::Auth("BUZZ_PRIVATE_KEY is required (use --private-key or set env var)".into())
})?;
let keys = Keys::parse(&private_key_str)
.map_err(|e| CliError::Key(format!("invalid SPROUT_PRIVATE_KEY: {e}")))?;
.map_err(|e| CliError::Key(format!("invalid BUZZ_PRIVATE_KEY: {e}")))?;
// NIP-OA: parse and verify the auth tag if provided.
let (auth_tag, auth_tag_json) = match cli.auth_tag {
Some(ref json) if !json.is_empty() => {
let tag = sprout_sdk::nip_oa::parse_auth_tag(json)
.map_err(|e| CliError::Auth(format!("SPROUT_AUTH_TAG is malformed: {e}")))?;
sprout_sdk::nip_oa::verify_auth_tag(json, &keys.public_key()).map_err(|e| {
let tag = buzz_sdk::nip_oa::parse_auth_tag(json)
.map_err(|e| CliError::Auth(format!("BUZZ_AUTH_TAG is malformed: {e}")))?;
buzz_sdk::nip_oa::verify_auth_tag(json, &keys.public_key()).map_err(|e| {
CliError::Auth(format!(
"SPROUT_AUTH_TAG verification failed for pubkey {}: {e}",
"BUZZ_AUTH_TAG verification failed for pubkey {}: {e}",
keys.public_key().to_hex()
))
})?;
@@ -1181,7 +1177,7 @@ async fn run(cli: Cli) -> Result<(), CliError> {
_ => (None, None),
};
let client = SproutClient::new(relay_url, keys, auth_tag, auth_tag_json)?;
let client = BuzzClient::new(relay_url, keys, auth_tag, auth_tag_json)?;
match cli.command {
Cmd::Messages(sub) => commands::messages::dispatch(sub, &client, &cli.format).await,
+4
View File
@@ -0,0 +1,4 @@
#[tokio::main]
async fn main() {
std::process::exit(buzz_cli::run_from_args(std::env::args()).await);
}
@@ -157,9 +157,9 @@ pub fn infer_language(file_path: &str) -> Option<String> {
/// Map `SdkError` to the appropriate `CliError` variant.
///
/// `InvalidInput` is a user error (exit 1), everything else is internal (exit 4).
pub fn sdk_err(e: sprout_sdk::SdkError) -> CliError {
pub fn sdk_err(e: buzz_sdk::SdkError) -> CliError {
match e {
sprout_sdk::SdkError::InvalidInput(msg) => CliError::Usage(msg),
buzz_sdk::SdkError::InvalidInput(msg) => CliError::Usage(msg),
other => CliError::Other(other.to_string()),
}
}
@@ -373,7 +373,7 @@ mod tests {
}
// Note: `extract_at_names`, `extract_at_mentions_with_known`, `merge_mentions`,
// and `normalize_mention_pubkeys` live in `sprout_sdk::mentions` and are tested there.
// and `normalize_mention_pubkeys` live in `buzz_sdk::mentions` and are tested there.
// --- parse_event_id ---
@@ -1,11 +1,11 @@
[package]
name = "sprout-core"
name = "buzz-core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Core types, event verification, and filter matching for Sprout"
description = "Core types, event verification, and filter matching for Buzz"
[features]
test-utils = []
@@ -1,6 +1,6 @@
//! Channel and membership enums shared across crates.
//!
//! These live in `sprout-core` (zero I/O deps) so both the SDK (client-side)
//! These live in `buzz-core` (zero I/O deps) so both the SDK (client-side)
//! and the DB layer (server-side) can use the same types without pulling in
//! sqlx/tokio.
@@ -4,7 +4,7 @@
//! not talk to relays or filesystems. Callers wire it to a transport and a
//! key source.
//!
//! Shared by `sprout-cli` (`sprout mem …`) and `sprout-acp` (core injection
//! Shared by `buzz-cli` (`buzz mem …`) and `buzz-acp` (core injection
//! at session creation).
use hmac::digest::KeyInit;
@@ -598,7 +598,7 @@ pub fn monotonic_created_at(now: u64, prior_head: Option<u64>) -> u64 {
}
}
/// Wire representation for `sprout mem ls`: one entry per non-tombstone
/// Wire representation for `buzz mem ls`: one entry per non-tombstone
/// memory slug (`core` is excluded by the listing procedure).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Listing {
@@ -56,7 +56,7 @@ mod tests {
fn make_event() -> nostr::Event {
let keys = Keys::generate();
EventBuilder::new(Kind::TextNote, "hello sprout")
EventBuilder::new(Kind::TextNote, "hello buzz")
.tags([])
.sign_with_keys(&keys)
.expect("sign")
@@ -1,7 +1,7 @@
//! Git permission types — ref patterns, protection rules, and policy evaluation inputs.
//!
//! This module defines the core data types for the Sprout git permission system.
//! The permission model: channel role = repo role; `sprout-protect` tags on
//! This module defines the core data types for the Buzz git permission system.
//! The permission model: channel role = repo role; `buzz-protect` tags on
//! kind:30617 add constraints that apply to everyone (including the owner).
//!
//! # Architecture
@@ -17,7 +17,7 @@ use std::fmt;
// ── Limits (DoS prevention for untrusted kind:30617 input) ───────────────────
/// Maximum number of `sprout-protect` tags per repo.
/// Maximum number of `buzz-protect` tags per repo.
pub const MAX_PROTECTION_RULES: usize = 50;
/// Maximum character length of a ref pattern.
pub const MAX_PATTERN_LENGTH: usize = 256;
@@ -244,9 +244,9 @@ pub struct RefUpdate {
// ── Protection Rules ─────────────────────────────────────────────────────────
/// A single protection rule parsed from a `sprout-protect` tag on kind:30617.
/// A single protection rule parsed from a `buzz-protect` tag on kind:30617.
///
/// Format: `["sprout-protect", "<ref-pattern>", "<rule>", ...]`
/// Format: `["buzz-protect", "<ref-pattern>", "<rule>", ...]`
/// Multiple rules per tag are allowed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtectionRule {
@@ -267,7 +267,7 @@ pub struct ProtectionRule {
pub require_patch: bool,
}
/// Errors from parsing a `sprout-protect` tag.
/// Errors from parsing a `buzz-protect` tag.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuleParseError {
/// Tag has fewer than 2 values (need at least pattern + one rule).
@@ -285,7 +285,7 @@ pub enum RuleParseError {
impl fmt::Display for RuleParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooFewValues => write!(f, "sprout-protect tag needs pattern + at least one rule"),
Self::TooFewValues => write!(f, "buzz-protect tag needs pattern + at least one rule"),
Self::TooManyRules => write!(f, "exceeds max {MAX_PROTECTION_RULES} rules per repo"),
Self::InvalidPattern(e) => write!(f, "invalid pattern: {e}"),
Self::UnknownRule(r) => write!(f, "unknown rule: {r:?}"),
@@ -296,18 +296,18 @@ impl fmt::Display for RuleParseError {
impl std::error::Error for RuleParseError {}
/// Parse a single `sprout-protect` tag into a `ProtectionRule`.
/// Parse a single `buzz-protect` tag into a `ProtectionRule`.
///
/// Tag format: `["sprout-protect", "<pattern>", "<rule1>", "<rule2>", ...]`
/// The first element ("sprout-protect") should already be stripped — pass
/// Tag format: `["buzz-protect", "<pattern>", "<rule1>", "<rule2>", ...]`
/// The first element ("buzz-protect") should already be stripped — pass
/// the remaining values starting with the pattern.
/// Parse a single `sprout-protect` tag (simple API, discards unknown rules).
/// Parse a single `buzz-protect` tag (simple API, discards unknown rules).
pub fn parse_protection_tag(values: &[&str]) -> Result<ProtectionRule, RuleParseError> {
let (rule, _unknowns) = parse_protection_tag_with_warnings(values)?;
Ok(rule)
}
/// Parse a single `sprout-protect` tag, returning unknown rules for logging.
/// Parse a single `buzz-protect` tag, returning unknown rules for logging.
pub fn parse_protection_tag_with_warnings(
values: &[&str],
) -> Result<(ProtectionRule, Vec<String>), RuleParseError> {
@@ -378,9 +378,9 @@ pub struct ParsedProtection {
pub unknown_rules: Vec<String>,
}
/// Parse all `sprout-protect` tags from a kind:30617 event's tag list.
/// Parse all `buzz-protect` tags from a kind:30617 event's tag list.
///
/// Returns an error if any `sprout-protect` tag is structurally malformed.
/// Returns an error if any `buzz-protect` tag is structurally malformed.
/// Unknown rule strings are skipped but reported in `ParsedProtection::unknown_rules`
/// so callers can log warnings (helps catch typos while maintaining forward-compat).
/// Enforces the per-repo rule count limit.
@@ -389,7 +389,7 @@ pub fn parse_protection_tags(tags: &[Vec<String>]) -> Result<ParsedProtection, R
let mut unknown_rules = Vec::new();
for tag in tags {
if tag.first().map(|s| s.as_str()) != Some("sprout-protect") {
if tag.first().map(|s| s.as_str()) != Some("buzz-protect") {
continue;
}
if rules.len() >= MAX_PROTECTION_RULES {
@@ -409,7 +409,7 @@ pub fn parse_protection_tags(tags: &[Vec<String>]) -> Result<ParsedProtection, R
// ── Built-in Defaults ────────────────────────────────────────────────────────
/// Built-in default minimum role for an operation when no `sprout-protect` tag matches.
/// Built-in default minimum role for an operation when no `buzz-protect` tag matches.
pub fn default_min_role(ref_name: &str, kind: UpdateKind) -> MemberRole {
let is_branch = ref_name.starts_with("refs/heads/");
let is_tag = ref_name.starts_with("refs/tags/");
@@ -1,6 +1,6 @@
//! Sprout V2 kind number registry.
//! Buzz V2 kind number registry.
//!
//! This module is the authoritative source for Sprout kind numbers.
//! This module is the authoritative source for Buzz kind numbers.
//! All constants are `u32` — NIP-01 specifies kind as an unsigned integer,
//! and u32 covers the full range without truncation.
@@ -50,7 +50,7 @@ pub const KIND_BOOKMARK_SET: u32 = 30003;
/// `required_scope_for_kind`), and the generic NIP-33 replace path keeps only the
/// latest per `(pubkey, d_tag)`.
pub const KIND_EMOJI_SET: u32 = 30030;
/// NIP-01: Channel metadata (replaceable). Not used by Sprout today.
/// NIP-01: Channel metadata (replaceable). Not used by Buzz today.
pub const KIND_CHANNEL_METADATA: u32 = 41;
/// NIP-09: Event deletion request.
pub const KIND_DELETION: u32 = 5;
@@ -80,7 +80,7 @@ pub const KIND_BLOSSOM_AUTH: u32 = 24242;
/// NIP-98: HTTP auth event (used in nip98.rs, not stored).
pub const KIND_HTTP_AUTH: u32 = 27235;
// NEW: Sprout command kinds (Pure Nostr plan)
// NEW: Buzz command kinds (Pure Nostr plan)
/// Agent metadata + owner reference (replaceable, agent-authored).
pub const KIND_AGENT_PROFILE: u32 = 10100;
@@ -155,7 +155,7 @@ pub const KIND_NIP29_GROUP_ROLES: u32 = 39003;
/// Workflow definition (parameterized replaceable, d=workflow_uuid).
pub const KIND_WORKFLOW_DEF: u32 = 30620;
/// Mesh-LLM relay status (relay-signed, parameterized replaceable, d=sprout-relay-mesh).
/// Mesh-LLM relay status (relay-signed, parameterized replaceable, d=buzz-relay-mesh).
///
/// Published only by the relay. Carries a sanitized, member-readable projection
/// of mesh status, including EndpointAddr dial pointers for serving nodes.
@@ -247,7 +247,7 @@ pub const KIND_DM_HIDE: u32 = 41012;
pub const KIND_DM_CREATED: u32 = 41001;
// Agent job protocol (43000–43999)
// Not using NIP-90 kinds (5000–6999) — Sprout requires auth chains (depth ≤ 3, breadth ≤ 10).
// Not using NIP-90 kinds (5000–6999) — Buzz requires auth chains (depth ≤ 3, breadth ≤ 10).
/// An agent job was requested.
pub const KIND_JOB_REQUEST: u32 = 43001;
/// An agent accepted a job request.
@@ -509,7 +509,7 @@ pub const fn is_identity_archive_request_kind(kind: u32) -> bool {
matches!(kind, KIND_IA_ARCHIVE_REQUEST | KIND_IA_UNARCHIVE_REQUEST)
}
/// Returns `true` if `kind` is a Sprout command kind that requires transactional execution.
/// Returns `true` if `kind` is a Buzz command kind that requires transactional execution.
pub const fn is_command_kind(kind: u32) -> bool {
matches!(
kind,
@@ -542,7 +542,7 @@ pub fn event_kind_u32(event: &nostr::Event) -> u32 {
}
/// Extract the kind from a nostr Event as i32 (for Postgres INT columns).
/// Safe: all Sprout kinds fit in i32 (max 65535 < i32::MAX).
/// Safe: all Buzz kinds fit in i32 (max 65535 < i32::MAX).
pub fn event_kind_i32(event: &nostr::Event) -> i32 {
event.kind.as_u16() as i32
}
@@ -563,7 +563,7 @@ const _: () = assert!(
&& KIND_GIT_REPO_STATE <= PARAM_REPLACEABLE_KIND_MAX
);
// Compile-time: all Sprout kind constants fit in nostr's u16-backed Kind.
// Compile-time: all Buzz kind constants fit in nostr's u16-backed Kind.
const _: () = assert!(KIND_AUTH <= u16::MAX as u32);
const _: () = assert!(KIND_CANVAS <= u16::MAX as u32);
const _: () = assert!(KIND_HUDDLE_GUIDELINES <= u16::MAX as u32);
@@ -1,9 +1,9 @@
#![deny(unsafe_code)]
#![warn(missing_docs)]
//! `sprout-core` — zero-I/O foundation types for the Sprout relay.
//! `buzz-core` — zero-I/O foundation types for the Buzz relay.
//!
//! Provides [`StoredEvent`], filter matching, kind constants, and event
//! verification. All other Sprout crates depend on this one.
//! verification. All other Buzz crates depend on this one.
/// Channel and membership enums shared across crates.
pub mod channel;
@@ -18,7 +18,7 @@ pub mod event;
pub mod filter;
/// Git permission types — ref patterns, protection rules, policy evaluation.
pub mod git_perms;
/// Sprout kind number registry — custom event type constants.
/// Buzz kind number registry — custom event type constants.
pub mod kind;
/// Network utilities — SSRF-safe IP classification.
pub mod network;
@@ -1,4 +1,4 @@
//! Network utility functions for Sprout.
//! Network utility functions for Buzz.
//!
//! Provides shared helpers used across crates for SSRF protection and
//! IP address classification.
@@ -1,7 +1,7 @@
//! Agent observer frame helpers.
//!
//! Observer frames are transient, owner-scoped agent telemetry/control messages.
//! They use a Sprout ephemeral event kind and carry NIP-44 encrypted JSON in the
//! They use a Buzz ephemeral event kind and carry NIP-44 encrypted JSON in the
//! event content so relays can route frames without reading ACP internals.
use nostr::{nips::nip44, Event, Keys, PublicKey};
@@ -672,7 +672,7 @@ Those behaviors remain normative in this document and in the Rust implementation
Run the proof with:
```bash
tamarin-prover --prove crates/sprout-core/src/pairing/NIP-AB.spthy
tamarin-prover --prove crates/buzz-core/src/pairing/NIP-AB.spthy
```
## Cryptographic Primitives
@@ -746,7 +746,7 @@ transcript_hash = HKDF-SHA256(IKM=transcript, salt=session_secret, info="nostr-p
d662818ff8911fc60a2d025f8b8b4756107104e85888dd202d28db5ca2cf28d3
```
Implementations MUST validate against these vectors. They can be reproduced with `sprout-pair test-vectors`.
Implementations MUST validate against these vectors. They can be reproduced with `buzz-pair test-vectors`.
A future external vector file (`nip-ab.vectors.json`) with a sha256 checksum committed in this document is planned. When published, it will include categorized intermediate-value vectors for each derivation step and negative/invalid test cases. The sha256 checksum will be the canonical commitment; implementations MUST verify against the checksum before using the file.
@@ -113,7 +113,7 @@ pub fn derive_transcript_hash(
///
/// # Examples
/// ```
/// use sprout_core::pairing::crypto::format_sas;
/// use buzz_core::pairing::crypto::format_sas;
/// assert_eq!(format_sas(291), "000291");
/// assert_eq!(format_sas(47291), "047291");
/// assert_eq!(format_sas(999999), "999999");
@@ -67,7 +67,7 @@ impl Drop for QrPayload {
/// # Example
///
/// ```
/// use sprout_core::pairing::qr::{QrPayload, encode_qr};
/// use buzz_core::pairing::qr::{QrPayload, encode_qr};
/// use nostr::Keys;
///
/// let keys = Keys::generate();

Some files were not shown because too many files have changed in this diff Show More