Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 13 additions & 33 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Verify tag is on main
run: git merge-base --is-ancestor ${{ github.sha }} origin/main

test:
unit-test:
needs: guard
runs-on: macos-15
steps:
Expand All @@ -31,14 +31,24 @@ jobs:
- name: Unit tests
run: make test

integration-test:
needs: guard
runs-on: macos-15
steps:
- uses: actions/checkout@v5

- uses: actions/setup-go@v6
with:
go-version: '1.24'

- name: Integration tests
env:
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
run: make test-integration

release:
needs: test
needs: [unit-test, integration-test]
runs-on: macos-15
steps:
- uses: actions/checkout@v5
Expand Down Expand Up @@ -67,34 +77,4 @@ jobs:
chmod +x "$universal"
packaging/mkdmg.sh "$universal" "$VERSION" "Zee-${VERSION}.dmg"
shasum -a 256 "Zee-${VERSION}.dmg" >> dist/checksums.txt
gh release upload "$VERSION" "Zee-${VERSION}.dmg" --clobber

- name: Update Homebrew cask
env:
VERSION: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.ZEE_RELEASE_TOKEN }}
run: |
CLEAN_VERSION="${VERSION#v}"
SHA256=$(shasum -a 256 "Zee-${VERSION}.dmg" | awk '{print $1}')
cat > zee.rb <<CASK
cask "zee" do
version "${CLEAN_VERSION}"
sha256 "${SHA256}"
url "https://github.com/sumerc/zee/releases/download/v#{version}/Zee-v#{version}.dmg"
name "Zee"
desc "Push-to-talk transcription for macOS"
homepage "https://github.com/sumerc/zee"
app "Zee.app"
end
CASK
# remove leading whitespace from heredoc
sed -i '' 's/^ //' zee.rb
cd "$(mktemp -d)"
git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/sumerc/homebrew-tap.git" .
mkdir -p Casks
cp "$OLDPWD/zee.rb" Casks/zee.rb
git add Casks/zee.rb
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "Update zee cask to ${VERSION}"
git push
gh release upload "$VERSION" "Zee-${VERSION}.dmg" dist/checksums.txt --clobber
13 changes: 0 additions & 13 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,3 @@ release:
github:
owner: sumerc
name: zee

brews:
- name: zee
repository:
owner: sumerc
name: homebrew-tap
token: "{{ .Env.GITHUB_TOKEN }}"
homepage: "https://github.com/sumerc/zee"
description: "Push-to-talk transcription for macOS"
install: |
bin.install "zee"
test: |
system "#{bin}/zee", "-version"
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## Unreleased

### Changed
- Distribution: replaced Homebrew tap with `install.sh` one-liner (downloads DMG, verifies SHA256, copies to `/Applications`, runs `xattr -cr`). GoReleaser `brews:` block and the cask-update CI step removed.

### Removed
- `-stream` CLI flag — streaming is derived from the selected provider/model (set via tray menu / persisted in `config.json`). The flag was overwritten ~40 lines after being read and had no effect.
- Spurious "format ignored in streaming mode" warning.
- `-crash`, `-profile`, and `-longpress` CLI flags — moved to env vars (`ZEE_CRASH=1`, `ZEE_PPROF=:6060`, `ZEE_LONGPRESS_DURATION=350ms`). Dev/tuning knobs, no need to clutter `-h`.

### Added
- `ZEE_FAKE_STREAM=1` env var to make the fake transcriber report a streaming model (used by integration tests that previously relied on `-stream`).

### Fixed
- Release workflow: re-upload `checksums.txt` after appending the DMG SHA256, so `install.sh` can verify the DMG.

## v0.3.6

### Added
Expand Down
23 changes: 13 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ make app # build macOS DMG (binary + icns + .app bu
GROQ_API_KEY=xxx ./zee # run (hold Ctrl+Shift+Space to record)
```

## Install (macOS DMG)
## Install

1. `make app` produces `Zee-<version>.dmg`
2. Open the DMG and drag `Zee.app` to `/Applications`
3. On first launch, grant **Microphone** and **Accessibility** permissions to the terminal or Zee.app via System Settings > Privacy & Security
4. Set `GROQ_API_KEY` in your shell profile or launch via: `GROQ_API_KEY=xxx open /Applications/Zee.app`
End users install via:

```bash
curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash
```

`install.sh` downloads the latest DMG from GitHub Releases, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and runs `xattr -cr` to clear quarantine. Permissions (Microphone, Accessibility) are still granted lazily by macOS TCC on first use — installers cannot pre-grant them.

Local dev DMG: `make app` produces `Zee-<version>.dmg`; drag to `/Applications`.

## Testing

Expand All @@ -32,7 +37,6 @@ make benchmark WAV=file.wav RUNS=5

## Flags

- `-stream` - enable streaming transcription (Deepgram only)
- `-debug` - enable diagnostic logging (default: false)
- `-debug-transcribe` - enable transcription text logging (requires `-debug`)
- `-format <mp3@16|mp3@64|flac>` - audio format (default: mp3@16)
Expand Down Expand Up @@ -106,12 +110,11 @@ git tag v0.3.0 && git push origin v0.3.0 # triggers CI release
CI (`.github/workflows/release.yml`) does:
1. Verify tag is on `main`
2. GoReleaser builds arm64 + amd64 binaries, universal binary, tar.gz archives, checksums, GitHub release
3. GoReleaser pushes Homebrew formula to `sumerc/homebrew-tap`
4. Post-step creates DMG from universal binary and uploads to release
3. Post-step creates DMG from universal binary, appends its SHA256 to `checksums.txt`, uploads to release

Requires `ZEE_RELEASE_TOKEN` repo secret (fine-grained PAT with Contents read/write on `zee` + `homebrew-tap`).
Requires `ZEE_RELEASE_TOKEN` repo secret (fine-grained PAT with Contents read/write on `zee`).

Users install via: `brew install sumerc/tap/zee`
Users install via the one-liner in README (`install.sh` fetches the DMG and verifies the checksum).

## Packaging

Expand Down
37 changes: 30 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build build-linux-amd64 build-linux-arm64 test test-integration benchmark integration-test clean release icns app
.PHONY: build build-linux-amd64 build-linux-arm64 test test-integration benchmark integration-test clean bump-version release icns app

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")

Expand Down Expand Up @@ -40,10 +40,33 @@ app: build icns
clean:
rm -f zee Zee-*.dmg

bump-version:
@branch=$$(git rev-parse --abbrev-ref HEAD); \
if [ "$$branch" != "main" ]; then echo "ERROR: must be on main branch" && exit 1; fi; \
ver="$(VER)"; \
if [ -z "$$ver" ]; then echo "usage: make bump-version VER=0.3.7" && exit 1; fi; \
latest=$$(git tag --sort=-v:refname | head -1); \
claude -p "Look at the git log from tag $$latest to HEAD. Write a CHANGELOG.md entry for Zee version v$$ver in this exact format: ## v$$ver, blank line, then markdown groups using ### Added, ### Changed, ### Fixed, ### Removed only when relevant, with concise '- ' bullets. Skip merge commits and CI-only changes. Output ONLY the changelog entry, no code fences." > /tmp/zee-changelog-entry; \
echo "" >> /tmp/zee-changelog-entry; \
sed -i '' '/^## Unreleased/r /tmp/zee-changelog-entry' CHANGELOG.md; \
rm -f /tmp/zee-changelog-entry; \
echo "CHANGELOG.md updated — review and edit as needed"

release:
@latest=$$(gh release view --json tagName -q .tagName 2>/dev/null || echo "none"); \
echo "latest release: $$latest"; \
read -p "new version (e.g. 0.2.0): " ver; \
test -n "$$ver" || (echo "aborted" && exit 1); \
echo "tagging v$$ver and pushing..."; \
git tag "v$$ver" && git push origin "v$$ver"
@branch=$$(git rev-parse --abbrev-ref HEAD); \
if [ "$$branch" != "main" ]; then echo "ERROR: must be on main branch" && exit 1; fi; \
ver="$(VER)"; \
if [ -z "$$ver" ]; then echo "usage: make release VER=0.3.7" && exit 1; fi; \
grep -q "^## v$$ver$$" CHANGELOG.md || (echo "ERROR: v$$ver missing from CHANGELOG.md — run make bump-version first" && exit 1); \
git diff --quiet || (echo "ERROR: working tree has uncommitted changes" && exit 1); \
git diff --cached --quiet || (echo "ERROR: index has staged changes" && exit 1); \
notes=$$(awk "/^## v$$ver$$/{found=1; next} /^## /{if(found) exit} found{print}" CHANGELOG.md | sed '/^$$/d'); \
echo ""; \
echo "v$$ver Release Notes:"; \
echo ""; \
echo "$$notes"; \
echo ""; \
read -p "create and push tag v$$ver? [y/N] " confirm; \
case "$$confirm" in y|Y) ;; *) echo "aborted" && exit 1;; esac; \
git tag -a "v$$ver" -m "v$$ver"; \
git push origin "v$$ver"
33 changes: 11 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

- **System tray app** — lives in the menu bar. Switch microphones, transcription providers, and languages from the tray menu. Dynamic icons show recording and warning states.
- **Two recording modes** — push-to-talk (hold hotkey) or tap-to-toggle (tap to start/stop).
- **Real-time streaming** — with `-stream`, words appear as you speak and auto-paste into the focused window incrementally. Powered by Deepgram's WebSocket API.
- **Real-time streaming** — when a streaming-capable model is selected (e.g. Deepgram Nova-3), words appear as you speak and auto-paste into the focused window incrementally.
- **Fast batch mode** — HTTP keep-alive, TLS connection reuse, pre-warmed connections, streaming encoder runs during recording (not after). Typical key-release to clipboard: under 500ms.
- **Auto-paste** — transcribed text goes straight to clipboard and pastes into the active window. In streaming mode, each new phrase pastes as it arrives.
- **Silence detection** — VAD-based voice activity detection warns when no speech is heard. In streaming mode, auto-closes recording after 30 seconds of silence.
Expand All @@ -31,45 +31,36 @@

## Install

### Homebrew (recommended)
### One-liner (recommended)

```bash
brew install --cask sumerc/tap/zee
curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash
```

Installs `Zee.app` to `/Applications`. Launch from Spotlight or the Applications folder.
Downloads the latest DMG, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and clears the quarantine attribute. Pin a version with `VERSION=vX.Y.Z bash`.

On first launch, macOS may warn that the app can't be verified. Run once to clear it:

```bash
xattr -cr /Applications/Zee.app
```

### macOS (DMG)
### Manual DMG

1. Download `Zee-<version>.dmg` from the [latest release](https://github.com/sumerc/zee/releases/latest)
2. Open the DMG and drag **Zee.app** to **Applications**
3. Clear quarantine: `xattr -cr /Applications/Zee.app`

### CLI binary

For terminal usage, install the formula or download directly:

```bash
brew install sumerc/tap/zee # installs to /opt/homebrew/bin/zee
```
For terminal usage:

```bash
# or download manually — Apple Silicon
# Apple Silicon
curl -L https://github.com/sumerc/zee/releases/latest/download/zee_darwin_arm64.tar.gz | tar xz

# Intel
curl -L https://github.com/sumerc/zee/releases/latest/download/zee_darwin_amd64.tar.gz | tar xz
```

```bash
GROQ_API_KEY=xxx zee # Groq Whisper
OPENAI_API_KEY=xxx zee -stream # Deepgram streaming
zee -debug # with diagnostic logging
GROQ_API_KEY=xxx ./zee # Groq Whisper
DEEPGRAM_API_KEY=xxx ./zee # Deepgram (streaming auto-enabled when a streaming model is selected from the tray)
./zee -debug # with diagnostic logging
```

> **Note:** When running from a terminal, macOS permissions (Microphone, Accessibility) are granted to the **terminal app** (e.g. Ghostty, iTerm2, Terminal), not to zee itself.
Expand All @@ -92,7 +83,6 @@ export OPENAI_API_KEY=your_key # batch mode (OpenAI Whisper)
export DEEPGRAM_API_KEY=your_key # streaming mode (Deepgram)
export MISTRAL_API_KEY=your_key # batch mode (Mistral Voxtral)
zee # starts in menu bar, hold Ctrl+Shift+Space to record
zee -stream # words appear as you speak
```

> **Note:** `export` only works in the current terminal session. To make API keys available to `Zee.app` when launched from Spotlight or Applications, use `launchctl`:
Expand Down Expand Up @@ -128,7 +118,6 @@ make benchmark WAV=file.wav RUNS=5 # multiple runs for timing

| Flag | Default | Description |
|------|---------|-------------|
| `-stream` | false | Real-time streaming transcription (Deepgram) |
| `-format` | mp3@16 | Audio format: `mp3@16`, `mp3@64`, or `flac` |
| `-autopaste` | true | Auto-paste into focused window |
| `-setup` | false | Select microphone device |
Expand Down
84 changes: 84 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Zee installer for macOS.
# Usage: curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash
# VERSION=vX.Y.Z bash install.sh
set -euo pipefail

REPO="sumerc/zee"
APP_DIR="/Applications"
TMP="$(mktemp -d)"
MOUNT=""

err() { echo "error: $*" >&2; exit 1; }
log() { echo "==> $*"; }
cleanup() {
[[ -n "$MOUNT" ]] && hdiutil detach "$MOUNT" -quiet >/dev/null 2>&1 || true
rm -rf "$TMP"
}
run_or_sudo() {
"$@" 2>/dev/null || { log "Need sudo: $*"; sudo "$@"; }
}
trap cleanup EXIT

[[ "$(uname -s)" == "Darwin" ]] || err "Zee currently supports macOS only."

VERSION="${VERSION:-}"
if [[ -z "$VERSION" ]]; then
log "Resolving latest release..."
VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| awk -F'"' '/"tag_name"/ {print $4; exit}')"
[[ -n "$VERSION" ]] || err "could not resolve latest version (GitHub API rate limit?). Set VERSION=vX.Y.Z and retry."
fi
log "Installing Zee ${VERSION}"

DMG="Zee-${VERSION}.dmg"
BASE="https://github.com/${REPO}/releases/download/${VERSION}"

log "Downloading ${DMG}..."
curl -fL --progress-bar "${BASE}/${DMG}" -o "${TMP}/${DMG}" \
|| err "download failed: ${BASE}/${DMG}"

log "Verifying checksum..."
curl -fsSL "${BASE}/checksums.txt" -o "${TMP}/checksums.txt" \
|| err "download failed: ${BASE}/checksums.txt"
expected="$(awk -v f="${DMG}" '$2==f {print $1}' "${TMP}/checksums.txt")"
[[ -n "$expected" ]] || err "${DMG} not found in checksums.txt"
actual="$(shasum -a 256 "${TMP}/${DMG}" | awk '{print $1}')"
[[ "$expected" == "$actual" ]] || err "checksum mismatch (expected $expected, got $actual)"
log "Checksum OK"

log "Mounting DMG..."
MOUNT="$(hdiutil attach -nobrowse -readonly -mountrandom /tmp "${TMP}/${DMG}" \
| grep -oE '/(private/tmp|Volumes)/[^[:space:]]+' \
| tail -1)"
[[ -n "$MOUNT" && -d "$MOUNT/Zee.app" ]] || err "Zee.app not found in DMG"

if [[ -d "${APP_DIR}/Zee.app" ]]; then
log "Removing existing ${APP_DIR}/Zee.app"
run_or_sudo rm -rf "${APP_DIR}/Zee.app"
fi

log "Copying Zee.app to ${APP_DIR}..."
run_or_sudo cp -R "$MOUNT/Zee.app" "${APP_DIR}/"

log "Clearing quarantine attribute..."
run_or_sudo xattr -cr "${APP_DIR}/Zee.app"

cat <<EOF

Zee ${VERSION} installed to ${APP_DIR}/Zee.app

Next:
1. Set an API key (at least one):
launchctl setenv GROQ_API_KEY your_key
launchctl setenv OPENAI_API_KEY your_key
launchctl setenv DEEPGRAM_API_KEY your_key
launchctl setenv MISTRAL_API_KEY your_key
(add to ~/.zshrc to persist across logins)

2. Launch Zee from Spotlight or:
open ${APP_DIR}/Zee.app

3. macOS may prompt for Microphone and Accessibility.
Grant both, then hold Ctrl+Shift+Space to record.
EOF
Loading