Rename desktop app to Buzz (#960)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
90
.env.example
@@ -1,5 +1,5 @@
|
||||
# =============================================================================
|
||||
# Sprout Backend — Local Development Environment
|
||||
# Buzz Backend — Local Development Environment
|
||||
# =============================================================================
|
||||
# Copy this file to .env and adjust as needed:
|
||||
# cp .env.example .env
|
||||
@@ -18,12 +18,12 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database (Postgres 17)
|
||||
# -----------------------------------------------------------------------------
|
||||
DATABASE_URL=postgres://sprout:sprout_dev@localhost:5432/sprout
|
||||
DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz
|
||||
PGHOST=localhost
|
||||
PGPORT=5432
|
||||
PGUSER=sprout
|
||||
PGPASSWORD=sprout_dev
|
||||
PGDATABASE=sprout
|
||||
PGUSER=buzz
|
||||
PGPASSWORD=buzz_dev
|
||||
PGDATABASE=buzz
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Redis 7
|
||||
@@ -33,23 +33,23 @@ REDIS_URL=redis://localhost:6379
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typesense (search)
|
||||
# -----------------------------------------------------------------------------
|
||||
TYPESENSE_API_KEY=sprout_dev_key
|
||||
TYPESENSE_API_KEY=buzz_dev_key
|
||||
TYPESENSE_URL=http://localhost:8108
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Relay (WebSocket server)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Bind address for the relay (host:port)
|
||||
SPROUT_BIND_ADDR=0.0.0.0:3000
|
||||
BUZZ_BIND_ADDR=0.0.0.0:3000
|
||||
# Public WebSocket URL — used in NIP-42 auth challenges
|
||||
RELAY_URL=ws://localhost:3000
|
||||
# Stable relay signing key. Set this in dev if you want REST-created forum posts
|
||||
# to keep resolving to the original author across relay restarts.
|
||||
# SPROUT_RELAY_PRIVATE_KEY=<32-byte hex private key>
|
||||
# BUZZ_RELAY_PRIVATE_KEY=<32-byte hex private key>
|
||||
# Optional: path to the web UI dist directory. When set, the relay serves
|
||||
# the web frontend at / for browser requests. Leave unset for local dev
|
||||
# (use `just web` for Vite HMR instead).
|
||||
# SPROUT_WEB_DIR=./web/dist
|
||||
# BUZZ_WEB_DIR=./web/dist
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Git (NIP-34 bare repositories)
|
||||
@@ -57,7 +57,7 @@ RELAY_URL=ws://localhost:3000
|
||||
# Root directory for bare git repos. Repos are stored at
|
||||
# {path}/{owner_hex}/{repo_id}.git/. Default: ./repos (relative to CWD).
|
||||
# Set an absolute path to keep repos stable across worktrees.
|
||||
# SPROUT_GIT_REPO_PATH=./repos
|
||||
# BUZZ_GIT_REPO_PATH=./repos
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ephemeral Channels (TTL testing)
|
||||
@@ -65,125 +65,125 @@ RELAY_URL=ws://localhost:3000
|
||||
# Override the TTL for all ephemeral channels (in seconds). When set, any
|
||||
# channel created with a TTL tag will use this value instead of the
|
||||
# client-provided one. Unset to use the client-provided TTL.
|
||||
# SPROUT_EPHEMERAL_TTL_OVERRIDE=60
|
||||
# BUZZ_EPHEMERAL_TTL_OVERRIDE=60
|
||||
|
||||
# How often the reaper checks for expired ephemeral channels (default: 60s).
|
||||
# SPROUT_REAPER_INTERVAL_SECS=5
|
||||
# BUZZ_REAPER_INTERVAL_SECS=5
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging / Tracing
|
||||
# -----------------------------------------------------------------------------
|
||||
RUST_LOG=sprout_relay=debug,sprout_db=debug,sprout_auth=debug,sprout_pubsub=debug,tower_http=debug
|
||||
RUST_LOG=buzz_relay=debug,buzz_db=debug,buzz_auth=debug,buzz_pubsub=debug,tower_http=debug
|
||||
|
||||
# OTLP tracing endpoint (optional — leave unset to disable)
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ACP (Agent Communication Protocol — sprout-acp harness)
|
||||
# ACP (Agent Communication Protocol — buzz-acp harness)
|
||||
# -----------------------------------------------------------------------------
|
||||
# The ACP harness bridges Sprout events to AI agents. Each env var below maps
|
||||
# The ACP harness bridges Buzz events to AI agents. Each env var below maps
|
||||
# to a CLI flag of the same name (lowercase, hyphens → underscores). All values
|
||||
# are optional unless noted; defaults are shown in comments.
|
||||
#
|
||||
# Quick start:
|
||||
# SPROUT_PRIVATE_KEY=<hex> SPROUT_RELAY_URL=ws://localhost:3000 sprout-acp
|
||||
# BUZZ_PRIVATE_KEY=<hex> BUZZ_RELAY_URL=ws://localhost:3000 buzz-acp
|
||||
|
||||
# ── Identity & auth ──────────────────────────────────────────────────────────
|
||||
# Nostr private key (hex or bech32). REQUIRED — identifies the agent on the relay.
|
||||
# SPROUT_PRIVATE_KEY=<32-byte hex or nsec1… private key>
|
||||
# BUZZ_PRIVATE_KEY=<32-byte hex or nsec1… private key>
|
||||
|
||||
# Relay WebSocket URL the harness connects to.
|
||||
# Note: the relay itself uses RELAY_URL (above); this is the ACP harness's
|
||||
# connection target — they happen to point at the same place in local dev.
|
||||
# SPROUT_RELAY_URL=ws://localhost:3000
|
||||
# BUZZ_RELAY_URL=ws://localhost:3000
|
||||
|
||||
# ── Agent subprocess ─────────────────────────────────────────────────────────
|
||||
# Binary to spawn as the AI agent (e.g. "goose", "codex-acp", "claude-code").
|
||||
# SPROUT_ACP_AGENT_COMMAND=goose
|
||||
# BUZZ_ACP_AGENT_COMMAND=goose
|
||||
|
||||
# Comma-separated arguments passed to the agent binary.
|
||||
# Goose default: "acp". Codex/Claude default: "" (empty).
|
||||
# SPROUT_ACP_AGENT_ARGS=acp
|
||||
# BUZZ_ACP_AGENT_ARGS=acp
|
||||
|
||||
# Binary for an optional MCP server sidecar (e.g. sprout-dev-mcp for sprout-agent).
|
||||
# SPROUT_ACP_MCP_COMMAND=
|
||||
# Binary for an optional MCP server sidecar (e.g. buzz-dev-mcp for buzz-agent).
|
||||
# BUZZ_ACP_MCP_COMMAND=
|
||||
|
||||
# Number of parallel agent subprocesses (1–32).
|
||||
# SPROUT_ACP_AGENTS=1
|
||||
# BUZZ_ACP_AGENTS=1
|
||||
|
||||
# Desired LLM model ID. Applied to every new ACP session.
|
||||
# Use `sprout-acp models` to discover available model IDs.
|
||||
# SPROUT_ACP_MODEL=
|
||||
# Use `buzz-acp models` to discover available model IDs.
|
||||
# BUZZ_ACP_MODEL=
|
||||
|
||||
# ── Timeouts & sessions ──────────────────────────────────────────────────────
|
||||
# Max seconds per agent turn before timeout (default 320 = ~5 min).
|
||||
# SPROUT_ACP_TURN_TIMEOUT=320
|
||||
# BUZZ_ACP_TURN_TIMEOUT=320
|
||||
|
||||
# Max turns per session before proactive rotation. 0 = disabled (rotate only
|
||||
# on MaxTokens / MaxTurnRequests). Recommended: 50 for long-running agents.
|
||||
# SPROUT_ACP_MAX_TURNS_PER_SESSION=0
|
||||
# BUZZ_ACP_MAX_TURNS_PER_SESSION=0
|
||||
|
||||
# ── Prompts ──────────────────────────────────────────────────────────────────
|
||||
# System prompt injected into every agent session (inline text).
|
||||
# SPROUT_ACP_SYSTEM_PROMPT=
|
||||
# BUZZ_ACP_SYSTEM_PROMPT=
|
||||
|
||||
# Path to a file containing the system prompt (mutually exclusive with above).
|
||||
# SPROUT_ACP_SYSTEM_PROMPT_FILE=
|
||||
# BUZZ_ACP_SYSTEM_PROMPT_FILE=
|
||||
|
||||
# Message sent to the agent immediately after session creation.
|
||||
# SPROUT_ACP_INITIAL_MESSAGE=
|
||||
# BUZZ_ACP_INITIAL_MESSAGE=
|
||||
|
||||
# ── Heartbeat ────────────────────────────────────────────────────────────────
|
||||
# Seconds between heartbeat prompts. 0 = disabled. Must be 0 or ≥10.
|
||||
# Recommended: 60 for long-running agents to prevent idle session timeouts.
|
||||
# SPROUT_ACP_HEARTBEAT_INTERVAL=0
|
||||
# BUZZ_ACP_HEARTBEAT_INTERVAL=0
|
||||
|
||||
# Heartbeat prompt text (inline). Mutually exclusive with file variant.
|
||||
# SPROUT_ACP_HEARTBEAT_PROMPT=
|
||||
# BUZZ_ACP_HEARTBEAT_PROMPT=
|
||||
|
||||
# Path to a file containing the heartbeat prompt.
|
||||
# SPROUT_ACP_HEARTBEAT_PROMPT_FILE=
|
||||
# BUZZ_ACP_HEARTBEAT_PROMPT_FILE=
|
||||
|
||||
# ── Subscription & filtering ─────────────────────────────────────────────────
|
||||
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
|
||||
# SPROUT_ACP_SUBSCRIBE=mentions
|
||||
# BUZZ_ACP_SUBSCRIBE=mentions
|
||||
|
||||
# Comma-separated event kind numbers to subscribe to (overrides mode defaults).
|
||||
# SPROUT_ACP_KINDS=
|
||||
# BUZZ_ACP_KINDS=
|
||||
|
||||
# Comma-separated channel UUIDs to limit subscription scope.
|
||||
# SPROUT_ACP_CHANNELS=
|
||||
# BUZZ_ACP_CHANNELS=
|
||||
|
||||
# Set to true to disable the @-mention filter in mentions mode.
|
||||
# SPROUT_ACP_NO_MENTION_FILTER=false
|
||||
# BUZZ_ACP_NO_MENTION_FILTER=false
|
||||
|
||||
# Path to TOML config file for rule-based subscriptions (config mode).
|
||||
# SPROUT_ACP_CONFIG=./sprout-acp.toml
|
||||
# BUZZ_ACP_CONFIG=./buzz-acp.toml
|
||||
|
||||
# ── Dedup & self-ignore ──────────────────────────────────────────────────────
|
||||
# How to handle duplicate events: "queue" (default) or "drop".
|
||||
# SPROUT_ACP_DEDUP=queue
|
||||
# BUZZ_ACP_DEDUP=queue
|
||||
|
||||
# Set to true to process the agent's own messages (default: ignore self).
|
||||
# SPROUT_ACP_NO_IGNORE_SELF=false
|
||||
# BUZZ_ACP_NO_IGNORE_SELF=false
|
||||
|
||||
# ── Context ──────────────────────────────────────────────────────────────────
|
||||
# Max context messages fetched for thread replies and DMs (0–100). 0 = disabled.
|
||||
# SPROUT_ACP_CONTEXT_MESSAGE_LIMIT=12
|
||||
# BUZZ_ACP_CONTEXT_MESSAGE_LIMIT=12
|
||||
|
||||
# ── Presence & typing ────────────────────────────────────────────────────────
|
||||
# Set to true to disable automatic online/offline presence status.
|
||||
# SPROUT_ACP_NO_PRESENCE=false
|
||||
# BUZZ_ACP_NO_PRESENCE=false
|
||||
|
||||
# Set to true to disable typing indicators while the agent is processing.
|
||||
# SPROUT_ACP_NO_TYPING=false
|
||||
# BUZZ_ACP_NO_TYPING=false
|
||||
|
||||
# ── Advanced tuning ──────────────────────────────────────────────────────────
|
||||
# Event channel buffer capacity (WebSocket → harness). Increase for
|
||||
# high-throughput agents. Minimum 1.
|
||||
# SPROUT_ACP_EVENT_BUFFER=256
|
||||
# BUZZ_ACP_EVENT_BUFFER=256
|
||||
|
||||
# ── Legacy aliases ───────────────────────────────────────────────────────────
|
||||
# These are accepted for backward compatibility but the canonical names above
|
||||
# are preferred:
|
||||
# SPROUT_ACP_PRIVATE_KEY → SPROUT_PRIVATE_KEY
|
||||
# BUZZ_ACP_PRIVATE_KEY → BUZZ_PRIVATE_KEY
|
||||
|
||||
28
.github/workflows/release.yml
vendored
@@ -169,28 +169,28 @@ jobs:
|
||||
EXTRACT_DIR="${RUNNER_TEMP}/signed-app-extract"
|
||||
rm -rf "$EXTRACT_DIR" && mkdir -p "$EXTRACT_DIR"
|
||||
ditto -x -k "$SIGNED_APP_ZIP" "$EXTRACT_DIR"
|
||||
rm -rf "${APP_DIR}/Sprout.app"
|
||||
cp -R "${EXTRACT_DIR}/Sprout.app" "${APP_DIR}/Sprout.app"
|
||||
rm -rf "${APP_DIR}/Buzz.app"
|
||||
cp -R "${EXTRACT_DIR}/Buzz.app" "${APP_DIR}/Buzz.app"
|
||||
|
||||
# Rebuild the updater archive from the signed .app and re-sign it with the Tauri updater key.
|
||||
rm -f "${APP_DIR}/Sprout.app.tar.gz" "${APP_DIR}/Sprout.app.tar.gz.sig"
|
||||
(cd "$APP_DIR" && tar -czf Sprout.app.tar.gz Sprout.app)
|
||||
TARBALL_ABS="$(pwd)/${APP_DIR}/Sprout.app.tar.gz"
|
||||
rm -f "${APP_DIR}/Buzz.app.tar.gz" "${APP_DIR}/Buzz.app.tar.gz.sig"
|
||||
(cd "$APP_DIR" && tar -czf Buzz.app.tar.gz Buzz.app)
|
||||
TARBALL_ABS="$(pwd)/${APP_DIR}/Buzz.app.tar.gz"
|
||||
(cd desktop && pnpm tauri signer sign "$TARBALL_ABS")
|
||||
|
||||
- name: Verify code signature
|
||||
run: |
|
||||
codesign --verify --deep --strict --verbose=2 \
|
||||
desktop/src-tauri/target/release/bundle/macos/Sprout.app
|
||||
desktop/src-tauri/target/release/bundle/macos/Buzz.app
|
||||
spctl --assess --type execute --verbose=4 \
|
||||
desktop/src-tauri/target/release/bundle/macos/Sprout.app
|
||||
desktop/src-tauri/target/release/bundle/macos/Buzz.app
|
||||
|
||||
- name: Locate build artifacts
|
||||
id: artifacts
|
||||
run: |
|
||||
BUNDLE_DIR="desktop/src-tauri/target/release/bundle"
|
||||
|
||||
# Find the DMG (Tauri names it Sprout_<version>_<arch>.dmg)
|
||||
# Find the DMG (Tauri names it Buzz_<version>_<arch>.dmg)
|
||||
DMG=$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -type f | head -1)
|
||||
if [[ -z "$DMG" ]]; then
|
||||
echo "::error::No DMG found in $BUNDLE_DIR/dmg"
|
||||
@@ -234,11 +234,11 @@ jobs:
|
||||
NOTES=$(awk "/^## v${VERSION}$/,/^## v/" CHANGELOG.md | sed '$d')
|
||||
fi
|
||||
if [[ -z "$NOTES" ]]; then
|
||||
NOTES="Sprout Desktop v${VERSION}"
|
||||
NOTES="Buzz Desktop v${VERSION}"
|
||||
fi
|
||||
gh release create "v${VERSION}" \
|
||||
--target "$RELEASE_SHA" \
|
||||
--title "Sprout Desktop v${VERSION}" \
|
||||
--title "Buzz Desktop v${VERSION}" \
|
||||
--notes "$NOTES" \
|
||||
"$DMG_PATH"
|
||||
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
run: |
|
||||
gh release create buzz-desktop-latest \
|
||||
--prerelease \
|
||||
--title "Sprout Desktop Auto-Update" \
|
||||
--title "Buzz 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 buzz-desktop-latest \
|
||||
@@ -349,12 +349,12 @@ jobs:
|
||||
EXTRACT_DIR="${RUNNER_TEMP}/signed-app-extract-x64"
|
||||
rm -rf "$EXTRACT_DIR" && mkdir -p "$EXTRACT_DIR"
|
||||
ditto -x -k "$SIGNED_APP_ZIP" "$EXTRACT_DIR"
|
||||
rm -rf "${APP_DIR}/Sprout.app"
|
||||
cp -R "${EXTRACT_DIR}/Sprout.app" "${APP_DIR}/Sprout.app"
|
||||
rm -rf "${APP_DIR}/Buzz.app"
|
||||
cp -R "${EXTRACT_DIR}/Buzz.app" "${APP_DIR}/Buzz.app"
|
||||
|
||||
- name: Verify code signature
|
||||
run: |
|
||||
APP_DIR="desktop/src-tauri/target/${TARGET}/release/bundle/macos/Sprout.app"
|
||||
APP_DIR="desktop/src-tauri/target/${TARGET}/release/bundle/macos/Buzz.app"
|
||||
codesign --verify --deep --strict --verbose=2 "$APP_DIR"
|
||||
spctl --assess --type execute --verbose=4 "$APP_DIR"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Releasing Sprout Desktop
|
||||
# Releasing Buzz Desktop
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sprout
|
||||
# Buzz
|
||||
|
||||
Desktop chat shell with:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/sprout.svg?v=20260310" />
|
||||
<link rel="icon" type="image/svg+xml" href="/buzz.svg?v=20260610" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "sprout",
|
||||
"name": "buzz",
|
||||
"private": true,
|
||||
"version": "0.3.16",
|
||||
"type": "module",
|
||||
|
||||
10
desktop/public/buzz.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Buzz">
|
||||
<rect width="512" height="512" rx="96" fill="#0b0b0b"/>
|
||||
<path d="M256 75 334 120v90l-78 45-78-45v-90z" fill="#ffd84d"/>
|
||||
<path d="M178 210 256 255v90l-78 45-78-45v-90z" fill="#ffbd24"/>
|
||||
<path d="M334 210 412 255v90l-78 45-78-45v-90z" fill="#ffb000"/>
|
||||
<path d="M256 300 334 345v90l-78 45-78-45v-90z" fill="#ff8a00"/>
|
||||
<path d="M256 105 306 134l-50 29-50-29z" fill="#ffe680" opacity=".75"/>
|
||||
<path d="M178 240 228 269l-50 29-50-29z" fill="#ffe680" opacity=".55"/>
|
||||
<path d="M334 240 384 269l-50 29-50-29z" fill="#ffe680" opacity=".55"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 646 B |
@@ -1,9 +0,0 @@
|
||||
<svg width="360" height="360" viewBox="0 0 360 360" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(32.4 32.4) scale(0.82)">
|
||||
<path d="M180 160V170H170V160H180Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M270 0C319.706 0 360 40.2944 360 90V270C360 319.706 319.706 360 270 360H200V350H190V290H200V280H210V270H260V260H270V250H280V240H290V230H300V220H310V210H330V200H310V190H240V200H220V210H210V220H200V240H220V230H230V220H250V210H270V220H250V230H230V240H220V250H210V260H200V270H190V200H180V260H170V250H160V240H140V230H160V240H170V220H160V210H150V200H140V190H120V180H50V190H30V200H40V210H50V220H60V230H70V240H80V250H90V260H160V280H170V360H90C40.2944 360 0 319.706 0 270V90C0 40.2944 40.2944 0 90 0H270ZM150 110V130H160V150H150V180H160V190H170V200H180V170H190V200H200V190H210V170H200V150H190V140H180V130H170V120H160V110H150Z" fill="black"/>
|
||||
<path d="M140 220V230H130V220H140Z" fill="black"/>
|
||||
<path d="M130 210V220H110V210H130Z" fill="black"/>
|
||||
<path d="M110 200V210H90V200H110Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -12,7 +12,7 @@ import { resolve } from "node:path";
|
||||
// 2. bundle.createUpdaterArtifacts = true so Tauri produces the .tar.gz
|
||||
// archive and .sig signature during the build.
|
||||
// 3. plugins.updater with the public key and endpoint from env vars.
|
||||
// Both SPROUT_UPDATER_PUBLIC_KEY and SPROUT_UPDATER_ENDPOINT are required -
|
||||
// Both BUZZ_UPDATER_PUBLIC_KEY and BUZZ_UPDATER_ENDPOINT are required -
|
||||
// the script fails if either is missing (OSS builds always ship with updater).
|
||||
//
|
||||
// Apple code signing and notarization happen post-build via
|
||||
@@ -24,12 +24,12 @@ const outputConfigPath = resolve(
|
||||
"src-tauri/tauri.release.conf.json",
|
||||
);
|
||||
|
||||
const updaterPubkey = process.env.SPROUT_UPDATER_PUBLIC_KEY;
|
||||
const updaterEndpoint = process.env.SPROUT_UPDATER_ENDPOINT;
|
||||
const updaterPubkey = process.env.BUZZ_UPDATER_PUBLIC_KEY;
|
||||
const updaterEndpoint = process.env.BUZZ_UPDATER_ENDPOINT;
|
||||
|
||||
const missing = [];
|
||||
if (!updaterPubkey) missing.push("SPROUT_UPDATER_PUBLIC_KEY");
|
||||
if (!updaterEndpoint) missing.push("SPROUT_UPDATER_ENDPOINT");
|
||||
if (!updaterPubkey) missing.push("BUZZ_UPDATER_PUBLIC_KEY");
|
||||
if (!updaterEndpoint) missing.push("BUZZ_UPDATER_ENDPOINT");
|
||||
if (missing.length > 0) {
|
||||
console.error(
|
||||
`Error: required environment variable(s) missing: ${missing.join(", ")}`,
|
||||
|
||||
@@ -14,7 +14,7 @@ ARCHIVE_URL="$3"
|
||||
# only. Supporting Intel Macs (darwin-x86_64) would require a matrix build.
|
||||
jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "Sprout v$VERSION" \
|
||||
--arg notes "Buzz v$VERSION" \
|
||||
--arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg signature "$(cat "$SIG_FILE")" \
|
||||
--arg url "$ARCHIVE_URL" \
|
||||
|
||||
124
desktop/src-tauri/Cargo.lock
generated
@@ -844,6 +844,68 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buzz-desktop"
|
||||
version = "0.3.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atomic-write-file",
|
||||
"audioadapter-buffers",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"buzz-core",
|
||||
"buzz-persona",
|
||||
"buzz-sdk",
|
||||
"bzip2 0.6.1",
|
||||
"chrono",
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"earshot",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"infer",
|
||||
"libc",
|
||||
"mesh-llm-host-runtime",
|
||||
"mesh-llm-sdk",
|
||||
"neteq",
|
||||
"nostr",
|
||||
"objc2-app-kit",
|
||||
"opus",
|
||||
"png 0.18.1",
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"rodio",
|
||||
"rubato",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2 0.11.0",
|
||||
"sherpa-onnx",
|
||||
"strip-ansi-escapes",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-websocket",
|
||||
"tauri-plugin-window-state",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.29.0",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"zeroize",
|
||||
"zip 8.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buzz-persona"
|
||||
version = "0.1.0"
|
||||
@@ -8220,68 +8282,6 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sprout-desktop"
|
||||
version = "0.3.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atomic-write-file",
|
||||
"audioadapter-buffers",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"buzz-core",
|
||||
"buzz-persona",
|
||||
"buzz-sdk",
|
||||
"bzip2 0.6.1",
|
||||
"chrono",
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"earshot",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"infer",
|
||||
"libc",
|
||||
"mesh-llm-host-runtime",
|
||||
"mesh-llm-sdk",
|
||||
"neteq",
|
||||
"nostr",
|
||||
"objc2-app-kit",
|
||||
"opus",
|
||||
"png 0.18.1",
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"rodio",
|
||||
"rubato",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2 0.11.0",
|
||||
"sherpa-onnx",
|
||||
"strip-ansi-escapes",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-websocket",
|
||||
"tauri-plugin-window-state",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.29.0",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"zeroize",
|
||||
"zip 8.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-stream"
|
||||
version = "0.2.3"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "sprout-desktop"
|
||||
name = "buzz-desktop"
|
||||
version = "0.3.16"
|
||||
description = "Sprout desktop app"
|
||||
description = "Buzz desktop app"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -13,7 +13,7 @@ edition = "2021"
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "sprout_lib"
|
||||
name = "buzz_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
@@ -61,9 +61,9 @@ nostr = { version = "0.44", features = ["nip44"] }
|
||||
zeroize = "1"
|
||||
reqwest = { version = "0.13", features = ["json", "query", "stream"] }
|
||||
url = "2"
|
||||
sprout-core = { package = "buzz-core", path = "../../crates/buzz-core" }
|
||||
sprout-persona = { package = "buzz-persona", path = "../../crates/buzz-persona" }
|
||||
sprout-sdk = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
|
||||
buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" }
|
||||
buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" }
|
||||
buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
|
||||
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "ebc3ab4c4b5f4a45d5bc942c66c18583800c3f82", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
|
||||
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "ebc3ab4c4b5f4a45d5bc942c66c18583800c3f82", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
|
||||
base64 = "0.22"
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sprout</string>
|
||||
<string>Buzz</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Sprout</string>
|
||||
<string>Buzz</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Sprout needs microphone access for voice huddles.</string>
|
||||
<string>Buzz needs microphone access for voice huddles.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=BUZZ_RELAY_URL");
|
||||
println!("cargo:rerun-if-env-changed=BUZZ_RELAY_HTTP");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_PUBLIC_KEY");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_ENDPOINT");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_BUILD_DATABRICKS_HOST");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_BUILD_DATABRICKS_MODEL");
|
||||
println!("cargo:rustc-check-cfg=cfg(sprout_updater_enabled)");
|
||||
println!("cargo:rerun-if-env-changed=BUZZ_UPDATER_PUBLIC_KEY");
|
||||
println!("cargo:rerun-if-env-changed=BUZZ_UPDATER_ENDPOINT");
|
||||
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DATABRICKS_HOST");
|
||||
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DATABRICKS_MODEL");
|
||||
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");
|
||||
|
||||
if let Ok(relay_url) = std::env::var("BUZZ_RELAY_URL") {
|
||||
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_URL={relay_url}");
|
||||
@@ -15,25 +15,25 @@ fn main() {
|
||||
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_HTTP={relay_http}");
|
||||
}
|
||||
|
||||
if let Ok(host) = std::env::var("SPROUT_BUILD_DATABRICKS_HOST") {
|
||||
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_DATABRICKS_HOST={host}");
|
||||
if let Ok(host) = std::env::var("BUZZ_BUILD_DATABRICKS_HOST") {
|
||||
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_DATABRICKS_HOST={host}");
|
||||
}
|
||||
|
||||
if let Ok(model) = std::env::var("SPROUT_BUILD_DATABRICKS_MODEL") {
|
||||
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_DATABRICKS_MODEL={model}");
|
||||
if let Ok(model) = std::env::var("BUZZ_BUILD_DATABRICKS_MODEL") {
|
||||
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_DATABRICKS_MODEL={model}");
|
||||
}
|
||||
|
||||
let updater_public_key = std::env::var("SPROUT_UPDATER_PUBLIC_KEY")
|
||||
let updater_public_key = std::env::var("BUZZ_UPDATER_PUBLIC_KEY")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let updater_endpoint = std::env::var("SPROUT_UPDATER_ENDPOINT")
|
||||
let updater_endpoint = std::env::var("BUZZ_UPDATER_ENDPOINT")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if updater_public_key.is_some() && updater_endpoint.is_some() {
|
||||
println!("cargo:rustc-cfg=sprout_updater_enabled");
|
||||
println!("cargo:rustc-cfg=buzz_updater_enabled");
|
||||
}
|
||||
|
||||
tauri_build::build()
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
<color name="ic_launcher_background">#050506</color>
|
||||
</resources>
|
||||
BIN
desktop/src-tauri/icons/buzz-source.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 648 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
@@ -1,18 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
|
||||
<defs>
|
||||
<clipPath id="icon-clip">
|
||||
<rect x="132" y="132" width="760" height="760" rx="171" ry="171" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#icon-clip)">
|
||||
<rect x="132" y="132" width="760" height="760" rx="171" ry="171" fill="#F4F0E6" />
|
||||
<image
|
||||
href="./sprout-art.png"
|
||||
x="156"
|
||||
y="156"
|
||||
width="712"
|
||||
height="712"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 506 B |
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -35,7 +35,7 @@ pub struct AppState {
|
||||
pub media_proxy_port: AtomicU16,
|
||||
/// IOKit power assertion state — prevents idle sleep while agents run.
|
||||
pub prevent_sleep: Arc<Mutex<crate::prevent_sleep::PreventSleepState>>,
|
||||
/// In-process mesh-llm node started by Sprout Desktop.
|
||||
/// In-process mesh-llm node started by Buzz Desktop.
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
pub mesh_llm_runtime: AsyncMutex<Option<crate::mesh_llm::DesktopMeshRuntime>>,
|
||||
/// Runtime-owned relay-mesh control plane (call-me-now listener + connect
|
||||
@@ -48,16 +48,16 @@ pub struct AppState {
|
||||
pub fn build_app_state() -> AppState {
|
||||
// Env var takes precedence (dev/CI). If absent, resolve_persisted_identity()
|
||||
// in setup() will replace the ephemeral placeholder with a persisted key.
|
||||
let (keys, source) = match std::env::var("SPROUT_PRIVATE_KEY") {
|
||||
let (keys, source) = match std::env::var("BUZZ_PRIVATE_KEY") {
|
||||
Ok(nsec) => match Keys::parse(nsec.trim()) {
|
||||
Ok(keys) => (keys, "configured"),
|
||||
Err(error) => {
|
||||
eprintln!("sprout-desktop: invalid SPROUT_PRIVATE_KEY: {error}");
|
||||
eprintln!("buzz-desktop: invalid BUZZ_PRIVATE_KEY: {error}");
|
||||
(Keys::generate(), "ephemeral")
|
||||
}
|
||||
},
|
||||
Err(std::env::VarError::NotUnicode(_)) => {
|
||||
eprintln!("sprout-desktop: SPROUT_PRIVATE_KEY contains invalid UTF-8");
|
||||
eprintln!("buzz-desktop: BUZZ_PRIVATE_KEY contains invalid UTF-8");
|
||||
(Keys::generate(), "ephemeral")
|
||||
}
|
||||
Err(std::env::VarError::NotPresent) => (Keys::generate(), "ephemeral"),
|
||||
@@ -65,7 +65,7 @@ pub fn build_app_state() -> AppState {
|
||||
|
||||
if source == "configured" {
|
||||
eprintln!(
|
||||
"sprout-desktop: configured identity pubkey {}",
|
||||
"buzz-desktop: configured identity pubkey {}",
|
||||
keys.public_key().to_hex()
|
||||
);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ impl AppState {
|
||||
|
||||
/// Resolve the user's identity key from the app data directory.
|
||||
///
|
||||
/// Priority: `SPROUT_PRIVATE_KEY` env var (already handled in `build_app_state`)
|
||||
/// Priority: `BUZZ_PRIVATE_KEY` env var (already handled in `build_app_state`)
|
||||
/// → `{app_data_dir}/identity.key` file → generate + save.
|
||||
///
|
||||
/// Writes use `atomic-write-file` which handles temp file creation, fsync,
|
||||
@@ -135,7 +135,7 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<(
|
||||
// Only skip file-based resolution if the env var was present AND parsed
|
||||
// successfully. A malformed env var should fall through to the persisted
|
||||
// key rather than leaving the app on an ephemeral identity.
|
||||
if let Ok(nsec) = std::env::var("SPROUT_PRIVATE_KEY") {
|
||||
if let Ok(nsec) = std::env::var("BUZZ_PRIVATE_KEY") {
|
||||
if Keys::parse(nsec.trim()).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -153,7 +153,7 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<(
|
||||
match load_key_file(&key_path) {
|
||||
Ok(keys) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: persisted identity pubkey {}",
|
||||
"buzz-desktop: persisted identity pubkey {}",
|
||||
keys.public_key().to_hex()
|
||||
);
|
||||
*state.keys.lock().map_err(|e| e.to_string())? = keys;
|
||||
@@ -168,7 +168,7 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<(
|
||||
.unwrap_or(0);
|
||||
let bad_name = format!("identity.key.bad.{ts}");
|
||||
eprintln!(
|
||||
"sprout-desktop: corrupt identity.key ({error}), quarantining to {bad_name}"
|
||||
"buzz-desktop: corrupt identity.key ({error}), quarantining to {bad_name}"
|
||||
);
|
||||
let bad_path = data_dir.join(bad_name);
|
||||
if std::fs::rename(&key_path, &bad_path).is_err() {
|
||||
@@ -183,7 +183,7 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<(
|
||||
save_key_file(&key_path, &keys)?;
|
||||
|
||||
eprintln!(
|
||||
"sprout-desktop: generated and saved identity pubkey {}",
|
||||
"buzz-desktop: generated and saved identity pubkey {}",
|
||||
keys.public_key().to_hex()
|
||||
);
|
||||
*state.keys.lock().map_err(|e| e.to_string())? = keys;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
util::now_iso,
|
||||
};
|
||||
|
||||
/// Query available models from an agent via `sprout-acp models --json`.
|
||||
/// Query available models from an agent via `buzz-acp models --json`.
|
||||
///
|
||||
/// Spawns a short-lived subprocess (no relay connection needed). The subprocess
|
||||
/// starts the agent, queries its model catalog, and exits. ~2-5s total.
|
||||
@@ -92,14 +92,14 @@ pub async fn get_agent_models(
|
||||
}
|
||||
}
|
||||
}
|
||||
// User env layering — written LAST so it overrides any Sprout-set env above.
|
||||
// User env layering — written LAST so it overrides any Buzz-set env above.
|
||||
for (k, v) in &merged_env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
cmd.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
.map_err(|e| format!("failed to spawn sprout-acp models: {e}"))
|
||||
.map_err(|e| format!("failed to spawn buzz-acp models: {e}"))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("model discovery task failed: {e}"))?
|
||||
@@ -113,7 +113,7 @@ pub async fn get_agent_models(
|
||||
let stderr_redacted =
|
||||
crate::managed_agents::redact_env_values_in(stderr.as_ref(), &env_for_redaction);
|
||||
return Err(format!(
|
||||
"sprout-acp models failed (exit {}): {stderr_redacted}",
|
||||
"buzz-acp models failed (exit {}): {stderr_redacted}",
|
||||
output.status.code().unwrap_or(-1)
|
||||
));
|
||||
}
|
||||
@@ -270,7 +270,7 @@ pub async fn update_managed_agent(
|
||||
{
|
||||
Ok(()) => None,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: relay profile sync failed after rename: {e}");
|
||||
eprintln!("buzz-desktop: relay profile sync failed after rename: {e}");
|
||||
Some(e)
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ pub async fn update_managed_agent(
|
||||
|
||||
// ── Model normalization ───────────────────────────────────────────────────────
|
||||
|
||||
/// Normalize raw `sprout-acp models --json` output into a typed DTO for the frontend.
|
||||
/// Normalize raw `buzz-acp models --json` output into a typed DTO for the frontend.
|
||||
///
|
||||
/// Merges models from both ACP paths (stable configOptions + unstable SessionModelState),
|
||||
/// deduplicates by ID (stable takes precedence), and returns a unified list.
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
};
|
||||
|
||||
/// Read the workspace owner's pubkey hex from app state without holding the
|
||||
/// lock for longer than necessary. Used to populate `SPROUT_ACP_AGENT_OWNER`
|
||||
/// lock for longer than necessary. Used to populate `BUZZ_ACP_AGENT_OWNER`
|
||||
/// as a fallback for legacy agent records that have no NIP-OA `auth_tag`.
|
||||
fn workspace_owner_hex(state: &AppState) -> Result<String, String> {
|
||||
let keys = state.keys.lock().map_err(|e| e.to_string())?;
|
||||
@@ -313,7 +313,7 @@ pub async fn create_managed_agent(
|
||||
crate::managed_agents::validate_user_env_keys(&input.env_vars)?;
|
||||
|
||||
// Validate & normalize the respond-to allowlist BEFORE any side effects.
|
||||
// The harness has its own validator (sprout-acp/src/config.rs) but we want
|
||||
// The harness has its own validator (buzz-acp/src/config.rs) but we want
|
||||
// to catch malformed input at the boundary so the agent never tries to
|
||||
// start with a list that will crash it on launch.
|
||||
let respond_to_allowlist =
|
||||
@@ -384,12 +384,12 @@ pub async fn create_managed_agent(
|
||||
// No tokens are minted. Fail closed: bad auth tag → don't create agent.
|
||||
let auth_tag = {
|
||||
let owner_keys = state.keys.lock().map_err(|e| e.to_string())?;
|
||||
// Bridge nostr 0.37 → 0.36 (sprout-sdk) via hex round-trip.
|
||||
// Bridge nostr 0.37 → 0.36 (buzz-sdk) via hex round-trip.
|
||||
let compat_owner = nostr::Keys::parse(&owner_keys.secret_key().to_secret_hex())
|
||||
.map_err(|e| format!("failed to bridge owner keys: {e}"))?;
|
||||
let compat_agent = nostr::PublicKey::from_hex(&agent_keys.public_key().to_hex())
|
||||
.map_err(|e| format!("failed to bridge agent pubkey: {e}"))?;
|
||||
let tag = sprout_sdk::nip_oa::compute_auth_tag(&compat_owner, &compat_agent, "")
|
||||
let tag = buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &compat_agent, "")
|
||||
.map_err(|e| format!("failed to compute NIP-OA auth tag: {e}"))?;
|
||||
Some(tag)
|
||||
};
|
||||
@@ -647,7 +647,7 @@ pub async fn create_managed_agent(
|
||||
if let Err(persist_err) = persist_create_deploy_error(&app, &state, &pubkey, &e)
|
||||
{
|
||||
eprintln!(
|
||||
"sprout-desktop: failed to persist deploy-prep error for {pubkey}: {persist_err}"
|
||||
"buzz-desktop: failed to persist deploy-prep error for {pubkey}: {persist_err}"
|
||||
);
|
||||
}
|
||||
Some(e)
|
||||
@@ -834,7 +834,7 @@ pub async fn start_managed_agent(
|
||||
.await
|
||||
{
|
||||
eprintln!(
|
||||
"sprout-desktop: profile reconciliation failed for agent {reconcile_pubkey}: {e}"
|
||||
"buzz-desktop: profile reconciliation failed for agent {reconcile_pubkey}: {e}"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1103,7 +1103,7 @@ pub fn discover_backend_providers() -> Vec<BackendProviderInfo> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn probe_backend_provider(binary_path: String) -> Result<serde_json::Value, String> {
|
||||
// Validate that the requested path is actually a discovered sprout-backend-* binary.
|
||||
// Validate that the requested path is actually a discovered buzz-backend-* binary.
|
||||
// This prevents arbitrary binary execution via a compromised frontend or IPC.
|
||||
let candidates = discover_provider_candidates();
|
||||
let path = std::path::PathBuf::from(&binary_path);
|
||||
@@ -1115,7 +1115,7 @@ pub async fn probe_backend_provider(binary_path: String) -> Result<serde_json::V
|
||||
.any(|(_, p)| p.canonicalize().ok().as_ref() == Some(&canonical));
|
||||
if !is_known {
|
||||
return Err(format!(
|
||||
"binary '{binary_path}' is not a discovered sprout-backend-* provider"
|
||||
"binary '{binary_path}' is not a discovered buzz-backend-* provider"
|
||||
));
|
||||
}
|
||||
// request_id is for provider-side logging — not validated in the response
|
||||
|
||||
@@ -211,7 +211,7 @@ pub async fn get_channels(state: State<'_, AppState>) -> Result<Vec<ChannelInfo>
|
||||
let events = query_relay(
|
||||
&state,
|
||||
&[serde_json::json!({
|
||||
"kinds": [sprout_core::kind::KIND_DM_VISIBILITY],
|
||||
"kinds": [buzz_core_pkg::kind::KIND_DM_VISIBILITY],
|
||||
"#p": [&my_pubkey],
|
||||
"limit": 1,
|
||||
})],
|
||||
|
||||
@@ -22,8 +22,8 @@ use nostr::PublicKey;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
use sprout_core::engram::{self, extract_refs, select_head, validate_and_decrypt, Body};
|
||||
use sprout_core::kind::KIND_AGENT_ENGRAM;
|
||||
use buzz_core_pkg::engram::{self, extract_refs, select_head, validate_and_decrypt, Body};
|
||||
use buzz_core_pkg::kind::KIND_AGENT_ENGRAM;
|
||||
|
||||
use crate::{app_state::AppState, managed_agents::load_managed_agents, relay::query_relay};
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ pub fn get_default_relay_url() -> String {
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_shared_identity() -> bool {
|
||||
std::env::var("SPROUT_SHARE_IDENTITY")
|
||||
std::env::var("BUZZ_SHARE_IDENTITY")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false)
|
||||
&& std::env::var("SPROUT_PRIVATE_KEY")
|
||||
&& std::env::var("BUZZ_PRIVATE_KEY")
|
||||
.ok()
|
||||
.and_then(|k| Keys::parse(k.trim()).ok())
|
||||
.is_some()
|
||||
@@ -112,7 +112,7 @@ pub fn decrypt_observer_event(
|
||||
return Err("observer event has invalid signature".into());
|
||||
}
|
||||
|
||||
sprout_core::observer::decrypt_observer_payload(&keys, &event)
|
||||
buzz_core_pkg::observer::decrypt_observer_payload(&keys, &event)
|
||||
.map_err(|error| format!("decrypt observer event failed: {error}"))
|
||||
}
|
||||
|
||||
@@ -132,12 +132,13 @@ pub fn build_observer_control_event(
|
||||
let agent_pubkey = PublicKey::from_hex(agent_pubkey.trim())
|
||||
.map_err(|error| format!("invalid agent pubkey: {error}"))?;
|
||||
let agent_pubkey_hex = agent_pubkey.to_hex();
|
||||
let encrypted = sprout_core::observer::encrypt_observer_payload(&keys, &agent_pubkey, &payload)
|
||||
.map_err(|error| format!("encrypt observer control failed: {error}"))?;
|
||||
let builder = sprout_sdk::build_agent_observer_frame(
|
||||
let encrypted =
|
||||
buzz_core_pkg::observer::encrypt_observer_payload(&keys, &agent_pubkey, &payload)
|
||||
.map_err(|error| format!("encrypt observer control failed: {error}"))?;
|
||||
let builder = buzz_sdk_pkg::build_agent_observer_frame(
|
||||
&agent_pubkey_hex,
|
||||
&agent_pubkey_hex,
|
||||
sprout_core::observer::OBSERVER_FRAME_CONTROL,
|
||||
buzz_core_pkg::observer::OBSERVER_FRAME_CONTROL,
|
||||
&encrypted,
|
||||
)
|
||||
.map_err(|error| format!("build observer control failed: {error}"))?;
|
||||
@@ -187,7 +188,7 @@ pub fn import_identity(
|
||||
bech32
|
||||
};
|
||||
|
||||
eprintln!("sprout-desktop: imported identity pubkey {}", pubkey_hex);
|
||||
eprintln!("buzz-desktop: imported identity pubkey {}", pubkey_hex);
|
||||
|
||||
Ok(IdentityInfo {
|
||||
pubkey: pubkey_hex,
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
///
|
||||
/// Mirrors the verification the relay will do (per spec gotcha #3: the
|
||||
/// preimage subject is the *target* pubkey, not the request signer). The
|
||||
/// `sprout-sdk` lives on nostr 0.36; the desktop is on 0.37, so we bridge
|
||||
/// `buzz-sdk` lives on nostr 0.36; the desktop is on 0.37, so we bridge
|
||||
/// via hex round-trip exactly like `relay::build_profile_event` does.
|
||||
fn extract_oa_owner(target_kind0: &nostr::Event) -> Option<(String, [String; 4])> {
|
||||
let target_hex = target_kind0.pubkey.to_hex();
|
||||
@@ -39,7 +39,7 @@ fn extract_oa_owner(target_kind0: &nostr::Event) -> Option<(String, [String; 4])
|
||||
continue;
|
||||
}
|
||||
let json = serde_json::to_string(slice).ok()?;
|
||||
match sprout_sdk::nip_oa::verify_auth_tag(&json, &target_compat) {
|
||||
match buzz_sdk_pkg::nip_oa::verify_auth_tag(&json, &target_compat) {
|
||||
Ok(owner) => {
|
||||
let raw: [String; 4] = [
|
||||
slice[0].clone(),
|
||||
@@ -228,7 +228,7 @@ pub struct ArchivedIdentitiesSnapshot {
|
||||
/// fetch wired up (the sibling relay-signed kind:13534 membership list is
|
||||
/// consumed the same way). Author-filtering against NIP-11 `self` is the
|
||||
/// correct hardening for an untrusted/multi-relay client and is tracked as a
|
||||
/// follow-up — not a runtime gap on Sprout's relay.
|
||||
/// follow-up — not a runtime gap on Buzz's relay.
|
||||
#[tauri::command]
|
||||
pub async fn list_archived_identities(
|
||||
state: State<'_, AppState>,
|
||||
@@ -273,15 +273,16 @@ mod tests {
|
||||
|
||||
/// Build a fake `kind:0` with a valid NIP-OA auth tag for a fresh owner.
|
||||
fn kind0_with_auth(agent: &Keys, owner: &Keys) -> nostr::Event {
|
||||
// Compute auth tag via sprout-sdk (nostr 0.36) and bridge.
|
||||
// Compute auth tag via buzz-sdk (nostr 0.36) and bridge.
|
||||
let agent_hex = agent.public_key().to_hex();
|
||||
let agent_compat = nostr::PublicKey::from_hex(&agent_hex).unwrap();
|
||||
let owner_compat_secret =
|
||||
nostr::SecretKey::from_slice(owner.secret_key().as_secret_bytes()).unwrap();
|
||||
let owner_compat_keys = nostr::Keys::new(owner_compat_secret);
|
||||
let tag_json = sprout_sdk::nip_oa::compute_auth_tag(&owner_compat_keys, &agent_compat, "")
|
||||
.expect("compute_auth_tag");
|
||||
let compat_tag = sprout_sdk::nip_oa::parse_auth_tag(&tag_json).unwrap();
|
||||
let tag_json =
|
||||
buzz_sdk_pkg::nip_oa::compute_auth_tag(&owner_compat_keys, &agent_compat, "")
|
||||
.expect("compute_auth_tag");
|
||||
let compat_tag = buzz_sdk_pkg::nip_oa::parse_auth_tag(&tag_json).unwrap();
|
||||
let tag = Tag::parse(compat_tag.as_slice()).unwrap();
|
||||
EventBuilder::new(Kind::Metadata, "{}")
|
||||
.tags([tag])
|
||||
|
||||
@@ -173,7 +173,7 @@ fn sign_blossom_upload_auth(
|
||||
if let Some(domain) = extract_server_authority(base_url) {
|
||||
tags.push(Tag::parse(vec!["server".to_string(), domain]).map_err(|e| e.to_string())?);
|
||||
}
|
||||
EventBuilder::new(Kind::from(24242), "Upload sprout-media")
|
||||
EventBuilder::new(Kind::from(24242), "Upload buzz-media")
|
||||
.tags(tags)
|
||||
.sign_with_keys(keys)
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -321,7 +321,7 @@ async fn process_picked_path(
|
||||
if let Some(poster) = poster_bytes {
|
||||
match do_upload(poster, "image/jpeg", state).await {
|
||||
Ok(poster_desc) => descriptor.image = Some(poster_desc.url),
|
||||
Err(e) => eprintln!("sprout-desktop: poster upload failed (non-fatal): {e}"),
|
||||
Err(e) => eprintln!("buzz-desktop: poster upload failed (non-fatal): {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ pub async fn upload_media_bytes(
|
||||
// All blocking I/O runs off the async runtime via spawn_blocking.
|
||||
tokio::task::spawn_blocking(move || -> Result<(Vec<u8>, Option<Vec<u8>>), String> {
|
||||
let tmp_input =
|
||||
std::env::temp_dir().join(format!("sprout-drop-{}", uuid::Uuid::new_v4()));
|
||||
std::env::temp_dir().join(format!("buzz-drop-{}", uuid::Uuid::new_v4()));
|
||||
// Cleanup guard: remove temp file on ALL exit paths (including write failure).
|
||||
let result = (|| {
|
||||
std::fs::write(&tmp_input, &data)
|
||||
@@ -422,7 +422,7 @@ pub async fn upload_media_bytes(
|
||||
if let Some(poster) = poster_bytes {
|
||||
match do_upload(poster, "image/jpeg", &state).await {
|
||||
Ok(poster_desc) => descriptor.image = Some(poster_desc.url),
|
||||
Err(e) => eprintln!("sprout-desktop: poster upload failed (non-fatal): {e}"),
|
||||
Err(e) => eprintln!("buzz-desktop: poster upload failed (non-fatal): {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,8 +111,7 @@ pub(super) fn transcode_to_mp4(
|
||||
ffmpeg: &std::path::Path,
|
||||
) -> Result<std::path::PathBuf, String> {
|
||||
// UUID-based temp path — unique across concurrent uploads.
|
||||
let output =
|
||||
std::env::temp_dir().join(format!("sprout-transcode-{}.mp4", uuid::Uuid::new_v4()));
|
||||
let output = std::env::temp_dir().join(format!("buzz-transcode-{}.mp4", uuid::Uuid::new_v4()));
|
||||
|
||||
let result = run_ffmpeg_with_timeout(
|
||||
std::process::Command::new(ffmpeg)
|
||||
@@ -167,7 +166,7 @@ pub(super) fn extract_poster_frame(
|
||||
mp4_path: &std::path::Path,
|
||||
ffmpeg: &std::path::Path,
|
||||
) -> Result<std::path::PathBuf, String> {
|
||||
let output = std::env::temp_dir().join(format!("sprout-poster-{}.jpg", uuid::Uuid::new_v4()));
|
||||
let output = std::env::temp_dir().join(format!("buzz-poster-{}.jpg", uuid::Uuid::new_v4()));
|
||||
|
||||
// Poster extraction is a single-frame decode — 30s is generous.
|
||||
let poster_timeout = std::time::Duration::from_secs(30);
|
||||
@@ -194,7 +193,7 @@ pub(super) fn extract_poster_frame(
|
||||
{
|
||||
if !result.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
eprintln!("sprout-desktop: poster seek-to-1s failed, trying first frame: {stderr}");
|
||||
eprintln!("buzz-desktop: poster seek-to-1s failed, trying first frame: {stderr}");
|
||||
}
|
||||
let _ = std::fs::remove_file(&output);
|
||||
let fallback = run_ffmpeg_with_timeout(
|
||||
@@ -211,7 +210,7 @@ pub(super) fn extract_poster_frame(
|
||||
|
||||
if !fallback.status.success() || !output.exists() {
|
||||
let stderr = String::from_utf8_lossy(&fallback.stderr);
|
||||
eprintln!("sprout-desktop: poster frame extraction failed: {stderr}");
|
||||
eprintln!("buzz-desktop: poster frame extraction failed: {stderr}");
|
||||
let _ = std::fs::remove_file(&output);
|
||||
return Err("ffmpeg could not extract a poster frame".to_string());
|
||||
}
|
||||
@@ -238,7 +237,7 @@ pub(super) fn transcode_and_extract_poster(
|
||||
bytes
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: poster extraction failed (non-fatal): {e}");
|
||||
eprintln!("buzz-desktop: poster extraction failed (non-fatal): {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
@@ -347,7 +347,7 @@ pub(crate) async fn ensure_relay_mesh_for_record(
|
||||
{
|
||||
// Non-fatal: the one-sided dial may still punch on a favorable NAT.
|
||||
// Surface the reason without blocking the agent's spawn.
|
||||
eprintln!("sprout-mesh: saved-start connect-request failed: {error}");
|
||||
eprintln!("buzz-mesh: saved-start connect-request failed: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,7 +503,7 @@ mod tests {
|
||||
/// frontend selected earlier.
|
||||
///
|
||||
/// Hardware-gated (`#[ignore]`): loads a real model. Run with:
|
||||
/// cargo test -p sprout-desktop --features mesh-llm \
|
||||
/// cargo test -p buzz-desktop --features mesh-llm \
|
||||
/// ensure_serve_runtime_serves_other_model -- --ignored --nocapture
|
||||
#[tokio::test]
|
||||
#[ignore = "loads a real model; run manually with --ignored"]
|
||||
|
||||
@@ -284,19 +284,19 @@ pub async fn send_channel_message(
|
||||
let media = media_tags.unwrap_or_default();
|
||||
let emoji = emoji_tags.unwrap_or_default();
|
||||
let mention_refs_only = mention_tags.unwrap_or_default();
|
||||
let kind_num = kind.unwrap_or(sprout_core::kind::KIND_STREAM_MESSAGE);
|
||||
let kind_num = kind.unwrap_or(buzz_core_pkg::kind::KIND_STREAM_MESSAGE);
|
||||
|
||||
let mut resolved_root: Option<String> = None;
|
||||
|
||||
let builder = match kind_num {
|
||||
sprout_core::kind::KIND_FORUM_POST => events::build_forum_post(
|
||||
buzz_core_pkg::kind::KIND_FORUM_POST => events::build_forum_post(
|
||||
channel_uuid,
|
||||
content.trim(),
|
||||
&mention_refs,
|
||||
&media,
|
||||
&mention_refs_only,
|
||||
)?,
|
||||
sprout_core::kind::KIND_FORUM_COMMENT => {
|
||||
buzz_core_pkg::kind::KIND_FORUM_COMMENT => {
|
||||
let parent_id = parent_event_id
|
||||
.as_deref()
|
||||
.ok_or("forum comment requires parent_event_id")?;
|
||||
@@ -362,7 +362,7 @@ pub async fn add_reaction(
|
||||
// Custom-emoji reaction (NIP-30): kind:7 with `:shortcode:` content and
|
||||
// an `["emoji", shortcode, url]` tag. Delegates to the SDK builder so
|
||||
// shortcode normalization + validation match the relay exactly.
|
||||
Some(url) => sprout_sdk::build_custom_emoji_reaction(target_eid, emoji.trim(), &url)
|
||||
Some(url) => buzz_sdk_pkg::build_custom_emoji_reaction(target_eid, emoji.trim(), &url)
|
||||
.map_err(|e| format!("invalid custom emoji reaction: {e}"))?,
|
||||
None => events::build_reaction(target_eid, emoji.trim())?,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use buzz_core_pkg::kind::KIND_PAIRING;
|
||||
use buzz_core_pkg::pairing::qr::encode_qr;
|
||||
use buzz_core_pkg::pairing::session::PairingSession;
|
||||
use buzz_core_pkg::pairing::types::{AbortReason, PayloadType};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use nostr::ToBech32;
|
||||
use serde::Serialize;
|
||||
use sprout_core::kind::KIND_PAIRING;
|
||||
use sprout_core::pairing::qr::encode_qr;
|
||||
use sprout_core::pairing::session::PairingSession;
|
||||
use sprout_core::pairing::types::{AbortReason, PayloadType};
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
@@ -400,7 +400,7 @@ fn event_to_relay_json(event: &nostr::Event) -> String {
|
||||
format!("[\"EVENT\",{}]", nostr::JsonUtil::as_json(event))
|
||||
}
|
||||
|
||||
/// Parse a relay EVENT message into a nostr 0.36 Event (sprout-core compatible).
|
||||
/// Parse a relay EVENT message into a nostr 0.36 Event (buzz-core compatible).
|
||||
fn parse_relay_event(text: &str, sub_id: &str) -> Option<nostr::Event> {
|
||||
let arr: serde_json::Value = serde_json::from_str(text).ok()?;
|
||||
let arr = arr.as_array()?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use buzz_core_pkg::PresenceStatus;
|
||||
use serde_json::Value;
|
||||
use sprout_core::PresenceStatus;
|
||||
use tauri::State;
|
||||
|
||||
use crate::{
|
||||
@@ -196,7 +196,7 @@ pub async fn search_users(
|
||||
|
||||
// NIP-50 full-text search on kind:0 profiles. The relay's HTTP bridge
|
||||
// intercepts the `search` field on POST /query and routes to Typesense
|
||||
// (see `crates/sprout-relay/src/api/bridge.rs::handle_bridge_search`),
|
||||
// (see `crates/buzz-relay/src/api/bridge.rs::handle_bridge_search`),
|
||||
// so we get indexed, server-side search instead of fetching every kind:0
|
||||
// and scanning client-side. The old path was capped at 2000 kind:0 events
|
||||
// by the relay's HTTP bridge limit, which silently hid users on busy relays.
|
||||
|
||||
@@ -260,7 +260,7 @@ fn parse_team_from_pack_zip(zip_bytes: &[u8]) -> Result<ParsedTeamPreview, Strin
|
||||
|
||||
let pack_root = crate::managed_agents::find_plugin_json(tmp.path())
|
||||
.ok_or_else(|| "No .plugin/plugin.json found in ZIP".to_string())?;
|
||||
let resolved = sprout_persona::resolve::resolve_pack(&pack_root)
|
||||
let resolved = buzz_persona_pkg::resolve::resolve_pack(&pack_root)
|
||||
.map_err(|e| format!("Pack validation failed: {e}"))?;
|
||||
|
||||
if resolved.personas.is_empty() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Signed-event builders for desktop write operations.
|
||||
//!
|
||||
//! Mirrors the sprout-sdk builder patterns but uses nostr 0.37 API
|
||||
//! Mirrors the buzz-sdk builder patterns but uses nostr 0.37 API
|
||||
//! (the desktop is excluded from the workspace which pins nostr 0.36).
|
||||
//!
|
||||
//! Mental model:
|
||||
@@ -9,19 +9,19 @@
|
||||
//! Each function validates inputs and returns a nostr::EventBuilder.
|
||||
//! Signing and submission happen in relay::submit_event.
|
||||
|
||||
use buzz_core_pkg::kind::{KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST};
|
||||
use nostr::{EventBuilder, EventId, Kind, Tag};
|
||||
use sprout_core::kind::{KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST};
|
||||
use uuid::Uuid;
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Maximum content size — matches sprout-sdk (64 KiB).
|
||||
/// Maximum content size — matches buzz-sdk (64 KiB).
|
||||
const MAX_CONTENT_BYTES: usize = 64 * 1024;
|
||||
|
||||
/// Maximum mention count — matches sprout-sdk.
|
||||
/// Maximum mention count — matches buzz-sdk.
|
||||
const MAX_MENTIONS: usize = 50;
|
||||
|
||||
/// Maximum emoji length in characters — matches sprout-sdk.
|
||||
/// Maximum emoji length in characters — matches buzz-sdk.
|
||||
const MAX_EMOJI_CHARS: usize = 64;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -33,7 +33,7 @@ Your text is read aloud via TTS. You will be interrupted when humans speak — t
|
||||
- When tool results are long, summarize the key finding verbally.
|
||||
- If interrupted, continue naturally. No apologies.
|
||||
- In multi-agent huddles, identify yourself only when needed.
|
||||
- Use your Sprout tools proactively when asked."
|
||||
- Use your Buzz tools proactively when asked."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ pub async fn add_agent_to_huddle(
|
||||
Ok(_) => (true, None),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: add agent to parent channel failed (may already be member): {e}"
|
||||
"buzz-desktop: add agent to parent channel failed (may already be member): {e}"
|
||||
);
|
||||
(false, Some(e))
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ pub(crate) fn open_output_sink_by_name(
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"sprout-desktop: preferred output device {name:?} not found, falling back to default"
|
||||
"buzz-desktop: preferred output device {name:?} not found, falling back to default"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ pub async fn set_voice_input_mode(
|
||||
// Best-effort restart — if models aren't ready, the pipeline
|
||||
// stays down until the next hotstart cycle picks it up.
|
||||
if let Err(e) = maybe_start_stt_pipeline(&state, &eph_id).await {
|
||||
eprintln!("sprout-desktop: STT pipeline restart on mode switch failed: {e}");
|
||||
eprintln!("buzz-desktop: STT pipeline restart on mode switch failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,7 @@ pub async fn start_huddle(
|
||||
events::build_huddle_guidelines(&ephemeral_channel_id, &guidelines)
|
||||
{
|
||||
if let Err(e) = submit_event(guidelines_builder, &state).await {
|
||||
eprintln!("sprout-desktop: huddle guidelines (kind:48106) failed: {e}");
|
||||
eprintln!("buzz-desktop: huddle guidelines (kind:48106) failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ pub async fn start_huddle(
|
||||
match submit_event(add_builder, &state).await {
|
||||
Ok(_) => successful_agents.push(pubkey.clone()),
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: huddle add_member failed for {pubkey}: {e}");
|
||||
eprintln!("buzz-desktop: huddle add_member failed for {pubkey}: {e}");
|
||||
// Intentionally not added — policy rejected this agent.
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,7 @@ pub async fn start_huddle(
|
||||
if let Ok(archive_builder) = events::build_archive(ephemeral_uuid) {
|
||||
if let Err(ae) = submit_event(archive_builder, &state).await {
|
||||
eprintln!(
|
||||
"sprout-desktop: rollback archive of {ephemeral_channel_id} failed: {ae}"
|
||||
"buzz-desktop: rollback archive of {ephemeral_channel_id} failed: {ae}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -287,7 +287,7 @@ pub async fn start_huddle(
|
||||
if let Ok(archive_builder) = events::build_archive(ephemeral_uuid) {
|
||||
if let Err(ae) = submit_event(archive_builder, &state).await {
|
||||
eprintln!(
|
||||
"sprout-desktop: rollback archive of {ephemeral_channel_id} failed: {ae}"
|
||||
"buzz-desktop: rollback archive of {ephemeral_channel_id} failed: {ae}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -423,7 +423,7 @@ async fn emit_end_and_archive(
|
||||
events::build_huddle_ended(parent_channel_id, ephemeral_channel_id)
|
||||
{
|
||||
if let Err(e) = submit_event(ended_builder, state).await {
|
||||
eprintln!("sprout-desktop: huddle_ended event failed: {e}");
|
||||
eprintln!("buzz-desktop: huddle_ended event failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,7 +431,7 @@ async fn emit_end_and_archive(
|
||||
if let Ok(uuid) = parse_channel_uuid(ephemeral_channel_id) {
|
||||
if let Ok(archive_builder) = events::build_archive(uuid) {
|
||||
if let Err(e) = submit_event(archive_builder, state).await {
|
||||
eprintln!("sprout-desktop: archive ephemeral channel failed: {e}");
|
||||
eprintln!("buzz-desktop: archive ephemeral channel failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -481,14 +481,14 @@ pub async fn leave_huddle(state: State<'_, AppState>) -> Result<(), String> {
|
||||
// Archive subsumes leave (the channel is gone, membership is moot).
|
||||
// This avoids the "cannot remove the last owner" relay error that
|
||||
// build_leave hits when the creator is the sole remaining member.
|
||||
eprintln!("sprout-desktop: last human left huddle — auto-ending");
|
||||
eprintln!("buzz-desktop: last human left huddle — auto-ending");
|
||||
emit_end_and_archive(&parent_channel_id, &ephemeral_channel_id, &state).await;
|
||||
} else {
|
||||
// Other humans still in the huddle — just remove self from membership.
|
||||
if let Ok(eph_uuid) = parse_channel_uuid(&ephemeral_channel_id) {
|
||||
if let Ok(leave_builder) = events::build_leave(eph_uuid) {
|
||||
if let Err(e) = submit_event(leave_builder, &state).await {
|
||||
eprintln!("sprout-desktop: huddle leave ephemeral channel failed: {e}");
|
||||
eprintln!("buzz-desktop: huddle leave ephemeral channel failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -671,14 +671,14 @@ pub async fn check_pipeline_hotstart(state: State<'_, AppState>) -> Result<(), S
|
||||
// Start TTS first (so STT can capture tts_cancel).
|
||||
if !has_tts && (tts_ready || models::is_tts_ready()) {
|
||||
if let Err(e) = maybe_start_tts_pipeline(&state).await {
|
||||
eprintln!("sprout-desktop: TTS hotstart failed: {e}");
|
||||
eprintln!("buzz-desktop: TTS hotstart failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if !has_stt && (stt_ready || models::is_stt_ready()) {
|
||||
if let Some(eph_id) = &ephemeral_channel_id {
|
||||
if let Err(e) = maybe_start_stt_pipeline(&state, eph_id).await {
|
||||
eprintln!("sprout-desktop: STT hotstart failed: {e}");
|
||||
eprintln!("buzz-desktop: STT hotstart failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,7 +812,7 @@ pub async fn set_tts_enabled(enabled: bool, state: State<'_, AppState>) -> Resul
|
||||
};
|
||||
if matches!(phase, HuddlePhase::Connected | HuddlePhase::Active) {
|
||||
if let Err(e) = maybe_start_tts_pipeline(&state).await {
|
||||
eprintln!("sprout-desktop: TTS pipeline restart failed: {e}");
|
||||
eprintln!("buzz-desktop: TTS pipeline restart failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -853,7 +853,7 @@ pub async fn speak_agent_message(text: String, state: State<'_, AppState>) -> Re
|
||||
// Lazy-start: models may have finished downloading after the huddle began.
|
||||
if needs_pipeline {
|
||||
if let Err(e) = maybe_start_tts_pipeline(&state).await {
|
||||
eprintln!("sprout-desktop: TTS lazy-start failed: {e}");
|
||||
eprintln!("buzz-desktop: TTS lazy-start failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//! Model download manager for STT (Parakeet TDT-CTC 110M) and TTS (Pocket TTS) models.
|
||||
//!
|
||||
//! Mental model:
|
||||
//! app launch → start_stt_download (background) → ~/.sprout/models/parakeet-tdt-ctc-110m-en/
|
||||
//! app launch → start_tts_download (background) → ~/.sprout/models/pocket-tts/
|
||||
//! app launch → start_stt_download (background) → ~/.buzz/models/parakeet-tdt-ctc-110m-en/
|
||||
//! app launch → start_tts_download (background) → ~/.buzz/models/pocket-tts/
|
||||
//! STT pipeline → is_stt_ready() → stt_model_dir() → run inference
|
||||
//! TTS pipeline → is_tts_ready() → tts_model_dir() → run synthesis
|
||||
//!
|
||||
//! Models are downloaded once and cached. A version manifest (`.sprout-model-manifest`)
|
||||
//! Models are downloaded once and cached. A version manifest (`.buzz-model-manifest`)
|
||||
//! is written alongside model files — if the on-disk version doesn't match the
|
||||
//! compiled-in version, the model is re-downloaded.
|
||||
//!
|
||||
//! Upgrade note: an older Moonshine STT model directory at
|
||||
//! `~/.sprout/models/moonshine-tiny/` is removed best-effort once the new STT
|
||||
//! `~/.buzz/models/moonshine-tiny/` is removed best-effort once the new STT
|
||||
//! model finishes installing successfully. Cleanup is gated on the new model
|
||||
//! being Ready, so a failed download never removes the previous on-disk model
|
||||
//! during migration. If removal fails (permissions, etc.) the leftover is
|
||||
@@ -94,7 +94,7 @@ const STT_MODEL_VERSION: &str = "2";
|
||||
const TTS_MODEL_VERSION: &str = "2";
|
||||
|
||||
/// Filename for the version manifest written alongside model files.
|
||||
const MANIFEST_FILENAME: &str = ".sprout-model-manifest";
|
||||
const MANIFEST_FILENAME: &str = ".buzz-model-manifest";
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -117,12 +117,12 @@ const STT_DOWNLOAD_URL: &str =
|
||||
/// Subdirectory name produced by `tar xjf` on the archive.
|
||||
const STT_ARCHIVE_SUBDIR: &str = "sherpa-onnx-nemo-parakeet_tdt_ctc_110m-en-36000-int8";
|
||||
|
||||
/// Final directory name under `~/.sprout/models/`.
|
||||
/// Final directory name under `~/.buzz/models/`.
|
||||
const STT_MODEL_DIR_NAME: &str = "parakeet-tdt-ctc-110m-en";
|
||||
|
||||
/// All files that must be present for the model to be considered ready.
|
||||
///
|
||||
/// Includes the attribution sidecar written by Sprout during install. The
|
||||
/// Includes the attribution sidecar written by Buzz during install. The
|
||||
/// upstream archive does not ship a license file, so readiness should require
|
||||
/// the local CC-BY-4.0 attribution to travel with the cached model bytes.
|
||||
const STT_EXPECTED_FILES: &[&str] = &["model.int8.onnx", "tokens.txt", STT_LICENSE_FILE_NAME];
|
||||
@@ -143,7 +143,7 @@ Licensed under the Creative Commons Attribution 4.0 International License
|
||||
|
||||
Original model: https://huggingface.co/nvidia/parakeet-tdt_ctc-110m
|
||||
Converted to ONNX with int8 quantization by the sherpa-onnx project
|
||||
(https://github.com/k2-fsa/sherpa-onnx); Sprout ships this conversion
|
||||
(https://github.com/k2-fsa/sherpa-onnx); Buzz ships this conversion
|
||||
unmodified.
|
||||
|
||||
Provided \"AS IS\", without warranty of any kind, express or implied. See the
|
||||
@@ -152,7 +152,7 @@ license text for full warranty disclaimer.
|
||||
|
||||
// ── Pocket TTS model ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Final directory name under `~/.sprout/models/`.
|
||||
/// Final directory name under `~/.buzz/models/`.
|
||||
const TTS_MODEL_DIR_NAME: &str = "pocket-tts";
|
||||
|
||||
/// Attribution sidecar written next to the Pocket TTS model files.
|
||||
@@ -184,7 +184,7 @@ https://datashare.ed.ac.uk/handle/10283/3443 (CC-BY-4.0).
|
||||
Recording enhancement (denoise/dereverb) by ai-coustics:
|
||||
https://ai-coustics.com/
|
||||
|
||||
Sprout ships all ONNX/model artifacts and the reference voice WAV unmodified,
|
||||
Buzz ships all ONNX/model artifacts and the reference voice WAV unmodified,
|
||||
renamed only by placement in the local model directory.
|
||||
|
||||
Provided \"AS IS\", without warranty of any kind, express or implied. See the
|
||||
@@ -391,7 +391,7 @@ where
|
||||
/// Per-model state + config. `ModelManager` owns two of these (stt, tts).
|
||||
#[derive(Clone)]
|
||||
struct ModelSlot {
|
||||
dir_name: &'static str, // subdir under ~/.sprout/models/
|
||||
dir_name: &'static str, // subdir under ~/.buzz/models/
|
||||
expected_files: &'static [&'static str], // files required for "ready"
|
||||
version: &'static str, // manifest version; increment to force re-download
|
||||
status: Arc<Mutex<ModelStatus>>,
|
||||
@@ -474,7 +474,7 @@ impl ModelSlot {
|
||||
// is accessible on the current thread. Tauri's runtime is always available.
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = download_fn(http_client).await {
|
||||
eprintln!("sprout-desktop: {name} download failed: {e}");
|
||||
eprintln!("buzz-desktop: {name} download failed: {e}");
|
||||
slot.set_status(ModelStatus::Error(e));
|
||||
}
|
||||
});
|
||||
@@ -539,18 +539,18 @@ impl ModelSlot {
|
||||
/// Cheap to clone — all inner state is behind `Arc`.
|
||||
#[derive(Clone)]
|
||||
pub struct ModelManager {
|
||||
/// `~/.sprout/models/`
|
||||
/// `~/.buzz/models/`
|
||||
models_dir: PathBuf,
|
||||
stt: ModelSlot,
|
||||
tts: ModelSlot,
|
||||
}
|
||||
|
||||
impl ModelManager {
|
||||
/// Create a new `ModelManager` rooted at `~/.sprout/models/`.
|
||||
/// Create a new `ModelManager` rooted at `~/.buzz/models/`.
|
||||
///
|
||||
/// Returns `None` if the home directory cannot be resolved.
|
||||
pub fn new() -> Option<Self> {
|
||||
let models_dir = dirs::home_dir()?.join(".sprout").join("models");
|
||||
let models_dir = dirs::home_dir()?.join(".buzz").join("models");
|
||||
Some(Self {
|
||||
models_dir,
|
||||
stt: ModelSlot::new(STT_MODEL_DIR_NAME, STT_EXPECTED_FILES, STT_MODEL_VERSION),
|
||||
@@ -654,7 +654,7 @@ impl ModelManager {
|
||||
.join(format!("{STT_MODEL_DIR_NAME}.tar.bz2"));
|
||||
let temp_dir = self.models_dir.join(format!("{STT_MODEL_DIR_NAME}.tmp"));
|
||||
|
||||
eprintln!("sprout-desktop: downloading STT model from {STT_DOWNLOAD_URL}");
|
||||
eprintln!("buzz-desktop: downloading STT model from {STT_DOWNLOAD_URL}");
|
||||
let response = fetch_url(&http_client, STT_DOWNLOAD_URL, "stt archive").await?;
|
||||
|
||||
let slot = self.stt.clone();
|
||||
@@ -674,7 +674,7 @@ impl ModelManager {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
eprintln!("sprout-desktop: downloaded {bytes} bytes, wrote to disk");
|
||||
eprintln!("buzz-desktop: downloaded {bytes} bytes, wrote to disk");
|
||||
|
||||
// Verify archive integrity before extraction.
|
||||
let hash = sha256_file(&archive_path).await?;
|
||||
@@ -690,7 +690,7 @@ impl ModelManager {
|
||||
});
|
||||
fresh_temp_dir(&temp_dir).await?;
|
||||
|
||||
eprintln!("sprout-desktop: extracting STT archive…");
|
||||
eprintln!("buzz-desktop: extracting STT archive…");
|
||||
let (ap, td) = (archive_path.clone(), temp_dir.clone());
|
||||
tokio::task::spawn_blocking(move || extract_archive(&ap, &td))
|
||||
.await
|
||||
@@ -735,7 +735,7 @@ impl ModelManager {
|
||||
cleanup_legacy_moonshine_dir(&self.models_dir).await;
|
||||
|
||||
eprintln!(
|
||||
"sprout-desktop: STT model ready at {}",
|
||||
"buzz-desktop: STT model ready at {}",
|
||||
self.stt.model_dir(&self.models_dir).display()
|
||||
);
|
||||
Ok(())
|
||||
@@ -743,10 +743,10 @@ impl ModelManager {
|
||||
|
||||
/// Download and verify the Pocket TTS model files from HuggingFace.
|
||||
///
|
||||
/// Downloads files into `~/.sprout/models/pocket-tts/`:
|
||||
/// Downloads files into `~/.buzz/models/pocket-tts/`:
|
||||
/// - five ONNX sessions (Pocket TTS + Mimi codec)
|
||||
/// - `vocab.json` / `token_scores.json` for sherpa-onnx text conditioning
|
||||
/// - upstream `LICENSE` plus Sprout's `MODEL_LICENSE.txt` attribution sidecar
|
||||
/// - upstream `LICENSE` plus Buzz's `MODEL_LICENSE.txt` attribution sidecar
|
||||
/// - `reference_sample.wav` as the bundled default voice
|
||||
///
|
||||
/// Files are written to a temp directory first, then moved atomically.
|
||||
@@ -776,7 +776,7 @@ impl ModelManager {
|
||||
let total_files = downloads.len() as u32;
|
||||
|
||||
for (i, (url, filename)) in downloads.iter().enumerate() {
|
||||
eprintln!("sprout-desktop: downloading Pocket TTS {filename} from {url}");
|
||||
eprintln!("buzz-desktop: downloading Pocket TTS {filename} from {url}");
|
||||
|
||||
let response = fetch_url(&http_client, url, filename)
|
||||
.await
|
||||
@@ -810,7 +810,7 @@ impl ModelManager {
|
||||
.inspect_err(|_| {
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
})?;
|
||||
eprintln!("sprout-desktop: downloaded {bytes} bytes ({filename}), wrote to disk");
|
||||
eprintln!("buzz-desktop: downloaded {bytes} bytes ({filename}), wrote to disk");
|
||||
|
||||
let expected = TTS_FILE_HASHES
|
||||
.iter()
|
||||
@@ -850,7 +850,7 @@ impl ModelManager {
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"sprout-desktop: Pocket TTS model ready at {}",
|
||||
"buzz-desktop: Pocket TTS model ready at {}",
|
||||
self.tts.model_dir(&self.models_dir).display()
|
||||
);
|
||||
Ok(())
|
||||
@@ -882,7 +882,7 @@ pub fn is_stt_ready() -> bool {
|
||||
|
||||
/// Best-effort cleanup of the legacy Moonshine STT model directory.
|
||||
///
|
||||
/// Removes `~/.sprout/models/moonshine-tiny/` if present (~70 MB on disk).
|
||||
/// Removes `~/.buzz/models/moonshine-tiny/` if present (~70 MB on disk).
|
||||
/// Idempotent — no-op if the directory is absent. Errors are logged and
|
||||
/// swallowed; the leftover is harmless and the user can remove it manually.
|
||||
///
|
||||
@@ -897,11 +897,11 @@ async fn cleanup_legacy_moonshine_dir(models_dir: &Path) {
|
||||
}
|
||||
match tokio::fs::remove_dir_all(&legacy).await {
|
||||
Ok(()) => eprintln!(
|
||||
"sprout-desktop: removed legacy STT model dir {}",
|
||||
"buzz-desktop: removed legacy STT model dir {}",
|
||||
legacy.display()
|
||||
),
|
||||
Err(e) => eprintln!(
|
||||
"sprout-desktop: could not remove legacy STT model dir {}: {e} \
|
||||
"buzz-desktop: could not remove legacy STT model dir {}: {e} \
|
||||
(harmless — remove manually to reclaim disk space)",
|
||||
legacy.display()
|
||||
),
|
||||
|
||||
@@ -60,10 +60,10 @@ pub(crate) async fn post_connect_setup(
|
||||
|
||||
// Start pipelines: TTS first (so STT can capture tts_cancel for barge-in).
|
||||
if let Err(e) = maybe_start_tts_pipeline(state).await {
|
||||
eprintln!("sprout-desktop: TTS pipeline failed to start: {e}");
|
||||
eprintln!("buzz-desktop: TTS pipeline failed to start: {e}");
|
||||
}
|
||||
if let Err(e) = maybe_start_stt_pipeline(state, ephemeral_channel_id).await {
|
||||
eprintln!("sprout-desktop: STT pipeline failed to start: {e}");
|
||||
eprintln!("buzz-desktop: STT pipeline failed to start: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -273,14 +273,14 @@ pub(crate) fn spawn_transcription_task(
|
||||
match events::build_message(channel_uuid, &t, None, &p_tags, &[], &[], &[]) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT build_message: {e}");
|
||||
eprintln!("buzz-desktop: STT build_message: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let event = match builder.sign_with_keys(&keys) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT sign event: {e}");
|
||||
eprintln!("buzz-desktop: STT sign event: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -294,7 +294,7 @@ pub(crate) fn spawn_transcription_task(
|
||||
) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT NIP-98 auth: {e}");
|
||||
eprintln!("buzz-desktop: STT NIP-98 auth: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -311,12 +311,12 @@ pub(crate) fn spawn_transcription_task(
|
||||
Ok(resp) if resp.status().is_success() => {}
|
||||
Ok(resp) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: STT kind:9 post failed: HTTP {}",
|
||||
"buzz-desktop: STT kind:9 post failed: HTTP {}",
|
||||
resp.status()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT kind:9 post failed: {e}");
|
||||
eprintln!("buzz-desktop: STT kind:9 post failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ impl PeerSlot {
|
||||
last_packet_at: tokio::time::Instant::now(),
|
||||
}),
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: jitter buffer init peer {peer_idx}: {e}");
|
||||
eprintln!("buzz-desktop: jitter buffer init peer {peer_idx}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ pub(crate) async fn run_playout_recv_loop(
|
||||
// queue grow without bound.
|
||||
if slot.player.len() >= PLAYOUT_QUEUE_HIGH_WATER {
|
||||
eprintln!(
|
||||
"sprout-desktop: playout queue high-water for peer {peer_idx} \
|
||||
"buzz-desktop: playout queue high-water for peer {peer_idx} \
|
||||
(depth={}) — dropping oldest frame",
|
||||
slot.player.len(),
|
||||
);
|
||||
@@ -204,7 +204,7 @@ pub(crate) async fn run_playout_recv_loop(
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: jitter get_audio peer {peer_idx}: {e}"
|
||||
"buzz-desktop: jitter get_audio peer {peer_idx}: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -237,7 +237,7 @@ pub(crate) async fn run_playout_recv_loop(
|
||||
// the slice is too short, which `if data.len() <= ...`
|
||||
// already guards. Defensive log + drop.
|
||||
eprintln!(
|
||||
"sprout-desktop: dropping malformed audio frame from peer {peer_idx} ({} bytes)",
|
||||
"buzz-desktop: dropping malformed audio frame from peer {peer_idx} ({} bytes)",
|
||||
data.len(),
|
||||
);
|
||||
continue;
|
||||
@@ -281,7 +281,7 @@ pub(crate) async fn run_playout_recv_loop(
|
||||
.insert_packet(header.seq, header.ts_48k, opus_bytes)
|
||||
{
|
||||
eprintln!(
|
||||
"sprout-desktop: jitter insert peer {peer_idx}: {err}"
|
||||
"buzz-desktop: jitter insert peer {peer_idx}: {err}"
|
||||
);
|
||||
} else {
|
||||
// Heartbeat for the playout tick's idle-peer
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
//! in <https://huggingface.co/kyutai/tts-voices>. CC-BY-4.0, base recording
|
||||
//! from the VCTK corpus, enhanced by ai-coustics.
|
||||
//!
|
||||
//! Sprout ships these files unmodified; see the on-disk `MODEL_LICENSE.txt`
|
||||
//! Buzz ships these files unmodified; see the on-disk `MODEL_LICENSE.txt`
|
||||
//! sidecar written by `huddle::models` during install for the canonical
|
||||
//! CC-BY-4.0 §3(a)(1) attribution block.
|
||||
//!
|
||||
@@ -539,7 +539,7 @@ impl PocketTts {
|
||||
let sample_rate = audio.sample_rate();
|
||||
if sample_rate != SAMPLE_RATE as i32 {
|
||||
eprintln!(
|
||||
"sprout-desktop: Pocket TTS returned unexpected sample rate {sample_rate}Hz \
|
||||
"buzz-desktop: Pocket TTS returned unexpected sample rate {sample_rate}Hz \
|
||||
(expected {SAMPLE_RATE}Hz); playback speed may be wrong"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ pub(crate) async fn connect_audio_relay(
|
||||
})
|
||||
.await
|
||||
{
|
||||
eprintln!("sprout-desktop: audio relay pipeline exited: {e}");
|
||||
eprintln!("buzz-desktop: audio relay pipeline exited: {e}");
|
||||
}
|
||||
|
||||
// Only emit the disconnect event for UNEXPECTED exits.
|
||||
@@ -292,7 +292,7 @@ async fn audio_relay_pipeline(args: AudioRelayPipelineArgs) -> Result<(), String
|
||||
let n = match encode_result {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: opus encode error: {e}");
|
||||
eprintln!("buzz-desktop: opus encode error: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -363,7 +363,7 @@ pub(crate) async fn fetch_channel_members_with_roles(
|
||||
let events = query_relay(state, std::slice::from_ref(&filter))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("sprout-desktop: fetch channel members failed: {e}");
|
||||
eprintln!("buzz-desktop: fetch channel members failed: {e}");
|
||||
e
|
||||
})?;
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ fn stt_worker(
|
||||
let mut resampler = match Fft::<f32>::new(48_000, 16_000, 1024, 2, 1, FixedSync::Input) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT resampler init failed: {e}");
|
||||
eprintln!("buzz-desktop: STT resampler init failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -239,7 +239,7 @@ fn stt_worker(
|
||||
let model_path = model_dir.join("model.int8.onnx");
|
||||
if !tokens_path.exists() || !model_path.exists() {
|
||||
eprintln!(
|
||||
"sprout-desktop: STT model not found at {} — STT disabled",
|
||||
"buzz-desktop: STT model not found at {} — STT disabled",
|
||||
model_dir.display()
|
||||
);
|
||||
drain_until_shutdown(audio_rx, &shutdown);
|
||||
@@ -257,7 +257,7 @@ fn stt_worker(
|
||||
let recognizer = match OfflineRecognizer::create(&cfg) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
eprintln!("sprout-desktop: OfflineRecognizer::create returned None — STT disabled");
|
||||
eprintln!("buzz-desktop: OfflineRecognizer::create returned None — STT disabled");
|
||||
drain_until_shutdown(audio_rx, &shutdown);
|
||||
return;
|
||||
}
|
||||
@@ -369,7 +369,7 @@ fn resample_chunk(resampler: &mut rubato::Fft<f32>, chunk_48k: &[f32]) -> Vec<f3
|
||||
let input = match InterleavedSlice::new(chunk_48k, 1, chunk_48k.len()) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT resample input error: {e}");
|
||||
eprintln!("buzz-desktop: STT resample input error: {e}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
@@ -377,7 +377,7 @@ fn resample_chunk(resampler: &mut rubato::Fft<f32>, chunk_48k: &[f32]) -> Vec<f3
|
||||
match resampler.process(&input, 0, None) {
|
||||
Ok(out) => out.take_data(),
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: STT resample error: {e}");
|
||||
eprintln!("buzz-desktop: STT resample error: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
@@ -545,7 +545,7 @@ fn flush_to_stt(
|
||||
|
||||
if !text.is_empty() {
|
||||
if let Err(e) = text_tx.blocking_send(text) {
|
||||
eprintln!("sprout-desktop: STT text channel closed: {e}");
|
||||
eprintln!("buzz-desktop: STT text channel closed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ impl TtsPipeline {
|
||||
/// `TEXT_QUEUE_DEPTH`) — caller may log and discard.
|
||||
pub fn speak(&self, text: String) -> Result<(), String> {
|
||||
self.text_tx.try_send(text).map_err(|e| {
|
||||
eprintln!("sprout-desktop: TTS queue saturated, dropping message: {e}");
|
||||
eprintln!("buzz-desktop: TTS queue saturated, dropping message: {e}");
|
||||
format!("TTS queue full, dropping: {e}")
|
||||
})
|
||||
}
|
||||
@@ -254,7 +254,7 @@ fn tts_worker(
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: TTS engine init failed (model_dir={}): {e}. TTS disabled.",
|
||||
"buzz-desktop: TTS engine init failed (model_dir={}): {e}. TTS disabled.",
|
||||
model_dir.display()
|
||||
);
|
||||
drain_until_shutdown(text_rx, &shutdown);
|
||||
@@ -268,7 +268,7 @@ fn tts_worker(
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: TTS voice style load failed ({voice_name}): {e}. TTS disabled."
|
||||
"buzz-desktop: TTS voice style load failed ({voice_name}): {e}. TTS disabled."
|
||||
);
|
||||
drain_until_shutdown(text_rx, &shutdown);
|
||||
return;
|
||||
@@ -284,11 +284,11 @@ fn tts_worker(
|
||||
let t = std::time::Instant::now();
|
||||
match engine.synth_chunk("warmup", "en", &style, SYNTH_STEPS, SYNTH_SPEED) {
|
||||
Ok(_) => eprintln!(
|
||||
"sprout-desktop: TTS warmup completed in {:.0}ms",
|
||||
"buzz-desktop: TTS warmup completed in {:.0}ms",
|
||||
t.elapsed().as_millis()
|
||||
),
|
||||
Err(e) => eprintln!(
|
||||
"sprout-desktop: TTS warmup failed after {:.0}ms: {e} — first utterance may be slow",
|
||||
"buzz-desktop: TTS warmup failed after {:.0}ms: {e} — first utterance may be slow",
|
||||
t.elapsed().as_millis()
|
||||
),
|
||||
}
|
||||
@@ -301,7 +301,7 @@ fn tts_worker(
|
||||
{
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: TTS audio output failed: {e}. TTS disabled.");
|
||||
eprintln!("buzz-desktop: TTS audio output failed: {e}. TTS disabled.");
|
||||
drain_until_shutdown(text_rx, &shutdown);
|
||||
return;
|
||||
}
|
||||
@@ -373,14 +373,14 @@ fn tts_worker(
|
||||
let channels = match NonZero::new(1u16) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
eprintln!("sprout-desktop: TTS channel count invariant violated");
|
||||
eprintln!("buzz-desktop: TTS channel count invariant violated");
|
||||
break;
|
||||
}
|
||||
};
|
||||
let rate = match NonZero::new(SAMPLE_RATE) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
eprintln!("sprout-desktop: TTS sample rate invariant violated");
|
||||
eprintln!("buzz-desktop: TTS sample rate invariant violated");
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -432,7 +432,7 @@ fn tts_worker(
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: TTS synth failed: {e}");
|
||||
eprintln!("buzz-desktop: TTS synth failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,13 +233,13 @@ fn shutdown_managed_agents(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
// All tracked PIDs have already been killed above, so pass an empty skip list.
|
||||
managed_agents::sweep_orphaned_agent_processes(app, &[]);
|
||||
|
||||
// System-wide sweep: agent workers (goose, sprout-agent, etc.) are spawned
|
||||
// in their own process groups by sprout-acp, so group-kills above only
|
||||
// System-wide sweep: agent workers (goose, buzz-agent, etc.) are spawned
|
||||
// in their own process groups by buzz-acp, so group-kills above only
|
||||
// reach the harness, not the workers. Scan all user processes and kill any
|
||||
// known agent binaries that are still running.
|
||||
managed_agents::sweep_system_agent_processes(&managed_agents::current_instance_id(app), &[]);
|
||||
|
||||
// Dead-instance reaping: find agents belonging to Sprout instances
|
||||
// Dead-instance reaping: find agents belonging to Buzz instances
|
||||
// whose desktop process is no longer running and reap them.
|
||||
managed_agents::reap_dead_instance_agents(&managed_agents::current_instance_id(app), &[]);
|
||||
|
||||
@@ -282,7 +282,7 @@ fn parse_message_deep_link(url: &Url) -> Option<serde_json::Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Handle an incoming `buzz://` deep link URL, with `sprout://` accepted as a legacy alias.
|
||||
/// Handle an incoming `buzz://` deep link URL.
|
||||
///
|
||||
/// Currently supports:
|
||||
/// - `buzz://connect?relay=<ws(s)://...>` — emits `deep-link-connect` to the frontend
|
||||
@@ -290,13 +290,13 @@ fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
|
||||
let url = match Url::parse(url_str) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: invalid deep link URL {url_str:?}: {e}");
|
||||
eprintln!("buzz-desktop: invalid deep link URL {url_str:?}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if url.scheme() != "sprout" && url.scheme() != "buzz" {
|
||||
eprintln!("sprout-desktop: ignoring unsupported deep link scheme: {url_str}");
|
||||
if url.scheme() != "buzz" {
|
||||
eprintln!("buzz-desktop: ignoring unsupported deep link scheme: {url_str}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
|
||||
.find(|(k, _)| k == "relay")
|
||||
.map(|(_, v)| v.into_owned());
|
||||
let Some(relay_url) = relay else {
|
||||
eprintln!("sprout-desktop: connect deep link missing relay param: {url_str}");
|
||||
eprintln!("buzz-desktop: connect deep link missing relay param: {url_str}");
|
||||
return;
|
||||
};
|
||||
// Validate the relay URL is ws:// or wss://
|
||||
@@ -315,13 +315,13 @@ fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
|
||||
Ok(parsed) if parsed.scheme() == "ws" || parsed.scheme() == "wss" => {}
|
||||
Ok(parsed) => {
|
||||
eprintln!(
|
||||
"sprout-desktop: rejecting non-websocket relay URL scheme {:?}: {relay_url}",
|
||||
"buzz-desktop: rejecting non-websocket relay URL scheme {:?}: {relay_url}",
|
||||
parsed.scheme()
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("sprout-desktop: invalid relay URL {relay_url:?}: {e}");
|
||||
eprintln!("buzz-desktop: invalid relay URL {relay_url:?}: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -337,16 +337,16 @@ fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
|
||||
// structure on this side (serde JSON) and let the TS code own
|
||||
// any further normalisation.
|
||||
let Some(payload) = parse_message_deep_link(&url) else {
|
||||
eprintln!("sprout-desktop: message deep link missing channel or id: {url_str}");
|
||||
eprintln!("buzz-desktop: message deep link missing channel or id: {url_str}");
|
||||
return;
|
||||
};
|
||||
let _ = app.emit("deep-link-message", payload);
|
||||
}
|
||||
Some(action) => {
|
||||
eprintln!("sprout-desktop: unknown deep link action: {action}");
|
||||
eprintln!("buzz-desktop: unknown deep link action: {action}");
|
||||
}
|
||||
None => {
|
||||
eprintln!("sprout-desktop: deep link missing action: {url_str}");
|
||||
eprintln!("buzz-desktop: deep link missing action: {url_str}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +377,7 @@ pub fn run() {
|
||||
}
|
||||
// Forward any deep link URLs from the duplicate launch.
|
||||
for arg in &argv {
|
||||
if arg.starts_with("sprout://") || arg.starts_with("buzz://") {
|
||||
if arg.starts_with("buzz://") {
|
||||
handle_deep_link_url(app, arg);
|
||||
}
|
||||
}
|
||||
@@ -494,23 +494,23 @@ pub fn run() {
|
||||
// Only register the updater in release builds that were compiled with a
|
||||
// real updater configuration. Local unsigned builds omit that config and
|
||||
// should still launch for debugging.
|
||||
#[cfg(sprout_updater_enabled)]
|
||||
#[cfg(buzz_updater_enabled)]
|
||||
let builder = if cfg!(debug_assertions) {
|
||||
builder
|
||||
} else {
|
||||
builder.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
};
|
||||
|
||||
#[cfg(not(sprout_updater_enabled))]
|
||||
#[cfg(not(buzz_updater_enabled))]
|
||||
let builder = builder;
|
||||
|
||||
let shutdown_started = Arc::new(AtomicBool::new(false));
|
||||
let restore_shutdown_started = Arc::clone(&shutdown_started);
|
||||
let app = builder
|
||||
.register_asynchronous_uri_scheme_protocol("sprout-media", |ctx, request, responder| {
|
||||
.register_asynchronous_uri_scheme_protocol("buzz-media", |ctx, request, responder| {
|
||||
let app = ctx.app_handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let response = media_proxy::handle_sprout_media(&app, &request).await;
|
||||
let response = media_proxy::handle_buzz_media(&app, &request).await;
|
||||
responder.respond(response);
|
||||
});
|
||||
})
|
||||
@@ -520,6 +520,10 @@ pub fn run() {
|
||||
let app_handle = app.handle().clone();
|
||||
let shutdown_started = Arc::clone(&restore_shutdown_started);
|
||||
|
||||
// Copy legacy app data into the Buzz app data directory
|
||||
// before any state is loaded from disk.
|
||||
migration::migrate_legacy_app_data_dir(&app_handle);
|
||||
|
||||
// Sync shared agent data from the canonical dev data directory to
|
||||
// this worktree's data directory. Must run before
|
||||
// restore_managed_agents_on_launch (which reads managed-agents.json).
|
||||
@@ -530,7 +534,7 @@ pub fn run() {
|
||||
migration::migrate_persona_provider_to_runtime(&app_handle);
|
||||
|
||||
if let Err(e) = managed_agents::sync_team_personas(&app_handle) {
|
||||
eprintln!("sprout-desktop: sync-team-personas: {e}");
|
||||
eprintln!("buzz-desktop: sync-team-personas: {e}");
|
||||
}
|
||||
|
||||
// Resolve persisted identity key (env var → file → generate+save).
|
||||
@@ -574,19 +578,19 @@ pub fn run() {
|
||||
.store(port, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
|
||||
// Create the Sprout nest (~/.sprout) before agents are restored,
|
||||
// Create the Buzz nest (~/.buzz) before agents are restored,
|
||||
// so default_agent_workdir() resolves to the nest directory.
|
||||
// Non-fatal: agents fall back to $HOME if nest creation fails.
|
||||
if let Err(error) = ensure_nest() {
|
||||
eprintln!("sprout-desktop: failed to create nest: {error}");
|
||||
eprintln!("buzz-desktop: failed to create nest: {error}");
|
||||
}
|
||||
|
||||
// Create/update ~/.local/bin/sprout symlink pointing to the
|
||||
// Create/update the local CLI symlink pointing to the
|
||||
// bundled CLI binary. Non-fatal: agents find CLI via PATH.
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
if let Err(error) = managed_agents::ensure_cli_symlink(parent) {
|
||||
eprintln!("sprout-desktop: failed to create CLI symlink: {error}");
|
||||
eprintln!("buzz-desktop: failed to create CLI symlink: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,7 +608,7 @@ pub fn run() {
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut};
|
||||
let shortcut = Shortcut::new(Some(Modifiers::CONTROL), Code::Space);
|
||||
if let Err(e) = app.handle().global_shortcut().register(shortcut) {
|
||||
eprintln!("sprout-desktop: failed to register PTT shortcut: {e}");
|
||||
eprintln!("buzz-desktop: failed to register PTT shortcut: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +632,7 @@ pub fn run() {
|
||||
if let Err(error) =
|
||||
restore_managed_agents_on_launch(&app_handle, shutdown_started.as_ref()).await
|
||||
{
|
||||
eprintln!("sprout-desktop: failed to restore managed agents: {error}");
|
||||
eprintln!("buzz-desktop: failed to restore managed agents: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -855,7 +859,7 @@ pub fn run() {
|
||||
}
|
||||
std::process::exit(0);
|
||||
}) {
|
||||
eprintln!("sprout-desktop: failed to register signal handler: {e}");
|
||||
eprintln!("buzz-desktop: failed to register signal handler: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,7 +870,7 @@ pub fn run() {
|
||||
if !run_shutdown_done.swap(true, Ordering::SeqCst) {
|
||||
prevent_sleep::release(&app_handle.state::<AppState>().prevent_sleep);
|
||||
if let Err(error) = shutdown_managed_agents(app_handle) {
|
||||
eprintln!("sprout-desktop: failed to stop managed agents: {error}");
|
||||
eprintln!("buzz-desktop: failed to stop managed agents: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -905,7 +909,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_message_deep_link_extracts_required_params() {
|
||||
let url = Url::parse("sprout://message?channel=abc&id=xyz").unwrap();
|
||||
let url = Url::parse("buzz://message?channel=abc&id=xyz").unwrap();
|
||||
let payload = parse_message_deep_link(&url).expect("required params present");
|
||||
assert_eq!(payload["channelId"], "abc");
|
||||
assert_eq!(payload["messageId"], "xyz");
|
||||
@@ -922,33 +926,33 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_message_deep_link_includes_thread_root() {
|
||||
let url = Url::parse("sprout://message?channel=abc&id=xyz&thread=root1").unwrap();
|
||||
let url = Url::parse("buzz://message?channel=abc&id=xyz&thread=root1").unwrap();
|
||||
let payload = parse_message_deep_link(&url).expect("required params present");
|
||||
assert_eq!(payload["threadRootId"], "root1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_deep_link_rejects_missing_id() {
|
||||
let url = Url::parse("sprout://message?channel=abc").unwrap();
|
||||
let url = Url::parse("buzz://message?channel=abc").unwrap();
|
||||
assert!(parse_message_deep_link(&url).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_deep_link_rejects_empty_channel() {
|
||||
// Regression: `channel=&id=foo` previously produced channelId: "".
|
||||
let url = Url::parse("sprout://message?channel=&id=foo").unwrap();
|
||||
let url = Url::parse("buzz://message?channel=&id=foo").unwrap();
|
||||
assert!(parse_message_deep_link(&url).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_deep_link_rejects_empty_id() {
|
||||
let url = Url::parse("sprout://message?channel=abc&id=").unwrap();
|
||||
let url = Url::parse("buzz://message?channel=abc&id=").unwrap();
|
||||
assert!(parse_message_deep_link(&url).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_deep_link_treats_empty_thread_as_absent() {
|
||||
let url = Url::parse("sprout://message?channel=abc&id=xyz&thread=").unwrap();
|
||||
let url = Url::parse("buzz://message?channel=abc&id=xyz&thread=").unwrap();
|
||||
let payload = parse_message_deep_link(&url).expect("required params present");
|
||||
assert!(payload["threadRootId"].is_null());
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
sprout_lib::run()
|
||||
buzz_lib::run()
|
||||
}
|
||||
|
||||
@@ -408,10 +408,10 @@ pub fn validate_provider_config(config: &serde_json::Value) -> Result<(), String
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enumerate PATH for sprout-backend-* executables. Returns (id, path) pairs.
|
||||
/// Enumerate PATH for buzz-backend-* executables. Returns (id, path) pairs.
|
||||
/// Only includes files that are executable. Does NOT execute any binaries.
|
||||
pub fn discover_provider_candidates() -> Vec<(String, PathBuf)> {
|
||||
let prefix = "sprout-backend-";
|
||||
let prefix = "buzz-backend-";
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut results = Vec::new();
|
||||
|
||||
@@ -441,7 +441,7 @@ pub fn discover_provider_candidates() -> Vec<(String, PathBuf)> {
|
||||
/// 3. Returns the canonical path of the discovered binary
|
||||
///
|
||||
/// All deploy, start, and create paths MUST use this instead of raw
|
||||
/// `resolve_command(format!("sprout-backend-{id}"))` to prevent a compromised
|
||||
/// `resolve_command(format!("buzz-backend-{id}"))` to prevent a compromised
|
||||
/// frontend/IPC caller from steering execution to an arbitrary binary.
|
||||
pub fn resolve_provider_binary(provider_id: &str) -> Result<PathBuf, String> {
|
||||
// Reject IDs that could be path components or shell metacharacters.
|
||||
@@ -467,7 +467,7 @@ pub fn resolve_provider_binary(provider_id: &str) -> Result<PathBuf, String> {
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("provider binary not accessible: {e}")),
|
||||
None => Err(format!(
|
||||
"provider 'sprout-backend-{provider_id}' not found on PATH"
|
||||
"provider 'buzz-backend-{provider_id}' not found on PATH"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub(crate) struct KnownAcpRuntime {
|
||||
pub commands: &'static [&'static str],
|
||||
pub aliases: &'static [&'static str],
|
||||
pub avatar_url: &'static str,
|
||||
/// Legacy MCP server binary field. Vestigial — all agents now use sprout CLI
|
||||
/// Legacy MCP server binary field. Vestigial — all agents now use the bundled CLI.
|
||||
/// directly. Will be removed when runtime discovery is simplified.
|
||||
pub mcp_command: Option<&'static str>,
|
||||
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
|
||||
@@ -29,8 +29,8 @@ pub(crate) struct KnownAcpRuntime {
|
||||
/// Human-readable hint about installing the ACP adapter.
|
||||
pub adapter_install_hint: &'static str,
|
||||
/// Harness-specific skill discovery directory (e.g. `.goose/skills`).
|
||||
/// `Some(dir)` → Sprout creates a symlink at `<nest>/<dir>/sprout-cli`
|
||||
/// pointing to the canonical `.agents/skills/sprout-cli`. `None` → this
|
||||
/// `Some(dir)` → Buzz creates a symlink at `<nest>/<dir>/buzz-cli`
|
||||
/// pointing to the canonical `.agents/skills/buzz-cli`. `None` → this
|
||||
/// runtime reads the canonical path directly or has no skill support.
|
||||
pub skill_dir: Option<&'static str>,
|
||||
/// Whether this runtime handles model switching via ACP protocol natively.
|
||||
@@ -47,8 +47,8 @@ pub(crate) struct KnownAcpRuntime {
|
||||
const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
|
||||
const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default";
|
||||
const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default";
|
||||
const SPROUT_AGENT_AVATAR_URL: &str =
|
||||
"https://raw.githubusercontent.com/block/sprout/refs/heads/main/crates/sprout-agent/sprout-agent.png";
|
||||
const BUZZ_AGENT_AVATAR_URL: &str =
|
||||
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png";
|
||||
|
||||
fn common_binary_paths() -> &'static [PathBuf] {
|
||||
use std::sync::OnceLock;
|
||||
@@ -137,18 +137,18 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
default_env: &[],
|
||||
},
|
||||
KnownAcpRuntime {
|
||||
id: "sprout-agent",
|
||||
label: "Sprout Agent",
|
||||
commands: &["sprout-agent"],
|
||||
id: "buzz-agent",
|
||||
label: "Buzz Agent",
|
||||
commands: &["buzz-agent"],
|
||||
aliases: &[],
|
||||
avatar_url: SPROUT_AGENT_AVATAR_URL,
|
||||
mcp_command: Some("sprout-dev-mcp"),
|
||||
avatar_url: BUZZ_AGENT_AVATAR_URL,
|
||||
mcp_command: Some("buzz-dev-mcp"),
|
||||
mcp_hooks: true,
|
||||
underlying_cli: None,
|
||||
cli_install_commands: &[],
|
||||
adapter_install_commands: &[],
|
||||
install_instructions_url: "https://github.com/block/sprout",
|
||||
cli_install_hint: "Ships with the Sprout desktop app.",
|
||||
install_instructions_url: "https://github.com/block/buzz",
|
||||
cli_install_hint: "Ships with the Buzz desktop app.",
|
||||
adapter_install_hint: "",
|
||||
skill_dir: None,
|
||||
supports_acp_model_switching: true,
|
||||
@@ -232,7 +232,7 @@ fn default_agent_args(command: &str) -> Option<Vec<String>> {
|
||||
match normalize_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,
|
||||
}
|
||||
}
|
||||
@@ -288,17 +288,37 @@ fn command_search_dirs() -> Vec<PathBuf> {
|
||||
unique
|
||||
}
|
||||
|
||||
fn is_executable_file(path: &Path) -> bool {
|
||||
let Ok(metadata) = path.metadata() else {
|
||||
return false;
|
||||
};
|
||||
if !metadata.is_file() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
metadata.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_workspace_command(command: &str) -> Option<PathBuf> {
|
||||
if command_looks_like_path(command) {
|
||||
let path = PathBuf::from(command);
|
||||
return path.exists().then_some(path);
|
||||
return is_executable_file(&path).then_some(path);
|
||||
}
|
||||
|
||||
let file_name = executable_basename(command);
|
||||
command_search_dirs()
|
||||
.into_iter()
|
||||
.map(|dir| dir.join(&file_name))
|
||||
.find(|candidate| candidate.exists())
|
||||
.find(|candidate| is_executable_file(candidate))
|
||||
}
|
||||
|
||||
fn resolve_cache() -> &'static std::sync::Mutex<std::collections::HashMap<String, Option<PathBuf>>>
|
||||
@@ -351,7 +371,7 @@ fn resolve_command_uncached(command: &str) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
for candidate in path_candidates_from_env(command) {
|
||||
if candidate.exists() {
|
||||
if is_executable_file(&candidate) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
@@ -361,7 +381,7 @@ fn resolve_command_uncached(command: &str) -> Option<PathBuf> {
|
||||
}
|
||||
for dir in common_binary_paths() {
|
||||
let candidate = dir.join(executable_basename(command));
|
||||
if candidate.exists() {
|
||||
if is_executable_file(&candidate) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
@@ -401,7 +421,7 @@ fn find_via_login_shell(command: &str) -> Option<PathBuf> {
|
||||
let stdout = run_in_login_shell(&["-l", "-c", r#"command -v -- "$1""#, "_", command])?;
|
||||
let resolved = stdout.lines().rfind(|line| !line.trim().is_empty())?;
|
||||
let path = PathBuf::from(resolved.trim());
|
||||
(path.is_absolute() && path.exists()).then_some(path)
|
||||
(path.is_absolute() && is_executable_file(&path)).then_some(path)
|
||||
}
|
||||
|
||||
/// Return the user's full PATH from a login shell.
|
||||
@@ -543,7 +563,7 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
classify_runtime, find_via_login_shell, managed_agent_avatar_url, normalize_agent_args,
|
||||
CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, SPROUT_AGENT_AVATAR_URL,
|
||||
BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL,
|
||||
};
|
||||
use crate::managed_agents::AcpAvailabilityStatus;
|
||||
|
||||
@@ -596,25 +616,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_sprout_agent_avatar() {
|
||||
fn resolves_buzz_agent_avatar() {
|
||||
assert_eq!(
|
||||
managed_agent_avatar_url("sprout-agent"),
|
||||
Some(SPROUT_AGENT_AVATAR_URL.to_string())
|
||||
managed_agent_avatar_url("buzz-agent"),
|
||||
Some(BUZZ_AGENT_AVATAR_URL.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
managed_agent_avatar_url("/usr/local/bin/sprout-agent"),
|
||||
Some(SPROUT_AGENT_AVATAR_URL.to_string())
|
||||
managed_agent_avatar_url("/usr/local/bin/buzz-agent"),
|
||||
Some(BUZZ_AGENT_AVATAR_URL.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[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()
|
||||
);
|
||||
}
|
||||
@@ -622,7 +642,7 @@ mod tests {
|
||||
#[test]
|
||||
fn login_shell_lookup_treats_command_as_data() {
|
||||
let marker =
|
||||
std::env::temp_dir().join(format!("sprout-discovery-marker-{}", uuid::Uuid::new_v4()));
|
||||
std::env::temp_dir().join(format!("buzz-discovery-marker-{}", uuid::Uuid::new_v4()));
|
||||
let payload = format!("doesnotexist; touch {} #", marker.display());
|
||||
|
||||
let resolved = find_via_login_shell(&payload);
|
||||
@@ -637,6 +657,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn explicit_path_resolution_ignores_non_executable_files() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("buzz-discovery-path-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).expect("create temp dir");
|
||||
let bin = dir.join("buzz-acp");
|
||||
std::fs::write(&bin, "").expect("write placeholder");
|
||||
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o644))
|
||||
.expect("chmod placeholder");
|
||||
|
||||
assert!(
|
||||
super::resolve_workspace_command(bin.to_str().expect("utf8 path")).is_none(),
|
||||
"non-executable placeholder must not resolve"
|
||||
);
|
||||
|
||||
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755))
|
||||
.expect("chmod executable");
|
||||
assert_eq!(
|
||||
super::resolve_workspace_command(bin.to_str().expect("utf8 path")),
|
||||
Some(bin.clone())
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_available_when_adapter_found() {
|
||||
let (status, cmd, path) = classify_runtime(
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) fn filter_derived_provider_model_env_vars(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Env var keys that Sprout sets itself and users must not override from
|
||||
/// Env var keys that Buzz sets itself and users must not override from
|
||||
/// the persona/agent env_vars UI. Three categories:
|
||||
///
|
||||
/// 1. **Identity / secrets** — overriding would swap the agent's nsec or
|
||||
@@ -107,8 +107,8 @@ pub(crate) fn is_reserved_env_key(key: &str) -> bool {
|
||||
/// nit: Rust's `Command::env` will happily accept a key containing `=`
|
||||
/// or whitespace and pass it straight into the child's environ block,
|
||||
/// where `getenv("FOO")` then matches whatever comes after the first
|
||||
/// `=`. That means a key like `SPROUT_AUTH_TAG=x` with value `forged`
|
||||
/// lands as `SPROUT_AUTH_TAG=x=forged` in the child env and
|
||||
/// `=`. That means a key like `BUZZ_AUTH_TAG=x` with value `forged`
|
||||
/// lands as `BUZZ_AUTH_TAG=x=forged` in the child env and
|
||||
/// `getenv("BUZZ_AUTH_TAG")` returns `"x=forged"` — a full reserved-
|
||||
/// key bypass. Rejecting non-POSIX keys closes this hole at the
|
||||
/// boundary where the input enters the system.
|
||||
@@ -200,7 +200,7 @@ pub fn validate_user_env_keys(env_vars: &BTreeMap<String, String>) -> Result<(),
|
||||
reserved.dedup();
|
||||
if !reserved.is_empty() {
|
||||
return Err(format!(
|
||||
"the following env vars are reserved by Sprout and cannot be overridden: {}",
|
||||
"the following env vars are reserved by Buzz and cannot be overridden: {}",
|
||||
reserved.join(", ")
|
||||
));
|
||||
}
|
||||
@@ -260,7 +260,7 @@ pub(crate) fn merged_user_env(
|
||||
merged.retain(|k, v| {
|
||||
if is_reserved_env_key(k) {
|
||||
eprintln!(
|
||||
"sprout-desktop: ignoring reserved env var `{k}` from persona/agent overrides"
|
||||
"buzz-desktop: ignoring reserved env var `{k}` from persona/agent overrides"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -270,7 +270,7 @@ pub(crate) fn merged_user_env(
|
||||
// smuggle a reserved key past us via `=`-in-key tricks. See
|
||||
// `is_well_formed_env_key` for the exploit.
|
||||
eprintln!(
|
||||
"sprout-desktop: ignoring malformed env var key `{}` from persona/agent overrides",
|
||||
"buzz-desktop: ignoring malformed env var key `{}` from persona/agent overrides",
|
||||
display_invalid_key(k)
|
||||
);
|
||||
return false;
|
||||
@@ -280,13 +280,13 @@ pub(crate) fn merged_user_env(
|
||||
// have escaped the value validator; drop them here rather
|
||||
// than crash the spawn. We deliberately do NOT log the value.
|
||||
eprintln!(
|
||||
"sprout-desktop: ignoring env var `{k}` with NUL byte in value"
|
||||
"buzz-desktop: ignoring env var `{k}` with NUL byte in value"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if v.len() > MAX_ENV_VALUE_BYTES {
|
||||
eprintln!(
|
||||
"sprout-desktop: ignoring env var `{k}` with oversize value ({} bytes > {MAX_ENV_VALUE_BYTES})",
|
||||
"buzz-desktop: ignoring env var `{k}` with oversize value ({} bytes > {MAX_ENV_VALUE_BYTES})",
|
||||
v.len()
|
||||
);
|
||||
return false;
|
||||
|
||||
@@ -161,7 +161,7 @@ fn reserved_keys_include_respond_to_gate() {
|
||||
|
||||
#[test]
|
||||
fn reserved_keys_include_code_execution_surface() {
|
||||
// The agent/MCP command + args are what Sprout actually exec's.
|
||||
// The agent/MCP command + args are what Buzz actually exec's.
|
||||
// Overriding lets the user run arbitrary code as the agent.
|
||||
for key in [
|
||||
"BUZZ_ACP_AGENT_COMMAND",
|
||||
|
||||