npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hasna/recordings

v0.5.5

Published

Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI

Readme

@hasna/recordings

Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI

npm License

Install

npm install -g @hasna/recordings

macOS App

Hasna Recordings is a native macOS 26 app with a companion menu bar control. The app opens a compact glass recorder with a microphone/stop/play control and timer. The clock opens searchable history and the gear opens Settings inside the same retained app window. A shared title bar and back navigation keep the recorder, transcripts, and settings together. The recorder uses a compact 224 × 244 point layout; normal interface text uses macOS-sized 11–13 point type. Glass backgrounds use one live native blur per surface, with matching translucent controls. A floating transcription bar shows live words, the microphone waveform, pause/resume, playback, and an Auto-paste switch. Recent pastes show delivery evidence from this session. There is no sidebar or project UI. New app recordings are unassigned; existing recordings and their metadata are preserved.

  • Click Record, or hold the global shortcut (F5 by default).
  • Click the clock or press ⌘L for recordings; select a row to read its transcript. Audio playback is available when its file is retained on this Mac.
  • Press ⇧⌘B for the floating transcription bar, or choose Keep bar visible from the menu bar’s More menu. Its play control opens Recent pastes.
  • Pause/resume from the bar or the recorder’s context menu. Paused microphone samples are excluded from the transcript and recording duration.
  • Turn Auto-paste off to keep transcripts available without typing into another app. Recordings still save through the configured API. Clearing Recent pastes only clears this session’s delivery list; it does not delete recordings.
  • Open Settings (⌘,), then API & Advanced… to configure the service connection, transcription cleanup, shortcuts, and permissions. The input follows the macOS default microphone; audio remains uncompressed 24 kHz PCM.

New installations default to dictation. Question and edit-command detection is optional in Settings; enabling it can add a model request before delivery. For verbatim dictation with the shortest delivery path, keep intent detection off and set Transcription Cleanup to Raw (Off). Settled realtime text can paste while the API save continues in the background. Capture shutdown runs off the UI thread, and the recording panel avoids animated glass so rendering does not stall live transcription.

In Settings → API & Advanced… → General → Recordings API, enter your API URL and service key, then choose Save Connection and Test Connection. There is no compiled-in API hostname. Both a service prefix such as https://api.example.com/recordings and its versioned form https://api.example.com/recordings/v1/ work; requests append the resource to exactly one /v1. The service key stays in macOS Keychain, scoped to the normalized endpoint, and is separate from the OpenAI transcription key.

An explicitly configured launch environment takes precedence over saved connection settings: HASNA_RECORDINGS_API_URL plus the existing Hasna credential chain, or HASNA_RECORDINGS_CLIENT_STORE=sqlite for an intentional local store. The native app passes the connection to its embedded CLI, so recording persistence, history, and deletion use the same API client as the CLI and MCP. No local fallback is selected when an API connection is missing or fails.

The bundle filename is Hasna Recordings.app for both full and menu bar builds. Older unspaced bundles remain discoverable as legacy installations. The managed updater's canonical path is part of its immutable cohort; an existing cohort bound to the old filename requires managed reprovisioning before receiving bundles under the new name. This change does not rename an already installed managed app.

The app embeds a same-version recordings CLI as its data layer, so the CLI, MCP, and app share one store without depending on a possibly stale global CLI installation. Production release installs use a one-time managed bootstrap at /Applications/Hasna Recordings.app; later release updates replace only that app through the installed root-owned broker.

# One-time production bootstrap, run by MDM/root with independently authenticated values:
# The three input files must use canonical paths beneath root-owned, non-writable directories.
sudo packaging/macos/managed_bootstrap.sh \
  --artifact /path/to/Recordings-0.2.13-macos-initial-bootstrap-updater.pkg \
  --manifest /path/to/Recordings-0.2.13-macos-initial-bootstrap.manifest.json \
  --envelope /path/to/Recordings-0.2.13-macos-initial-bootstrap-updater.bootstrap-envelope.json \
  --expected-package-sha256 AUTHENTICATED_PACKAGE_SHA256 \
  --expected-installer-team-id TEAMID1234 \
  --expected-installer-certificate-sha256 AUTHENTICATED_INSTALLER_CERTIFICATE_SHA256

# After bootstrap, install only a signed app artifact through the immutable cohort:
recordings app install \
  --artifact /path/to/Recordings-0.2.13-macos-app-update.zip \
  --manifest /path/to/Recordings-0.2.13-macos-app-update.manifest.json \
  --envelope /path/to/Recordings-0.2.13-macos-app-update.update-envelope.json \
  --manifest-sha256 AUTHENTICATED_MANIFEST_SHA256 \
  --expected-source-sha APPROVED_40_CHARACTER_COMMIT_SHA \
  --expected-version 0.2.13 \
  --expected-hostname station03 \
  --expected-team-id TEAMID1234

recordings app open           # launch it
recordings app status         # show install state
recordings app snapshot       # write ./desktop-snapshot.png for local UI debugging
# From this repository, optionally choosing another output path:
bun run desktop:snapshot -- /tmp/recordings-desktop.png
"/Applications/Hasna Recordings.app/Contents/Helpers/recordings-update-client" status

# Release builds run only as the isolated _recordingsbuild account. Provision these first:
# - /private/var/recordings-build owned by _recordingsbuild, mode 0700, beneath a
#   root-owned non-writable parent;
# - /Library/Application Support/Hasna/Recordings/BuildTrust/isolated-builder-v1
#   as a root-owned non-linked mode-0444 file containing recordings-isolated-builder-v1;
# - an absolute Bun executable, clean source commit, locked dependencies, Swift/Xcode,
#   universal native guard, unlocked Developer ID Application identity, notarytool profile,
#   and 32-byte Ed25519 public/private release-envelope keys (private key owner-only).
cd src/native/Recordings
# Fresh source checkouts must install the locked JavaScript dependencies first:
(cd ../../.. && /absolute/path/to/bun install --frozen-lockfile)

# One-time initial bootstrap. This is the only subtype that accepts an Installer identity;
# it does not accept RECORDINGS_RELEASE_COMPATIBLE_COHORT_MANIFEST and emits no update envelope.
HOME="/private/var/recordings-build/home" \
BUN_EXECUTABLE="/absolute/path/to/bun" \
RECORDINGS_CODESIGN_IDENTITY="Developer ID Application: ..." \
RECORDINGS_INSTALLER_CODESIGN_IDENTITY="Developer ID Installer: ..." \
RECORDINGS_EXPECTED_TEAM_IDENTIFIER="TEAMID1234" \
RECORDINGS_NOTARY_KEYCHAIN_PROFILE="recordings-notary" \
RECORDINGS_RELEASE_SEQUENCE="1" \
RECORDINGS_RELEASE_KEY_EPOCH="1" \
RECORDINGS_RELEASE_ENVELOPE_EXPIRES_AT_UTC="2026-08-01T00:00:00.000Z" \
RECORDINGS_RELEASE_ENVELOPE_PRIVATE_KEY="<path to your private envelope key>" \
RECORDINGS_RELEASE_ENVELOPE_PUBLIC_KEY="/absolute/public/envelope-key.raw" \
  ./build.sh release initial-bootstrap

# After independent review, root-preauthorize the exact emitted cohort for later updates.
COHORT_MANIFEST="/private/var/recordings-build/release-output/Recordings-0.2.13-macos-initial-bootstrap-updater.compatible-cohort.json"
COHORT_DIGEST="$(/usr/bin/awk 'NR == 1 { print $1 }' "${COHORT_MANIFEST}.sha256")"
test "$(/usr/bin/shasum -a 256 "${COHORT_MANIFEST}" | /usr/bin/awk '{ print $1 }')" = "${COHORT_DIGEST}"
sudo /usr/bin/install -d -o root -g wheel -m 0755 \
  "/Library/Application Support/Hasna/Recordings/BuildTrust/compatible-cohorts"
sudo /usr/bin/install -o root -g wheel -m 0444 "${COHORT_MANIFEST}" \
  "/Library/Application Support/Hasna/Recordings/BuildTrust/compatible-cohorts/${COHORT_DIGEST}.json"

# App-only update. Copy the reviewed bootstrap cohort manifest into the root-owned
# compatible-cohorts directory under its exact SHA-256 filename before invoking this.
# No Installer identity, PKG, broker, verifier, bootstrap preflight, marker, or bootstrap
# envelope is used or emitted by this subtype.
HOME="/private/var/recordings-build/home" \
BUN_EXECUTABLE="/absolute/path/to/bun" \
RECORDINGS_CODESIGN_IDENTITY="Developer ID Application: ..." \
RECORDINGS_EXPECTED_TEAM_IDENTIFIER="TEAMID1234" \
RECORDINGS_NOTARY_KEYCHAIN_PROFILE="recordings-notary" \
RECORDINGS_RELEASE_SEQUENCE="2" \
RECORDINGS_RELEASE_KEY_EPOCH="1" \
RECORDINGS_RELEASE_ENVELOPE_EXPIRES_AT_UTC="2026-08-15T00:00:00.000Z" \
RECORDINGS_RELEASE_ENVELOPE_PRIVATE_KEY="<path to your private envelope key>" \
RECORDINGS_RELEASE_ENVELOPE_PUBLIC_KEY="/absolute/public/envelope-key.raw" \
RECORDINGS_RELEASE_COMPATIBLE_COHORT_MANIFEST="/Library/Application Support/Hasna/Recordings/BuildTrust/compatible-cohorts/AUTHENTICATED_COHORT_SHA256.json" \
  ./build.sh release app-update

# Explicit local-only alternative when Developer ID credentials are unavailable.
# Build on a Mac other than the approved target; this does not replace a release.
# The approved targets are declared once in
# scripts/policy/local-only-approved-targets.txt, which the builder, the installer,
# and the artifact tool all read; add a Mac there rather than in any guard:
RECORDINGS_LOCAL_APPROVED_TARGET="station06" \
RECORDINGS_LOCAL_APPROVED_TARGET_IDENTITY_KIND="tailscale_node_id_sha256" \
RECORDINGS_LOCAL_APPROVED_TARGET_IDENTITY_SHA256="AUTHENTICATED_TAILSCALE_NODE_ID_SHA256" \
  BUN_EXECUTABLE="/absolute/path/to/bun" \
  ./build.sh local

# Install only on that exact target, with the immutable manifest digest recorded separately:
recordings app install \
  --artifact /path/to/Recordings-0.2.13-macos-station06-local-only.zip \
  --manifest /path/to/Recordings-0.2.13-macos-station06-local-only.manifest.json \
  --manifest-sha256 AUTHENTICATED_MANIFEST_SHA256 \
  --expected-source-sha APPROVED_40_CHARACTER_COMMIT_SHA \
  --expected-version 0.2.13 \
  --artifact-policy local-only \
  --approved-target station06 \
  --approved-target-identity-kind tailscale_node_id_sha256 \
  --approved-target-identity-sha256 AUTHENTICATED_TAILSCALE_NODE_ID_SHA256 \
  --acknowledge-local-signing-and-permissions \
  --launch

# Reinstalling or repairing an already-installed local-only app additionally needs
# --allow-adhoc-identity-migration. Local-only builds are ad-hoc signed, so every rebuild
# produces a new CDHash; replacing the installed app is therefore a real identity migration
# and the installer refuses it with exit 1 until you approve it once, explicitly. Approving
# it voids the Microphone and Accessibility grants held by the replaced app -- macOS keys
# those to code identity and the installer cannot restore them -- so expect to grant both
# again afterwards. Run `scripts/install_macos_app.sh --help` for the full argument list.
recordings app install \
  --artifact /path/to/Recordings-0.2.13-macos-station06-local-only.zip \
  --manifest /path/to/Recordings-0.2.13-macos-station06-local-only.manifest.json \
  --manifest-sha256 AUTHENTICATED_MANIFEST_SHA256 \
  --expected-source-sha APPROVED_40_CHARACTER_COMMIT_SHA \
  --expected-version 0.2.13 \
  --artifact-policy local-only \
  --approved-target station06 \
  --approved-target-identity-kind tailscale_node_id_sha256 \
  --approved-target-identity-sha256 AUTHENTICATED_TAILSCALE_NODE_ID_SHA256 \
  --acknowledge-local-signing-and-permissions \
  --allow-adhoc-identity-migration \
  --launch
swift test                    # run the native test suite

The production release location is /Applications/Hasna Recordings.app. The managed bootstrap installs one signed/notarized PKG exactly once, including the root broker, no-login verifier, launchd policy, release key, and initial app. That root cohort is intentionally immutable: lifecycle=bootstrap-v1-app-updates-only, root_maintenance_supported=false, and key_rotation_supported=false. Subsequent release envelopes must bind the exact installed broker/verifier cohort, protocol version, and pinned key epoch, and may replace only /Applications/Hasna Recordings.app. A second bootstrap PKG, a broker or verifier mismatch, a broker-protocol incompatibility, or a key-epoch change fails before app activation with unsupported_lifecycle. Root updater maintenance and release-key rotation require a separate managed reprovisioning lifecycle; the current tooling does not run Installer or overwrite root trust as an update fallback.

Managed deployments should prefer a root-owned, root:wheel /Applications directory with mode 0755. The updater intentionally also accepts the macOS-compatible root:admin mode 0775, but that compatibility permits local admin-group actors to race application-namespace operations and force fail-closed recovery or another availability loss. It does not authorize an update or bypass the signed release, code-signing, or audit-token peer checks.

Before Installer runs, the managed bootstrap copies all three release inputs into a root-private snapshot, verifies the out-of-band PKG digest, Gatekeeper decision, Installer certificate and Team ID, expands that exact PKG without installing it, and runs its separately Developer-ID-signed recordings-bootstrap-preflight executable. The preflight validates the Ed25519 signature and expiry plus the package, manifest, app tree, code requirements, protected components, bootstrap marker, key epoch, and installer-certificate bindings. Installer is never used when preflight fails. A retry after a crash may skip Installer only when the complete immutable cohort is already present and the absent, highest-seen, or committed release state exactly matches the same signed bootstrap; partial or conflicting cohort evidence fails closed.

An initial-bootstrap release emits the stapled PKG, its digest and notary evidence, the bootstrap envelope, and a schema-v2 compatible-cohort manifest plus its digest. The release operator must retain that complete bootstrap set as the canonical onboarding artifact for the cohort. New machines are onboarded only with that retained PKG set; a later app-update must never be substituted for it, and rebuilding the same version is not a recovery mechanism. Replacing any root component, Installer certificate, or release key requires a separately approved managed-reprovisioning lifecycle.

Each app-update build requires RECORDINGS_RELEASE_COMPATIBLE_COHORT_MANIFEST to name a root-owned, mode-0444, content-addressed JSON file under /Library/Application Support/Hasna/Recordings/BuildTrust/compatible-cohorts/. Its schema must be exactly version 2 with no additional or missing keys. It authorizes exactly lifecycle bootstrap-v1-app-updates-only, protocol version 1, the current pinned key epoch, root_maintenance_supported=false, and key_rotation_supported=false, together with the finalized stapled cohort PKG, bootstrap marker, Installer certificate, exact release-envelope public-key SHA-256, broker and verifier digests and designated requirements, Team ID, and minimum broker version. Every source-directory ancestor must be canonical, root-owned, and non-writable; the builder validates the content-addressed source once, copies it into its private build directory, and reads only that stable snapshot. The builder refuses a mutable, misnamed, broader, schema-mismatched, key-mismatched, or otherwise incompatible cohort manifest.

Release installation accepts only a finalized ZIP plus its manifest, signed update envelope, and operator-supplied authenticated provenance. The manifest binds the bundle identifier, version, source commit, architectures, pinned Team ID, designated-requirement digest, companion version and hash, complete app-tree hash, archive hash, and trusted signing timestamps. The root broker verifies the signed envelope, immutable cohort, monotonic release state, isolated verifier result, and candidate code identity before transactional activation. Compatible app-only updates preserve the stable signing identity and do not reset Microphone or Accessibility permissions. The updater never calls tccutil reset, clears quarantine, re-signs an artifact, or builds on the target machine. A private, fsynced root journal and anti-rollback state recover interrupted app replacement before the next install attempt.

The explicit local-only development path remains separate and installs ~/Applications/Hasna Recordings.app; it never provisions or imitates the production root cohort. For a station-specific deployment, pass --expected-hostname so the installer proves the live short hostname before taking a lock or mutating state while the release artifact itself remains fleet-distributable. Obtain AUTHENTICATED_MANIFEST_SHA256 from independently authenticated release evidence (for example, signed release metadata or the reviewed release ledger), never by hashing the co-delivered manifest on the target. The release build prints the manifest digest for that separate recording step. Release mode runs the native Swift test suite before compiling and rechecks the clean pinned source commit before provenance and finalization.

An unattended build is valid only when the Developer ID private key and notarization profile are already provisioned, unlocked, and authorized on the non-target builder. Tooling does not bypass a Keychain approval prompt or manufacture credential authority. Likewise, SSH runtime smoke binds evidence to the exact executable path but cannot prove foreground/key-window behavior; final UI acceptance requires a logged-in console session or equivalent trusted GUI automation.

The local build mode is an explicit, target-scoped exception. It still requires a clean source commit, an immutable ZIP and manifest, matching app/helper architectures and hashes, consistent ad-hoc signatures, transactional state backup and rollback, and exact-path postactivation probes. It is intentionally marked local_only and non_notarized, binds only SHA-256 digests of the approved target and non-target builder identities, never runs notarization or Gatekeeper release checks, and cannot be installed without matching the live Mac name and acknowledging that the changed signing identity can require manual Microphone or Accessibility reauthorization. For new artifacts, use tailscale_node_id_sha256: the authenticated operator input is the SHA-256 of the exact target's Tailscale node ID. The non-target builder separately reads and hashes its own live online Self.ID, records builder_identity_kind=tailscale_node_id_sha256, and requires that same-namespace digest to differ from the target digest. Before locking or mutating local state, the installer ignores caller PATH and accepts only the canonical standard app at /Applications/Tailscale.app. Using pinned macOS system tools and a clean environment, it cryptographically verifies the complete app and its CLI against Tailscale's official TeamIdentifier (W5364U7YZB) and bundle identifier (io.tailscale.ipn.macsys). It then copies the complete app into a mode-700 installer-owned temporary directory, re-verifies the copied app and CLI, and reads tailscale status --json only from that private snapshot after one final signature check. The mutable /Applications path is never executed, caller status environment is not inherited, and all snapshot paths are removed by normal installer cleanup. The builder applies the same checks in its private build directory and removes the snapshot with the rest of the build workspace. The status parser then requires online Self whose hostname equals the caller's --expected-hostname — the approved target when the installer verifies the target, and the builder's own host when the builder verifies itself, which build.sh requires to differ from the target — requires the single nonempty Self.ID to contain no whitespace or NUL, hashes its exact decoded bytes without a newline, and compares the digest. Neither raw node ID is written to the manifest, build log, or installer log. Older schema-v3 artifacts without an identity-kind field remain compatible as hardware_uuid_sha256; that kind is retained only for backward compatibility. The installer never resets or inspects TCC and never clears quarantine in either policy.

Requires macOS 26+; source builds also require a Swift toolchain (Xcode or Command Line Tools). Release packaging also requires the universal descriptor-guard prebuild at scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node. Build it on a trusted Mac with bun run build:native-fs-guard; target installers never compile native recovery code and fail before creating install state when the pinned prebuild is missing or unsafe. Set the OpenAI API key in Settings or via recordings config; transcription/enhancement use it.

The app's Transcription Cleanup setting controls the same post-processing pipeline as the CLI and MCP server. Use Raw to keep verbatim text only, Auto to clean up only when trigger phrases or instruction patterns are detected, or Always to run the transcriber cleanup prompt for every recording. Global cleanup instructions can be set in Settings. The native app applies only those global instructions.

The native app uses OpenAI realtime transcription for the stop-and-paste path: settled gpt-realtime-whisper text is saved and pasted immediately, while full-file gpt-4o-transcribe remains the bounded quality fallback when realtime is empty, unsettled, or cannot be saved. Raw and processed transcript fields are still stored separately, so cleanup instructions never replace the verbatim transcript.

What the app reads to confirm a paste

Posting a Cmd-V keystroke returns no delivery receipt — CGEvent.post returns Void — so the only way to know whether a transcript actually landed is to look. Around each paste the app therefore reads the text value of whatever field is focused in the target app, via Accessibility, once before the keystroke and once after, and compares them.

State this plainly because it is a real change in what a dictation app can see:

  • The read covers the focused field's full value and its current selection, not only the pasted fragment, so text you did not dictate is inside the app's process during the comparison.
  • It is never logged and never persisted. The comparison happens in memory and only its verdict (pasted / not observed / unverified plus a reason) reaches the log.
  • Fields longer than 20,000 characters are reported unverifiable rather than copied and scanned.
  • A field that publishes no Accessibility value — terminals, canvas editors, some Electron apps — is reported as unverified, never as a success.
  • Reading requires the Accessibility permission the app already needs to post the keystroke. No additional permission is requested, which is exactly why this is worth writing down.

One known limit, in the safe direction: pasting text identical to the selection it replaces leaves the field's value and the occurrence count unchanged, so a genuinely successful paste is reported as not observed. The app under-claims rather than over-claims.

CLI Usage

recordings --help
  • recordings record
  • recordings transcribe <file>
  • recordings transcribe <file> --stream
  • recordings transcribe <file> --prompt "DALL-E, Hasna, gpt-4o"
  • recordings transcribe <file> --transcriber-prompt "Clean up punctuation only" --post-processing always
  • recordings save-text --text-file transcript.txt --source realtime_fast_path
  • recordings rewrite <text> --instruction "<instruction>"
  • recordings list --limit 20 --cursor 0
  • recordings list --verbose
  • recordings show <id> / recordings inspect <id>
  • recordings search <query> --limit 20 --cursor 0
  • recordings delete <id>
  • recordings stats

Compact Output

Agent-facing list commands are compact by default. Terminal output shows bounded rows, short text previews, totals, pagination cursors, and the next detail command instead of dumping full recording objects.

recordings list                 # compact rows, default limit 20
recordings list --cursor 20     # next page
recordings list --verbose       # more metadata, still no full transcript dump
recordings show <id>            # full recording detail
recordings inspect <id>         # alias for show
recordings --json list -n 100   # machine-readable records for integrations

Terminal list output is capped at 50 rows. JSON list output preserves complete recording objects and accepts up to 500 rows per page.

Transcription Prompts

Recordings separates speech-to-text context from post-transcription cleanup:

  • --prompt / transcription_prompt is passed to the OpenAI audio transcription request as vocabulary or context. Use it for names, acronyms, technical terms, or preceding segment context.
  • --transcriber-prompt / transcriber_prompt is used after raw transcription by the text transcriber pipeline. Use it for cleanup, formatting, tone, summaries, or transformations.
  • --post-processing off|auto|always controls whether cleanup runs. --no-enhance is a compatibility alias for off.

Examples:

# Verbatim dictation, no cleanup
recordings transcribe meeting.wav --post-processing off

# Better STT recognition for names and acronyms, still verbatim
recordings transcribe demo.wav --prompt "Hasna, Alumia, DALL-E, gpt-4o"

# Always clean up punctuation and paragraphs after raw transcription
recordings transcribe note.wav \
  --post-processing always \
  --transcriber-prompt "Fix punctuation and paragraph breaks. Preserve the speaker's meaning."

# Auto mode only cleans up when the transcript asks for it, such as "say it better"
recordings transcribe draft.wav --post-processing auto

Persistent config can be stored in ~/.hasna/recordings/config.json or a project-local .recordings/config.json:

{
  "transcription_prompt": "Hasna, Alumia, gpt-4o",
  "transcriber_prompt": "Clean up grammar and format as concise Markdown notes.",
  "post_processing_mode": "always",
  "enhancement_model": "gpt-4o"
}

Environment overrides are also supported:

export RECORDINGS_TRANSCRIPTION_PROMPT="Hasna, DALL-E, gpt-4o"
export RECORDINGS_TRANSCRIBER_PROMPT="Format as polished meeting notes"
export RECORDINGS_POST_PROCESSING_MODE=always
export RECORDINGS_TRANSCRIBER_MODEL=gpt-4o
export RECORDINGS_MODEL=gpt-4o-transcribe
export RECORDINGS_REALTIME_SESSION_MODEL=gpt-realtime
export RECORDINGS_REALTIME_TRANSCRIPTION_MODEL=gpt-realtime-whisper

RECORDINGS_MODEL is the bounded file-transcription model. Realtime session and realtime transcription models are separate slots; recordings check --json reports all three and includes config_warnings if a model is placed in the wrong slot.

MCP Server

recordings-mcp

HTTP transport

recordings-mcp --http              # default port 8873
MCP_HTTP=1 MCP_HTTP_PORT=8873 recordings-mcp

Endpoints: GET /health{"status":"ok","name":"recordings"}, MCP at /mcp.

HTTP API (recordings-serve)

recordings-serve is the HTTP API. Its data backend is selected by the environment: a HASNA_RECORDINGS_DATABASE_URL (or RECORDINGS_DATABASE_URL, or DATABASE_URL) selects postgresql; any other environment serves from the on-box sqlite file. On postgresql the process reads/writes that database directly with API-key auth via @hasna/contracts.

The CLI and MCP client have exactly two stores and never open Postgres — they read the on-box sqlite file only when the deliberate unhosted opt-in HASNA_RECORDINGS_LOCAL=1 (alias RECORDINGS_LOCAL=1) selects it, or they call the server's /v1 HTTP API. Which one it is is decided by the ONE credential resolver in @hasna/contracts (resolved fresh on every call):

| tier | source | | ---- | ------ | | 1 | explicit --api-key / --profile, or HASNA_RECORDINGS_API_KEY_OVERRIDE / HASNA_PROFILE / HASNA_RECORDINGS_API_KEY_REF | | 2 | the macOS Keychain item hasna.credentials.recordings.api-key (account HASNA_STATION, else hostname -s, else $USER) | | 3 | ~/.hasna/recordings/config/credentials (owner-only 0400/0600; HASNA_HOME / HASNA_CONFIG_HOME move the root) | | 4 | HASNA_RECORDINGS_API_KEY — a legitimate tier, no deprecation notice |

The authority follows the same ladder — HASNA_RECORDINGS_API_URL, the Keychain api-url item, the credentials file — and otherwise DEFAULTS to the fleet gateway https://api.hasna.com/recordings, so a key alone is a complete configuration. Retired inputs are gone: nothing reads the old fleet-env or cloud credential directories, ~/.config/hasna or $XDG_CONFIG_HOME, and no *_MODE / *_STORAGE_MODE / *_CLIENT_STORE switch exists (the old HASNA_RECORDINGS_CLIENT_STORE variable selects nothing any more).

Fail closed. With no local opt-in and no credential, the CLI and MCP exit non-zero with one REMOTE_API_* line naming every tier that was consulted; there is no SQLite fallback and no local-fallback event. The on-box file is reachable ONLY through the explicit opt-in above, and an opted-in run reads neither the Keychain nor any credential file. RECORDINGS_API_KEY remains the OpenAI transcription-key override only — it is carved out of the resolver environment and never selects or fails client transport.

The same gate runs before anything else connects: recordings-mcp refuses to answer initialize (exit 1, first stderr line ERROR: REMOTE_API_…) when no credential resolves and no opt-in is set, and recordings check renders ✗ Active store: none — fail-closed (REMOTE_API_…); <db path> is present but NOT opened (or absent) and exits non-zero instead of displaying the on-box file as the live store. With the opt-in set, recordings-mcp prints one recordings: LOCAL mode line on stderr before serving the on-box store.

Native Settings saves new OpenAI transcription keys in macOS Keychain under service hasna.credentials.openai.api-key, account openai/api_key. Finder launches read that entry and pass it to the embedded helper in memory. Use RECORDINGS_OPENAI_API_KEY for an explicit provider-key override, separate from the Hasna service credential. The CLI no longer walks ~/.secrets/**/*.env for the OpenAI key (retired in the #1720 validation wave): the declared stores are the env vars above, the config file, and the macOS Keychain entry.

recordings-serve --port 8874          # start the API
recordings-serve migrate              # apply the cloud schema, then exit

Service surface (unauthenticated): GET /health, GET /ready, GET /version (each returns {status, version, mode}), and GET /openapi.json (the OpenAPI 3.1 document the SDK is generated from).

The HTTP server allows 12,000 requests per minute per network peer, per process, by default. Set HASNA_RECORDINGS_RATE_LIMIT_MAX to an integer from 1 to 1000000 to change the finite budget. The legacy RECORDINGS_RATE_LIMIT_MAX remains supported; the canonical variable takes precedence. An absent or blank value uses the default, and invalid values refuse startup. All routes count, including health probes and unauthenticated requests, except OPTIONS. Exceeding the budget returns 429 with Retry-After in seconds; authentication requirements remain unchanged.

With proxy trust off, callers behind a proxy share its bucket. Only enable RECORDINGS_TRUST_PROXY=1 when ingress is restricted to proxies that overwrite forwarding headers. RECORDINGS_TRUSTED_PROXIES resolves the forwarded chain; it is not a socket access-control list. A larger allowance does not prove sustainable throughput or change database, gateway, or provider capacity.

Versioned API (/v1/*, API-key auth via x-api-key or Authorization: Bearer):

| Method | Path | Scope | | ------ | ---- | ----- | | GET/POST | /v1/recordings | recordings:read / recordings:write | | GET/DELETE | /v1/recordings/:id | recordings:read / recordings:write | | GET | /v1/stats | recordings:read | | GET/POST | /v1/agents · GET /v1/agents/:id | recordings:read / recordings:write | | GET/POST | /v1/projects · GET /v1/projects/:id | recordings:read / recordings:write |

Env: HASNA_RECORDINGS_DATABASE_URL (PostgreSQL DSN — selects the postgresql backend) and HASNA_RECORDINGS_API_SIGNING_KEY (HMAC signing secret for API-key auth).

Production two-role deploy contract

The ECS deploy uses TWO database roles and TWO DSNs (the migrate one-shot resolves HASNA_RECORDINGS_MIGRATE_DATABASE_URL / RECORDINGS_MIGRATE_DATABASE_URL, falling back to the runtime DSN):

  • Migration / owner role (e.g. recordings_owner) — runs the one-shot migrate task and OWNS the schema. The task definition's HASNA_RECORDINGS_MIGRATE_DATABASE_URL secret must point at this role's DSN; the ECS execution role needs secretsmanager:GetSecretValue on that secret.

  • Runtime role (e.g. recordings_app) — the DATABASE_URL the serve process reads. It must be STRICTLY DML-only; recordings-serve /ready (and the migrate verb, when a dedicated migration DSN is configured) enforce the least-privilege posture contract: no table/sequence ownership, no CREATE on any schema, no TEMPORARY on the database, and exactly these grants on the public schema:

    | table | grants | | --- | --- | | recordings | SELECT, INSERT, DELETE | | recording_tags | SELECT, INSERT | | agents | SELECT, INSERT, UPDATE | | projects | SELECT, INSERT, UPDATE | | feedback | INSERT | | api_keys | SELECT | | recording_idempotency | SELECT, INSERT |

    plus USAGE on schema public. A runtime role that owns tables (for example because the migrate task ran DDL with the runtime DSN) makes /ready return 503 {"error":"dependency unavailable"} — the database is healthy; the role posture is not. Remediate by re-owning the tables and sequences to the owner role and granting the DML set above.

The table owner must also retain the privileges used by PostgreSQL's foreign key cleanup: SELECT/UPDATE on recording_idempotency.recording_id and SELECT/DELETE on recording_tags. Revoking the owner's DML privileges can make recording deletion fail even when the runtime grants are correct. Readiness checks both owner cleanup permissions. Restore these grants to the affected table's owner; the runtime role remains SELECT/INSERT on both tables.

SDK

The typed /v1 client is generated from the serve OpenAPI document (bun run generate:sdk). The resolver-backed factory resolves the credential and authority through the same @hasna/contracts chain as the CLI, fresh on every request, so a key rotation heals a client held open for hours:

import { createRecordingsV1Client } from "@hasna/recordings/sdk";

const client = createRecordingsV1Client();       // resolves through the chain
const { recordings } = await client.listRecordings({ limit: 20 });

An explicit baseUrl pins the authority: with no apiKey beside it the client sends NO credential at all (the ambient chain is never consulted), and with one it sends exactly that key, every request. The unhosted local recordings-serve (http://localhost:8874) is reachable only under HASNA_RECORDINGS_LOCAL=1 and prints one line on stderr naming the local transport; every other refusal throws RECORDINGS_CREDENTIAL_MISSING. The raw generated constructor new RecordingsV1Client({ baseUrl, apiKey }) still works for explicit configurations.

Useful agent tools include recordings_status for safe service/config diagnostics, transcribe_audio, save_recording, list_recordings, search_recordings, register_agent, heartbeat, and set_focus.

MCP list_recordings and search_recordings are compact by default. Compact output is capped at 50 rows, full=true metadata rows are capped at 10, previews remain bounded, and results include next-cursor hints. Use get_recording { id } for full transcript details.

For MCP, transcribe_audio accepts transcription_prompt (or legacy prompt) for STT context, transcriber_prompt for cleanup instructions, and post_processing_mode with off, auto, or always. Tool results preserve raw_text and return processed_text only when post-processing actually produced enhanced output.

Releasing

Apply the applicable Changesets in a release worktree first. package.json is the version authority; src/version.ts derives its value at runtime. Synchronize both native bundle version fields in src/native/Recordings/RecordingsLib/Info.plist and regenerate the SDK after the Changesets bump:

bun run version:set 0.4.0   # use the version selected by Changesets
bun run generate:sdk        # restamps the generated SDK's header
bun run version:check       # exits 1 if any site disagrees with package.json

The SDK version stamp is generated, not written: src/server/openapi.ts stamps VERSION into the OpenAPI document and bun run generate:sdk bakes it into the // Source: … header of src/sdk/v1.generated.ts. version:set leaves that file alone — the generator owns it, and patching the stamp by hand would hide real regeneration drift — so regenerate after every bump. src/__tests__/version-site-guard.test.ts fails when the committed header disagrees with package.json.

package.json is the authority because scripts/build_companion_cli.sh compares the compiled CLI's --version against it and exits 1 on a mismatch. That abort propagates through src/native/Recordings/build.sh (set -euo pipefail), so a partial bump does not just fail an assertion -- the native app cannot be built at all, and the whole native-app-companion-contract suite aborts on its first test.

Three things enforce it. The repo-root .github/workflows/ci.yml runs the whole TypeScript suite on every push through the turbo build-test job (this app's test script), which is what makes the two guards below actually block a branch rather than wait for someone to run them locally. prepack runs build:native-fs-guard first (the fail-closed macOS gate), then version:check, so a partial bump stops before the build rather than deep inside it. And prepublishOnly runs the complete gated release suite, which covers the sites through src/__tests__/native-bundle-version.test.ts and src/__tests__/version-site-guard.test.ts.

The Swift side compiles in CI on every push through the repo-root .github/workflows/recordings-macos.yml (the native compile gate, bun run verify:ci-native). Runtime behaviour of the Swift half is not covered on the headless runner: no reachable machine currently runs the Swift suite with TCC grants, so a version claim about the app bundle is only as verified as the last macOS build.

Data Directory

Data is stored in ~/.hasna/recordings/.

Audio artifact upload (S3 via the artifact kit)

On recordings record / recordings transcribe, the audio is uploaded at creation as a content-addressed object and the row records audio_object_key, audio_sha256 and audio_bytes (the local audio_path stays as provenance). Without configuration, nothing changes: audio remains local-only.

Environment:

  • HASNA_RECORDINGS_S3_BUCKET (fallback RECORDINGS_S3_BUCKET) — the bucket to upload into; unset/empty keeps the historical local-only behaviour.
  • HASNA_RECORDINGS_S3_PREFIX (fallback RECORDINGS_S3_PREFIX) — object-key prefix inside the bucket; defaults to recordings, so keys look like recordings/<recording_id>/<sha256>.<ext>.
  • RECORDINGS_S3_REGION (fallback AWS_REGION, then us-east-1) — the region for the S3 client. Credentials are never read by the app: the AWS SDK resolves them from the ambient environment or instance role at send time.

Upload failures never lose a recording: the row is still created and the warning is logged, matching the fail-soft contract of the app-side fix.

License

Apache-2.0 -- see LICENSE

Reusing the native recorder

The Swift package at src/native/Recordings exports the RecordingsLib library. A separate macOS application can depend on that package and reuse RecordingEngine for real PCM capture, pause, meters, recording state, clipboard and verified paste:

import RecordingsLib

let configuration = try RecordingEngineConfiguration(
    isolatedHomePath: candidateStateHome.path,
    preferencesSuiteName: "com.example.recorder.preferences"
)
let engine = RecordingEngine(configuration: configuration, transcriptionProvider: provider)

The isolated initializer never installs global shortcuts or fn monitors and does not read the legacy provider credentials, service URL, launch environment or CLI. It rejects the installed recorder's home and preferences suite. Its audio and logs live below <isolatedHomePath>/.hasna/recordings; preferences use only the named suite. The original RecordingEngine(homePath:installsGlobalHandlers:) initializer retains the legacy application's behavior.

Implement RecordingTranscriptionProvider.makeSession(configuration:onPartialTranscript:) to create one session per recording. Construction must return promptly. Its appendPCM method receives ordered 24 kHz mono PCM16LE chunks during recording; paused samples are excluded. Publish partial callbacks as the complete current transcript. finish receives the finalized WAV URL, duration, language and capture ID, and returns RecordingProviderResult. cancel must safely interrupt a pending connection or finish; any in-flight append must be ignored after cancellation. Providers own their bounded networking queues and credential namespace. A local file recognizer may ignore streaming chunks and recognize the WAV at finish.

Completed results appear in recentTranscriptions with captureID and audioURL before automatic paste starts, allowing the consumer to persist them asynchronously. This publication is not a storage acknowledgement; persistedRecordingRevision continues to describe only legacy CLI persistence. Call cancelRecording() to discard capture or cancel an isolated provider that is still finalizing.

Completed paste transactions publish RecentPaste.captureID and the typed deliveryStatus (confirmed, unconfirmed, or notDelivered). A posted keystroke is never a confirmed delivery. Pass a captureID to pasteIntoFrontApp when manually pasting a saved recording. Early permission or target failures remain visible in the engine status without a completed transaction receipt.

Native dependency receipts

An npm archive includes the complete Swift package at src/native/Recordings, including RecordingsLib, its resources, updater targets, and resolved dependency pins. An application can extract the verified archive into its dependency cache and use that Swift package as a local SwiftPM dependency. This is consumption of published package bytes; a development checkout alone is not release evidence.

Create a sidecar receipt for the exact reviewed npm archive after committing the release metadata. The helper checks the shipped native bytes and package version against the specified public Git revision. It does not extract into the working app, access credentials, install software, or publish anything:

python3 scripts/native-core-receipt.py /path/to/recordings-0.4.0.tgz \
  --source-revision <full-public-release-commit> \
  --repository /path/to/hasna-apps \
  --output /path/to/recordings-0.4.0.prepared.json

After the separately authorized npm publication, run the same command with --verify-registry and a new output filename. That explicit read-only step checks the version and integrity in public registry metadata, downloads the registry archive, and requires byte equality with the reviewed archive. It never upgrades a prepared receipt merely because a version exists. Existing receipt files are never overwritten.

The external JSON format has schemaVersion: 1 and kind: "hasna.recordings.native-core". It records:

  • package: exact npm name and version.
  • source: public repository URL and full verified Git revision.
  • distribution: status: "prepared", or status: "published" with registry authority and verification time after the actual registry archive matches.
  • archive: fixed npm URL, byte count, SHA-256, and npm SHA-512 integrity.
  • native: package path, RecordingsLib product, Swift 6.2/macOS 26 requirements, and each shipped native file's relative path, mode, byte count, and SHA-256. treeSHA256 hashes UTF-8 lines sorted by path, each formatted as path + NUL + octalMode + NUL + decimalBytes + NUL + sha256 + LF.

A receipt is integrity evidence, not a signature or permission to release. A consumer must pin the reviewed receipt digest through its own trusted release configuration, require distribution.status == "published", and compare the archive and extracted source bytes before building. Co-delivered metadata must not supply its own trusted expected digest. The receipt stays outside the npm archive so its archive hash has no circular dependency. Notarization, signing, and end-user acceptance of the consuming application remain separate checks.