Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d137590
docs(spec): voice2note → CrickNote classify-and-route integration
hele211 Jun 18, 2026
2852cd3
docs(spec): r2 — harden integration against data-loss failure modes
hele211 Jun 18, 2026
5856b24
docs(spec): r3 — write-once (drop hash-guard TOCTOU), crash-safe state
hele211 Jun 18, 2026
55c634b
docs(spec): r4 — approved-pending fixes (per-task markers, adopt veri…
hele211 Jun 18, 2026
6cae714
docs(plan): phase-1 voice2note implementation plan (TDD, 10 tasks)
hele211 Jun 18, 2026
7ba6e67
feat(routing): recording-type disposition + ambiguity rule
hele211 Jun 18, 2026
bde3908
feat(yamllite): stdlib YAML double-quoted scalar escaper
hele211 Jun 18, 2026
11af119
feat(lock): mandatory fcntl single-instance lock
hele211 Jun 18, 2026
86d0549
feat(analyze): classify recording_type + confidence
hele211 Jun 18, 2026
d0ee778
feat(notes): deterministic path + frontmatter render, no checkboxes/d…
hele211 Jun 18, 2026
a4aed1e
fix(notes): quote recorded datetime; test(analyze): end-to-end confid…
hele211 Jun 18, 2026
dedfa84
feat(state): v2 source_id map, migration, atomic save
hele211 Jun 18, 2026
d0a6c3b
feat(vault_writer): cricknote CLI write bridge + adopt-check fs helpers
hele211 Jun 18, 2026
1c7971a
fix(state,vault_writer): unique temp file + cleanup; guard read_front…
hele211 Jun 18, 2026
fc21d25
feat(config): explicit USE_CRICKNOTE, reldir validation, gated vault-…
hele211 Jun 18, 2026
765e193
feat(run): create-once flow under lock; adopt-with-verify; transcript…
hele211 Jun 18, 2026
92d513c
refactor(run): thread all config through cfg; test fallback records s…
hele211 Jun 18, 2026
0fb1161
fix: hard-fail on missing cricknote CLI; unescape frontmatter; drop d…
hele211 Jun 18, 2026
2636dfd
docs(plan): Phase 3 hardening/docs plan (review round 2 revisions)
hele211 Jun 21, 2026
ad876c0
docs(plan): Phase 3 plan — Codex-approved (review rounds 3-4)
hele211 Jun 21, 2026
e116ee0
feat(install): wire cricknote plist keys + safe discovery (Task 1)
hele211 Jun 21, 2026
afe7da4
docs(config): refresh example env for cricknote integration (Task 2)
hele211 Jun 21, 2026
9874c4c
docs(config): correct CONFIDENCE_MIN description (Task 2 fixup)
hele211 Jun 21, 2026
f861f5a
docs(readme): current note contract + cricknote integration (Task 3)
hele211 Jun 21, 2026
cc1418c
test(smoke): isolated cricknote integration smoke (Task 5)
hele211 Jun 21, 2026
87aaa2a
test(smoke): fire assertion failure messages; printf in fail() (Task …
hele211 Jun 21, 2026
32e72fc
Merge feat/cricknote-voice-phase3: Phase 3 hardening/docs (Tasks 1-6)
hele211 Jun 21, 2026
fb5493a
feat: add Apple reminders and calendar automation
hele211 Jul 20, 2026
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
133 changes: 122 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,35 @@ hours, with **no API key** (it uses your existing Codex/ChatGPT login).

## What you get, per memo

- A full note in your vault (e.g. `00_Journal/VoiceMemos/`): summary, action items
as dated checkboxes, topic tags, notable details, and the complete transcript.
- A one-line linked summary + tasks appended under `## Voice Memos` in that day's
daily note.
- **One Markdown note** per recording in `Memory/voiceMemo/` (configurable via
`VOICE2NOTE_MEMO_RELDIR`).
- Filename: `<YYYY-MM-DD-HHMM>-<title-slug>-<16-hex-source-hash>.md`.
- Notes are **created once and never overwritten** (create-once / idempotent). If a
note file already exists for a memo, voice2note verifies its `source_id` and adopts
it; on a mismatch it raises rather than silently clobbering.
- **`status: complete`** — set for `conversation` and `talk` recordings (finalized,
no to-dos generated).
- **`status: draft`** — set for `record` recordings, ambiguous classifications, any
classification whose confidence is below `VOICE2NOTE_CONFIDENCE_MIN` (default 0.5),
and any memo where analysis fails entirely (written as a transcript-only draft with
`analysis: pending`). Draft notes wait in `Memory/voiceMemo/` for human triage via
the `cricknote-voice-inbox` skill (see Integration with CrickNote below).

## How it works

```
launchd (every ~2h) → python -m voice2note.run
find new memos (CloudRecordings.db) [Full Disk Access]
→ copy audio out of the protected folder [Python has FDA; ffmpeg doesn't]
→ ffmpeg → 16 kHz wav → whisper-cli [local transcription]
→ codex exec (analysis → JSON) [your Codex login, no API key]
→ write per-memo note + daily-note summary [into your Obsidian vault]
find new memos (CloudRecordings.db) [Full Disk Access]
→ copy audio out of the protected folder [Python has FDA; ffmpeg doesn't]
→ ffmpeg → 16 kHz wav → whisper-cli [local transcription]
→ codex exec (analysis → JSON) [your Codex login, no API key]
→ classify type + confidence [complete vs. draft status]
→ if you said “CrickNote please”: create a macOS Reminder or Calendar event
→ write single note (create-once, idempotent)
standalone: direct file write into vault
cricknote: cricknote tool vault_write [when USE_CRICKNOTE=true]
→ human triage: ask an agent to run
cricknote-voice-inbox [routing is always human-confirmed]
→ record processed id (no duplicates)
```

Expand All @@ -37,6 +52,9 @@ Everything is local except the analysis step, which sends the **transcript text*
- **whisper.cpp**: `brew install whisper-cpp`
- **ffmpeg**: `brew install ffmpeg`
- **Codex CLI**, logged in: `npm install -g @openai/codex` then `codex login`
- **CrickNote CLI** (optional for standalone; required when `VOICE2NOTE_USE_CRICKNOTE=true`):
install from the [CrickNote project](https://github.com/hele211/cricknote) and ensure
`cricknote` is on your `PATH`.

## Install

Expand All @@ -58,6 +76,20 @@ PYTHONPATH=. /usr/bin/python3 -m voice2note.run --check
./scripts/install.sh
```

The installer (`scripts/install.sh`) auto-discovers the `cricknote` CLI and writes
explicit values into the generated LaunchAgent plist:

- It compares `VOICE2NOTE_VAULT` against `~/.cricknote/config.json` `vaultPath`
(by realpath). If they match **and** the CLI is runnable, it installs with
`VOICE2NOTE_USE_CRICKNOTE=true` and records `VOICE2NOTE_CRICKNOTE_BIN` and
`VOICE2NOTE_MEMO_RELDIR` in the plist.
- If the vaults do **not** match, or the CLI is missing, it installs in standalone
mode (`VOICE2NOTE_USE_CRICKNOTE=false`) and prints a warning.
- Pass `--no-cricknote` to force standalone even when a CrickNote CLI is discoverable.
- If you explicitly set `VOICE2NOTE_USE_CRICKNOTE=true` in your env but the CLI is
not runnable or the vaults do not match, the installer **fails** — there is no
silent downgrade.

### Grant Full Disk Access (required, one-time)

macOS protects the Voice Memos folder. The agent runs **Python directly** so the
Expand Down Expand Up @@ -85,14 +117,85 @@ Set in `voice2note.env` (or as environment variables). Only the vault is require
| Variable | Default | Meaning |
|---|---|---|
| `VOICE2NOTE_VAULT` | _(required)_ | Path to your Obsidian vault |
| `VOICE2NOTE_MEMO_DIR` | `<vault>/00_Journal/VoiceMemos` | Per-memo notes folder |
| `VOICE2NOTE_DAILY_DIR` | `<vault>/00_Journal/Daily` | Daily-notes folder |
| `VOICE2NOTE_MEMO_RELDIR` | `Memory/voiceMemo` | Per-memo notes folder, relative to vault |
| `VOICE2NOTE_MODEL` | `./models/ggml-small.bin` | whisper.cpp model file |
| `VOICE2NOTE_LANGUAGE` | `auto` | Transcription language (`auto`, `en`, `zh`, …) |
| `VOICE2NOTE_CODEX_MODEL` | `gpt-5.5` | Codex model for analysis |
| `VOICE2NOTE_CONFIDENCE_MIN` | `0.5` | Minimum classification confidence; below this the note is downgraded to `draft` |
| `VOICE2NOTE_USE_CRICKNOTE` | `false` | Route note writes through the CrickNote CLI instead of a direct file write |
| `VOICE2NOTE_CRICKNOTE_BIN` | _(auto-discovered)_ | Absolute path to the `cricknote` binary; defaults to `shutil.which("cricknote")` |
| `VOICE2NOTE_APPLE_AUTOMATION` | `false` | Enable explicitly spoken Apple Reminders/Calendar commands |
| `VOICE2NOTE_APPLE_REMINDERS_LIST` | default list | Optional exact Apple Reminders list name |
| `VOICE2NOTE_APPLE_CALENDAR` | first writable calendar | Optional exact Apple Calendar name |

Schedule frequency: `VOICE2NOTE_INTERVAL=<seconds> ./scripts/install.sh` (default 7200).

## Integration with CrickNote

CrickNote is an optional vault-management layer. When enabled, voice2note writes
notes through the CrickNote CLI rather than directly to the filesystem; in every
other way the notes are identical — same file, same frontmatter, same location under
`Memory/voiceMemo/`.

**Same-vault requirement.** `VOICE2NOTE_VAULT` must point to the same vault as
`~/.cricknote/config.json` `vaultPath` (compared by realpath). voice2note checks this
at startup and raises `SystemExit` if they differ.

**Strict no-fallback when integration is enabled.** If `VOICE2NOTE_USE_CRICKNOTE=true`
and the `cricknote` binary is not runnable, startup fails immediately. A per-memo
write failure also raises, so the memo is retried on the next run and never silently
dropped.

**Human triage via `cricknote-voice-inbox`.** Draft notes accumulate in
`Memory/voiceMemo/`. To triage them, open a Claude Code (or Codex) session inside
your Obsidian vault and ask the agent to run the `cricknote-voice-inbox` skill. The
skill reindexes the vault, shows you each draft, and asks for confirmation before
routing it to experiments, ideas, or tasks — or dismissing / reclassifying it.
**Routing is always human-confirmed; voice2note never routes automatically.**

**Enabling manually.** Add these lines to `voice2note.env`:

```
VOICE2NOTE_USE_CRICKNOTE=true
VOICE2NOTE_CRICKNOTE_BIN=/usr/local/bin/cricknote # or wherever your binary lives
```

Then re-run `--check` to confirm the vault match and binary path are correct before
starting the agent.

## Apple Reminders and Calendar commands

Voice2Note can create items in the built-in **Apple Reminders** and **Apple
Calendar** apps. This is separate from CrickNote's Obsidian task list and is
off by default. To enable it, add this to `voice2note.env`:

```bash
VOICE2NOTE_APPLE_AUTOMATION=true
# Optional: choose specific destinations instead of the defaults.
# VOICE2NOTE_APPLE_REMINDERS_LIST=Reminders
# VOICE2NOTE_APPLE_CALENDAR=Calendar
```

Then speak the deliberate trigger **“CrickNote please”** in a Voice Memo. The
phrase is case-insensitive; “Crick Note please” and the common transcription
mistake “Cricket Note please” work too. Examples:

- “CrickNote please, remind me to email Sarah tomorrow at 9.” → a Reminder.
- “CrickNote please, meeting with Sarah this Friday at 2 pm for one hour.” → a
Calendar event.

The analysis uses the meaning of the memo: tasks and follow-ups become
Reminders; appointments and meetings with a clear date and time become Calendar
events. It will not invent a time. A task without a stated time is still added
to Reminders, but without a timed notification. A date without a time is also a
Reminder, rather than an all-day Calendar event.

The first command causes macOS to ask for permission to control Reminders or
Calendar. Allow it. Each memo's created Apple item ID is saved before Voice2Note
marks the memo finished, so if the vault write is retried it will not create a
duplicate Apple item. The corresponding voice-memo note also shows the item
created under **Apple action**.

## Troubleshooting

- **`--check` says DENIED even after granting access.** The grant must be on the
Expand All @@ -106,6 +209,14 @@ Schedule frequency: `VOICE2NOTE_INTERVAL=<seconds> ./scripts/install.sh` (defaul
<model>` with `stdin` detached. `--ignore-user-config` avoids an invalid
`service_tier` in some `~/.codex/config.toml` files; set `VOICE2NOTE_CODEX_MODEL`
to a model your account supports.
- **`cricknote CLI is not runnable` at startup.** `VOICE2NOTE_USE_CRICKNOTE=true` but
the binary cannot be found or executed. Either install the CrickNote CLI and ensure
it is on your `PATH`, set `VOICE2NOTE_CRICKNOTE_BIN` to its absolute path, or set
`VOICE2NOTE_USE_CRICKNOTE=false` to revert to standalone mode.
- **`VOICE2NOTE_VAULT does not match ~/.cricknote/config.json vaultPath`.** Both
tools must point at the same vault directory (compared by realpath, so symlinks are
resolved). Update `VOICE2NOTE_VAULT` in `voice2note.env` or reconfigure CrickNote
so both agree, then re-run `--check`.

## Development

Expand Down
6 changes: 6 additions & 0 deletions com.voice2note.agent.plist.template
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
<string>__PROJECT_DIR__</string>
<key>VOICE2NOTE_VAULT</key>
<string>__VAULT__</string>
<key>VOICE2NOTE_USE_CRICKNOTE</key>
<string>__USE_CRICKNOTE__</string>
<key>VOICE2NOTE_CRICKNOTE_BIN</key>
<string>__CRICKNOTE_BIN__</string>
<key>VOICE2NOTE_MEMO_RELDIR</key>
<string>__MEMO_RELDIR__</string>
</dict>
<key>StartInterval</key>
<integer>__INTERVAL__</integer>
Expand Down
41 changes: 37 additions & 4 deletions config.example.env
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
# Copy this file to `voice2note.env` and edit. Only VOICE2NOTE_VAULT is required.
# Copy this file to `voice2note.env` and edit. Only VOICE2NOTE_VAULT is required
# for standalone (no-CrickNote) mode.
# (The installer also writes VOICE2NOTE_VAULT into the LaunchAgent.)

# Path to your Obsidian vault — where notes are written.
VOICE2NOTE_VAULT=/Users/you/path/to/ObsidianVault

# --- Optional overrides (defaults shown) ---
# Per-memo notes folder and the daily-note folder (default: under the vault):
# VOICE2NOTE_MEMO_DIR=/Users/you/path/to/ObsidianVault/00_Journal/VoiceMemos
# VOICE2NOTE_DAILY_DIR=/Users/you/path/to/ObsidianVault/00_Journal/Daily

# CrickNote integration — set to true to hand off each voice memo to CrickNote
# after transcription and analysis. Requires the cricknote CLI to be installed.
# When enabled, VOICE2NOTE_VAULT must resolve to the same path as the
# `vaultPath` in ~/.cricknote/config.json.
# VOICE2NOTE_USE_CRICKNOTE=true

# Path to the cricknote CLI binary. By default, config.py auto-discovers it via
# shutil.which("cricknote"). install.sh also auto-detects it when writing the
# LaunchAgent plist. Set this only if your binary is not on PATH for launchd
# (launchd has a minimal PATH; manual ./run.sh runs inherit your shell PATH).
# VOICE2NOTE_CRICKNOTE_BIN=/Users/you/.npm-global/bin/cricknote

# Folder for per-recording voice-memo notes, relative to the vault root.
# Must be a relative path with no leading slash and no '..'.
# VOICE2NOTE_MEMO_RELDIR=Memory/voiceMemo

# Minimum classification confidence (0.0–1.0). The analyzer guesses each memo's
# type (conversation/talk/record) with a confidence score; if it's below this
# threshold the memo is treated as "record" and the note is created as a draft
# for you to triage (bias-to-record). A note is always written either way.
# VOICE2NOTE_CONFIDENCE_MIN=0.5

# whisper.cpp model file (default: ./models/ggml-small.bin):
# VOICE2NOTE_MODEL=/absolute/path/to/ggml-small.bin
Expand All @@ -16,3 +36,16 @@ VOICE2NOTE_VAULT=/Users/you/path/to/ObsidianVault

# Codex model used for analysis:
# VOICE2NOTE_CODEX_MODEL=gpt-5.5

# --- Apple Reminders and Calendar (optional) ---
# Enable this only if you want spoken "CrickNote please" commands to create items
# in the built-in macOS Reminders and Calendar apps. Normal recordings are never
# sent to either app. On the first command, macOS asks permission for Python to
# control each app.
# VOICE2NOTE_APPLE_AUTOMATION=true

# Optional destination names. Leave either blank to use your default Reminders
# list or the first writable Calendar. Names must match exactly what you see in
# the Apple apps.
# VOICE2NOTE_APPLE_REMINDERS_LIST=Reminders
# VOICE2NOTE_APPLE_CALENDAR=Calendar
Loading