mirror of
https://github.com/wiseguru/ReWrite-Voice-Notes.git
synced 2026-07-22 07:49:19 +00:00
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:
parent
400ceeafa6
commit
187264ff55
6 changed files with 26 additions and 26 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue