Rename plugin id to satisfy Obsidian manifest rules

The id rewrite-plugin is rejected on submission because an id may not
end in plugin. Rename to rewrite-voice-notes (lowercase + hyphens,
no obsidian, does not end in plugin). Pre-release, no installed users.

- manifest.json: id -> rewrite-voice-notes
- package.json / package-lock.json: name -> rewrite-voice-notes
- README.md, CLAUDE.md, docs/DEVCONFLICTS.md: update install-folder paths,
  command-id examples, and the id-locked note
- src/ needs no change (uses this.manifest.id dynamically)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
WiseGuru 2026-06-15 21:26:48 -07:00
parent 400ceeafa6
commit 187264ff55
6 changed files with 26 additions and 26 deletions

View file

@ -144,7 +144,7 @@ Saving flow strips secrets out of `data.json` and writes them to `secrets.json.n
[src/secrets.ts](src/secrets.ts) supports two encryption modes for API keys, file-wide (not per-key). There is no unencrypted at-rest option.
- **`secretStorage`** — Obsidian's first-party `app.secretStorage` (GA 1.11.4): a shared, cross-plugin, OS-encrypted store (Keychain/DPAPI/libsecret under the hood, the same backend Electron `safeStorage` used). **Default/preferred when available.** Values live in Obsidian's store, NOT in `secrets.json.nosync` (the on-disk envelope in this mode is just `{ version, mode: 'secretStorage', keys: {} }` to record the mode). Keys are namespaced with `manifest.id` (`rewrite-plugin:profile:desktop:transcription`) because the store is shared across plugins. The API is reached via a narrow `SecretStorageLike` interface + cast (the installed typings predate it) that normalizes `getSecret`/`get`, `setSecret`/`set`, `listSecrets`/`list`, and `deleteSecret`/`removeSecret`/`delete` (falling back to `setSecret(id, '')` when no delete exists); each method may be sync or async, so callers `await`. **Availability** is a round-trip self-test (`setSecret` a sentinel, `getSecret`, compare, clean up) cached for the session — a device with no working OS secret store (e.g. Linux without a keyring) reads unavailable and falls back to passphrase. `warmSecretStorage(plugin)` runs the probe once in `onload` BEFORE `getEncryptionStatus`, so the synchronous `defaultEnvelope()` can prefer `secretStorage` on first run. **Sync caveat:** because the store can sync across devices via Obsidian Sync, keys in this mode are not bound to one device (unlike the `.nosync` file); this is surfaced in the settings mode description.
- **`secretStorage`** — Obsidian's first-party `app.secretStorage` (GA 1.11.4): a shared, cross-plugin, OS-encrypted store (Keychain/DPAPI/libsecret under the hood, the same backend Electron `safeStorage` used). **Default/preferred when available.** Values live in Obsidian's store, NOT in `secrets.json.nosync` (the on-disk envelope in this mode is just `{ version, mode: 'secretStorage', keys: {} }` to record the mode). Keys are namespaced with `manifest.id` (`rewrite-voice-notes:profile:desktop:transcription`) because the store is shared across plugins. The API is reached via a narrow `SecretStorageLike` interface + cast (the installed typings predate it) that normalizes `getSecret`/`get`, `setSecret`/`set`, `listSecrets`/`list`, and `deleteSecret`/`removeSecret`/`delete` (falling back to `setSecret(id, '')` when no delete exists); each method may be sync or async, so callers `await`. **Availability** is a round-trip self-test (`setSecret` a sentinel, `getSecret`, compare, clean up) cached for the session — a device with no working OS secret store (e.g. Linux without a keyring) reads unavailable and falls back to passphrase. `warmSecretStorage(plugin)` runs the probe once in `onload` BEFORE `getEncryptionStatus`, so the synchronous `defaultEnvelope()` can prefer `secretStorage` on first run. **Sync caveat:** because the store can sync across devices via Obsidian Sync, keys in this mode are not bound to one device (unlike the `.nosync` file); this is surfaced in the settings mode description.
- **`passphrase`** — WebCrypto AES-GCM-256 with a key derived from a user-supplied passphrase. The KDF is **Argon2id** by default (via `hash-wasm`, `m = 32 MiB`, `t = 3`, `p = 1`, 32-byte output), with **PBKDF2-SHA256** (600,000 iterations) as the fallback when a device can't run Argon2id. The chosen algorithm + params live in the envelope `kdf` (see below). Each value is stored as `<iv-b64>.<ct-b64>` (12-byte random IV per value) in `secrets.json.nosync`. A `verifier` field stores an encryption of `VERIFIER_PLAINTEXT` so unlock can validate the passphrase without decrypting user keys. Works on every platform including mobile and Linux-without-keyring; the always-available fallback when `secretStorage` is not. **Entropy gate:** `changeEncryptionMode`/`changePassphrase` reject a passphrase scoring below `MIN_PASSPHRASE_SCORE` (3 of 4) via [src/passphrase-strength.ts](src/passphrase-strength.ts) (zxcvbn-ts wrapper); this is the hard, non-bypassable gate. The passphrase modal ([src/ui/passphrase-modal.ts](src/ui/passphrase-modal.ts)) surfaces a live strength meter and a **Generate** button (6-word EFF-wordlist diceware via [src/diceware.ts](src/diceware.ts) + [src/eff-large-wordlist.ts](src/eff-large-wordlist.ts)) on create/change flows (`enforceStrength: true`). **zxcvbn-ts is lazy-loaded:** its dictionaries are ~1.6 MB / ~140 ms to build, so [src/passphrase-strength.ts](src/passphrase-strength.ts) pulls the packages via dynamic `import()` (not a static top-level import), keeping them out of plugin-startup evaluation; `evaluatePassphrase`/`isPassphraseAcceptable` are therefore **async**, and the modal calls `warmPassphraseStrength()` on open so the first keystroke does not pay the build cost. The bytes stay in `main.js` (esbuild has no code-splitting here); only the parse/construct work is deferred.
History note: an earlier hand-rolled `safeStorage` mode (direct Electron keychain) was REMOVED and superseded by `secretStorage`, which uses the same OS backend but is Obsidian-managed and cross-platform. No users had it (unpublished), so it was dropped outright with no migration; a stale `safeStorage` envelope parses as invalid and resets to default.
@ -258,12 +258,12 @@ UI surfaces, both a collapsed `<details>` so the frictionless path is untouched:
Registered in [src/main.ts](src/main.ts):
- **`rewrite-plugin:open-modal`** ("Open"): opens the main modal with the last-used template selected.
- **`rewrite-plugin:quick-record`** ("Quick record (last used)"): starts a recording immediately with a floating mini-UI (no modal). Second press toggles to Stop. Template = `pickQuickRecordTemplate` (`lastUsedTemplateId` → `defaultTemplateId``templates[0]`). On unconfigured profile or capture-API unavailability, opens the modal instead. On post-capture pipeline error, opens the modal so the user can retry (the persisted audio file is the recovery path; the pipeline no longer copies the transcript to the clipboard).
- **`rewrite-plugin:quick-record-fixed`** ("Quick record (set template)"): same flow, but records with the template chosen in `GlobalSettings.quickRecordTemplateId` (Settings dropdown beside Default template). If that id is unset or no longer resolves, it shows a "choose a Quick record template in settings" `Notice` and does not start (no `templates[0]` fallback, unlike the last-used command). Both commands share `toggleQuickRecord(opts?)` and the single `activeQuickRecord` ref, so either one stops an in-flight recording. The floating UI shows a stop-hotkey hint ("Press <combo> or click Stop") read live from the command's binding; see the Gotcha on `hotkeyManager`.
- **`rewrite-plugin:process-text`** ("Process text with template"): runs a template over the active editor's selection (or the whole note body if there's no selection). Opens a template quick-picker, then runs the pipeline in the background with progress shown via `Notice`. Gates on LLM-only configuration; opens the main modal's setup card when not configured. Bails with a Notice when no Markdown editor is active.
- **`rewrite-plugin:reprocess-audio`** ("Reprocess audio file with template"): reruns the pipeline over an audio file already in the vault. Opens an `AudioFilePickerModal` (`FuzzySuggestModal<TFile>` filtered to `AUDIO_EXTENSIONS` from [src/audio-persist.ts](src/audio-persist.ts)) then the template quick-picker, then calls `runAudioFilePipeline` in [src/ui/audio-source.ts](src/ui/audio-source.ts). The pipeline skips its `persist-audio` stage because the `audio` source variant carries a `sourcePath` (the existing vault path is reused for the `![[<path>]]\n\n` prepend). Gates on the full voice profile (`isProfileConfigured`).
- **`rewrite-plugin:start-whisper-host`** / **`rewrite-plugin:stop-whisper-host`**: start or stop the local whisper.cpp server. Both use `checkCallback` so the palette only shows them on desktop, when the active profile's transcription provider is `whisper-local` (start) or when the host is currently `running` / `starting` (stop). Errors surface via `Notice`. Same code paths as the settings-tab Start/Stop button.
- **`rewrite-voice-notes:open-modal`** ("Open"): opens the main modal with the last-used template selected.
- **`rewrite-voice-notes:quick-record`** ("Quick record (last used)"): starts a recording immediately with a floating mini-UI (no modal). Second press toggles to Stop. Template = `pickQuickRecordTemplate` (`lastUsedTemplateId` → `defaultTemplateId``templates[0]`). On unconfigured profile or capture-API unavailability, opens the modal instead. On post-capture pipeline error, opens the modal so the user can retry (the persisted audio file is the recovery path; the pipeline no longer copies the transcript to the clipboard).
- **`rewrite-voice-notes:quick-record-fixed`** ("Quick record (set template)"): same flow, but records with the template chosen in `GlobalSettings.quickRecordTemplateId` (Settings dropdown beside Default template). If that id is unset or no longer resolves, it shows a "choose a Quick record template in settings" `Notice` and does not start (no `templates[0]` fallback, unlike the last-used command). Both commands share `toggleQuickRecord(opts?)` and the single `activeQuickRecord` ref, so either one stops an in-flight recording. The floating UI shows a stop-hotkey hint ("Press <combo> or click Stop") read live from the command's binding; see the Gotcha on `hotkeyManager`.
- **`rewrite-voice-notes:process-text`** ("Process text with template"): runs a template over the active editor's selection (or the whole note body if there's no selection). Opens a template quick-picker, then runs the pipeline in the background with progress shown via `Notice`. Gates on LLM-only configuration; opens the main modal's setup card when not configured. Bails with a Notice when no Markdown editor is active.
- **`rewrite-voice-notes:reprocess-audio`** ("Reprocess audio file with template"): reruns the pipeline over an audio file already in the vault. Opens an `AudioFilePickerModal` (`FuzzySuggestModal<TFile>` filtered to `AUDIO_EXTENSIONS` from [src/audio-persist.ts](src/audio-persist.ts)) then the template quick-picker, then calls `runAudioFilePipeline` in [src/ui/audio-source.ts](src/ui/audio-source.ts). The pipeline skips its `persist-audio` stage because the `audio` source variant carries a `sourcePath` (the existing vault path is reused for the `![[<path>]]\n\n` prepend). Gates on the full voice profile (`isProfileConfigured`).
- **`rewrite-voice-notes:start-whisper-host`** / **`rewrite-voice-notes:stop-whisper-host`**: start or stop the local whisper.cpp server. Both use `checkCallback` so the palette only shows them on desktop, when the active profile's transcription provider is `whisper-local` (start) or when the host is currently `running` / `starting` (stop). Errors surface via `Notice`. Same code paths as the settings-tab Start/Stop button.
Plus an editor-menu item "ReWrite with template..." registered via `workspace.on('editor-menu', ...)` (and a second "Reprocess audio with template..." item that appears only when the cursor sits inside an `![[<audio>]]` embed, resolved via `app.metadataCache.getFirstLinkpathDest`), a `workspace.on('file-menu', ...)` handler that adds "Reprocess audio with template..." for audio files in the file explorer, an `addRibbonIcon('mic', 'ReWrite', ...)` that opens the modal, and a status-bar item ([src/ui/whisper-status-bar.ts](src/ui/whisper-status-bar.ts)) showing the live whisper-host status. The status bar polls `whisperHost.status()` every 1 s via `registerInterval`, click toggles start/stop, and the item is hidden via the `rewrite-hidden` CSS class when on mobile or when the active profile is not `whisper-local`.
@ -275,7 +275,7 @@ Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Ma
[AGENTS.md](AGENTS.md) has the full Obsidian-specific playbook. The non-obvious rules that actually constrain implementation:
- **Never change `manifest.json`'s `id` after release.** It's `rewrite-plugin`. Locked.
- **Never change `manifest.json`'s `id` after release.** It's `rewrite-voice-notes`. Locked. (Renamed once pre-release from the invalid `rewrite-plugin`, which Obsidian's manifest rules reject because an id may not end in `plugin`; no users were affected. Do not change it again.)
- **Use `this.register*` helpers** (`registerEvent`, `registerDomEvent`, `registerInterval`) for anything that needs cleanup. Otherwise reload/unload leaks. The Quick Record floater is the one exception (a `document.body` div lifecycled by `QuickRecordController.cancel()`, which `onunload` calls).
- **Mobile compatibility**: avoid Node/Electron APIs unless `manifest.json` sets `isDesktopOnly: true`. It's `false`, and the spec's mobile profile depends on this.
- **Keep [src/main.ts](src/main.ts) minimal**: only plugin lifecycle, command registration, settings tab registration. Feature logic belongs in dedicated modules.
@ -335,7 +335,7 @@ Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Ma
## Local install for testing
Build, then place/symlink `main.js`, `manifest.json`, and `styles.css` into `<Vault>/.obsidian/plugins/rewrite-plugin/` and reload Obsidian (Settings, Community plugins).
Build, then place/symlink `main.js`, `manifest.json`, and `styles.css` into `<Vault>/.obsidian/plugins/rewrite-voice-notes/` and reload Obsidian (Settings, Community plugins).
## Never use em dashes in your own writing.

View file

@ -81,7 +81,7 @@ Notes:
Until the plugin is in the community plugin directory, install manually:
1. Build or download the release artifacts: `main.js`, `manifest.json`, `styles.css`.
2. Create the folder `<YourVault>/.obsidian/plugins/rewrite-plugin/`.
2. Create the folder `<YourVault>/.obsidian/plugins/rewrite-voice-notes/`.
3. Copy the three files into that folder.
4. In Obsidian, go to Settings, Community plugins, and enable "ReWrite (Voice Notes)". (Make sure Restricted mode is off.)
5. Open Settings, ReWrite (Voice Notes), enter at least one provider API key, and pick a model for both the transcription and LLM provider.
@ -89,8 +89,8 @@ Until the plugin is in the community plugin directory, install manually:
### Building from source
```bash
git clone https://github.com/<your-fork>/rewrite-plugin.git
cd rewrite-plugin
git clone https://github.com/<your-fork>/rewrite-voice-notes.git
cd rewrite-voice-notes
npm install
npm run build
```
@ -227,7 +227,7 @@ For podcasts, meetings, and interviews, you can turn on **Identify speakers** in
## Excluding `secrets.json.nosync` from sync
API keys are stored in `<YourVault>/.obsidian/plugins/rewrite-plugin/secrets.json.nosync`, separately from the rest of the plugin's settings.
API keys are stored in `<YourVault>/.obsidian/plugins/rewrite-voice-notes/secrets.json.nosync`, separately from the rest of the plugin's settings.
The plugin supports two at-rest encryption modes, selectable in settings under "API key encryption". There is no unencrypted option:
@ -239,14 +239,14 @@ If you use **passphrase mode** and do not want the encrypted key file copied aro
The path to exclude is always:
```
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
.obsidian/plugins/rewrite-voice-notes/secrets.json.nosync
```
### Obsidian Sync (official)
Obsidian Sync excludes folders, not individual files (Settings, Sync, Excluded folders). You have two options:
- Exclude the entire `.obsidian/plugins/rewrite-plugin` folder and accept that you will lose template/profile sync (`data.json` lives there too).
- Exclude the entire `.obsidian/plugins/rewrite-voice-notes` folder and accept that you will lose template/profile sync (`data.json` lives there too).
- Or sync the folder and accept that the encrypted `secrets.json.nosync` blob will be uploaded; on other devices it will fail to decrypt and the plugin will treat it as no key set, prompting you to enter the key again.
### Syncthing
@ -255,7 +255,7 @@ Add to `.stignore` in the synced folder root:
```
// ReWrite plugin secrets, never sync API keys
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
.obsidian/plugins/rewrite-voice-notes/secrets.json.nosync
```
If the vault is not at the Syncthing folder root, omit the leading slash from any patterns.
@ -265,7 +265,7 @@ If the vault is not at the Syncthing folder root, omit the leading slash from an
Add this line to `.sync/IgnoreList` on each peer:
```
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
.obsidian/plugins/rewrite-voice-notes/secrets.json.nosync
```
### Git / GitHub
@ -274,14 +274,14 @@ Add to the vault's `.gitignore`:
```gitignore
# ReWrite plugin, never commit API keys
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
.obsidian/plugins/rewrite-voice-notes/secrets.json.nosync
```
If you have already committed it:
```bash
git rm --cached .obsidian/plugins/rewrite-plugin/secrets.json.nosync
git commit -m "remove rewrite-plugin secrets from tracking"
git rm --cached .obsidian/plugins/rewrite-voice-notes/secrets.json.nosync
git commit -m "remove rewrite-voice-notes secrets from tracking"
```
### Dropbox
@ -290,7 +290,7 @@ Dropbox has no ignore-file mechanism. Use Selective Sync:
1. Open the Dropbox desktop app.
2. Preferences, Sync, Selective Sync.
3. Deselect the `rewrite-plugin` plugin folder, or use file-level exclusions if your Dropbox plan supports them.
3. Deselect the `rewrite-voice-notes` plugin folder, or use file-level exclusions if your Dropbox plan supports them.
Alternatively, delete `secrets.json.nosync` from Dropbox via the web interface after setup; the plugin will recreate it locally when you next enter keys.

View file

@ -57,7 +57,7 @@ Guideline: prefer `app.vault` over `app.vault.adapter` (caching + serialized, sa
Both touch config/sidecar files that are intentionally not regular vault notes (e.g. the `.nosync` secrets envelope), so direct adapter access is arguably justified — but it is still a documented deviation a reviewer may question.
> **Resolution: Accepted as correct (reclassified).** Both paths resolve under `plugin.manifest.dir` (`.obsidian/plugins/rewrite-plugin/`), which is the plugin config directory, not vault note content. The `app.vault` TFile API does not address files there; `app.vault.adapter` is the appropriate API for plugin-config files. No change.
> **Resolution: Accepted as correct (reclassified).** Both paths resolve under `plugin.manifest.dir` (`.obsidian/plugins/rewrite-voice-notes/`), which is the plugin config directory, not vault note content. The `app.vault` TFile API does not address files there; `app.vault.adapter` is the appropriate API for plugin-config files. No change.
### 6. Use of undocumented / private Obsidian internals
The guidelines steer away from relying on internals not in the public API (they can change without notice). The plugin reaches several via `as unknown as` casts:

View file

@ -1,5 +1,5 @@
{
"id": "rewrite-plugin",
"id": "rewrite-voice-notes",
"name": "ReWrite (Voice Notes)",
"version": "1.0.0",
"minAppVersion": "1.4.0",

4
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "rewrite-plugin",
"name": "rewrite-voice-notes",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rewrite-plugin",
"name": "rewrite-voice-notes",
"version": "0.1.1",
"license": "0-BSD",
"dependencies": {

View file

@ -1,5 +1,5 @@
{
"name": "rewrite-plugin",
"name": "rewrite-voice-notes",
"version": "1.0.0",
"description": "Record or paste speech and have it transcribed and structured by AI.",
"main": "main.js",