mirror of
https://github.com/wiseguru/ReWrite-Voice-Notes.git
synced 2026-07-22 07:49:19 +00:00
Phase 12-13
This commit is contained in:
parent
3f859fad27
commit
43ef509d23
2 changed files with 236 additions and 91 deletions
124
CLAUDE.md
124
CLAUDE.md
|
|
@ -4,14 +4,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project state
|
||||
|
||||
**Implementation in progress against [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md). The live status of each phase (what's committed, what's uncommitted, architectural decisions made along the way) is in [docs/claude-scratch/STATUS.md](docs/claude-scratch/STATUS.md). Read STATUS.md first; it is more current than the rest of this section, which will be rewritten wholesale in Phase 13.**
|
||||
This is the **ReWrite (Voice Notes) plugin for Obsidian**: record or paste speech, transcribe via a user-configured provider, clean and structure via an LLM, insert per a chosen template. Desktop and mobile.
|
||||
|
||||
This repo is becoming the **ReWrite (Voice Notes) plugin for Obsidian**. The spec (providers, profile system, modal UX, templates, settings layout, API request shapes) lives in [obsidian-voice-notes-spec.md](obsidian-voice-notes-spec.md), which is still the source of truth for behavior.
|
||||
The v1 implementation is feature-complete against [obsidian-voice-notes-spec.md](obsidian-voice-notes-spec.md) and [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md). The spec is still the source of truth for behavior; the implementation plan resolves the spec's internal discrepancies (notably manifest id, base URL handling for `openai-compatible`, and mobile `safeStorage` unavailability). [docs/claude-scratch/STATUS.md](docs/claude-scratch/STATUS.md) tracks per-phase commit state and the running list of architectural decisions made during implementation; consult it when picking up work or before changing anything cross-cutting.
|
||||
|
||||
When implementing spec features, follow the file layout the spec prescribes (provider adapters under `src/transcription/` and `src/llm/`, factories in each `index.ts`, no provider-specific logic leaking outside its own file).
|
||||
When extending the plugin, follow the file layout the spec prescribes: provider adapters under `src/transcription/` and `src/llm/`, factories in each `index.ts`, no provider-specific logic leaking outside its own file.
|
||||
|
||||
## Documentation maintenance
|
||||
Update CLAUDE.md with every behavioral change. When modifying code that this document describes (pipeline step count, step responsibilities, CLI flags, config keys, constants like `_SECTION_ORDER`, hardware/model defaults, caching behavior, gotchas), update CLAUDE.md in the same change. If a behavioral change has no existing section, add one or drop a note under "Gotchas". Treat the doc update as part of the task, not a follow-up.
|
||||
|
||||
Update CLAUDE.md with every behavioral change. When modifying code that this document describes (pipeline stages, command IDs, settings keys, gotchas, conventions), update CLAUDE.md in the same change. If a behavioral change has no existing section, add one or drop a note under "Gotchas". Treat the doc update as part of the task, not a follow-up.
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -23,18 +24,89 @@ npm run lint # eslint over the repo (uses eslint-plugin-obsidianmd recomme
|
|||
npm version <patch|minor|major> # bumps manifest.json + versions.json via version-bump.mjs
|
||||
```
|
||||
|
||||
There is no test runner configured.
|
||||
There is no test runner configured. Verification is `npm run build && npm run lint` (CI parity) plus the manual checklist in [obsidian-voice-notes-spec.md](obsidian-voice-notes-spec.md) (lines 454-476).
|
||||
|
||||
CI ([.github/workflows/lint.yml](.github/workflows/lint.yml)) runs `npm ci`, `npm run build`, and `npm run lint` on Node 20.x and 22.x for every push/PR.
|
||||
|
||||
## Build architecture
|
||||
|
||||
- Entry: [src/main.ts](src/main.ts) → bundled to `./main.js` at repo root (the file Obsidian loads).
|
||||
- Bundler: [esbuild.config.mjs](esbuild.config.mjs). `obsidian`, `electron`, all `@codemirror/*`, all `@lezer/*`, and Node built-ins are marked `external` — never import other runtime deps without bundling them in.
|
||||
- Entry: [src/main.ts](src/main.ts) → bundled to `./main.js` at repo root (the file Obsidian loads). Kept minimal: settings load, ribbon icon, two commands, settings tab, plus a single `activeQuickRecord` ref so the Quick Record command can toggle.
|
||||
- Bundler: [esbuild.config.mjs](esbuild.config.mjs). `obsidian`, `electron`, all `@codemirror/*`, all `@lezer/*`, and Node built-ins are marked `external`. Never import other runtime deps without bundling them in.
|
||||
- Release artifacts are `main.js`, `manifest.json`, and `styles.css` at the repo root. Do not commit the generated `main.js`.
|
||||
- TypeScript config ([tsconfig.json](tsconfig.json)) is strict: `noImplicitAny`, `strictNullChecks`, `noImplicitReturns`, `noUncheckedIndexedAccess`, `useUnknownInCatchVariables`, `baseUrl: "src"`. Target ES6, module ESNext, lib DOM + ES5/6/7 only — no Node lib, so don't reach for Node APIs in plugin code.
|
||||
- TypeScript config ([tsconfig.json](tsconfig.json)) is strict: `noImplicitAny`, `strictNullChecks`, `noImplicitReturns`, `noUncheckedIndexedAccess`, `useUnknownInCatchVariables`, `baseUrl: "src"`. Target ES6, module ESNext, lib DOM + ES5/6/7 only. No Node lib, so don't reach for Node APIs in plugin code.
|
||||
- ESLint ([eslint.config.mts](eslint.config.mts)) layers `eslint-plugin-obsidianmd`'s recommended rules on top of `typescript-eslint`. These rules encode Obsidian-specific correctness checks; respect them rather than disabling.
|
||||
|
||||
## Source layout
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts # Lifecycle, commands, ribbon, settings tab registration
|
||||
├── types.ts # Shared interfaces (provider IDs, configs, templates, settings)
|
||||
├── http.ts # requestUrl wrappers: jsonPost/jsonGet/multipartPost + ProviderError
|
||||
├── platform.ts # Active-profile resolver + MediaRecorder/Web Speech availability probes
|
||||
├── secrets.ts # safeStorage (desktop) + plaintext fallback (mobile) for API keys
|
||||
├── recorder.ts # MediaRecorder state machine + getBestMimeType + 25 MB size guard
|
||||
├── webspeech.ts # SpeechRecognition wrapper
|
||||
├── pipeline.ts # transcribe → cleanup → insert orchestrator
|
||||
├── insert.ts # cursor/newFile/append + {{date}}/{{time}} expansion
|
||||
├── settings/
|
||||
│ ├── index.ts # DEFAULT_SETTINGS, load/save, secret hydration, family resolvers
|
||||
│ ├── tab.ts # PluginSettingTab with all 6 sections
|
||||
│ └── default-templates.ts # The 5 seeded templates (General cleanup, Todo list, etc.)
|
||||
├── ui/
|
||||
│ ├── modal.ts # Main modal: template select + Record/Paste tabs + setup-card injection
|
||||
│ ├── setup-card.ts # Inline blocker when active profile is unconfigured
|
||||
│ └── quick-record.ts # QuickRecordController + floating mini-UI for the Quick Record command
|
||||
├── transcription/
|
||||
│ ├── index.ts # TranscriptionProvider interface + createTranscriptionProvider()
|
||||
│ ├── openai.ts # Whisper-shape POST (also used by openai-compatible + groq)
|
||||
│ ├── assemblyai.ts # upload → submit → poll
|
||||
│ ├── deepgram.ts # single POST
|
||||
│ ├── revai.ts # submit → poll → fetch text
|
||||
│ └── webspeech.ts # adapter; throws "should not be called" (see Gotchas)
|
||||
└── llm/
|
||||
├── index.ts # LLMProvider interface + createLLMProvider()
|
||||
├── openai.ts # /v1/chat/completions (also used by openai-compatible + mistral)
|
||||
├── anthropic.ts # /v1/messages
|
||||
└── gemini.ts # :generateContent
|
||||
```
|
||||
|
||||
## Pipeline
|
||||
|
||||
[src/pipeline.ts](src/pipeline.ts) runs three stages with `onStage` callbacks for UI:
|
||||
|
||||
1. **Transcribe**: `audio` → `createTranscriptionProvider(profile.transcriptionProvider).transcribe(blob, config)`. Skipped when the source is `paste` (text passes through). Short-circuited when the source is `webspeech` (the transcript was already captured live by `src/webspeech.ts` during recording).
|
||||
2. **Cleanup**: `createLLMProvider(profile.llmProvider).complete(template.prompt, transcript, config)`. On error, the raw transcript is copied to the clipboard before re-throwing, so the user keeps their words.
|
||||
3. **Insert**: `src/insert.ts` routes to `cursor` / `newFile` / `append` per the template. `cursor` falls back to `append` when no editor is active; `append` falls back to `newFile` when no markdown file exists. `{{date}}` / `{{time}}` in filename templates expand via Obsidian's `moment`.
|
||||
|
||||
The pipeline accepts an `AbortSignal` (forwarded to providers) and is consumed by both [src/ui/modal.ts](src/ui/modal.ts) and [src/ui/quick-record.ts](src/ui/quick-record.ts).
|
||||
|
||||
## Provider system
|
||||
|
||||
[src/transcription/index.ts](src/transcription/index.ts) and [src/llm/index.ts](src/llm/index.ts) each define a small interface plus a `create...Provider(id)` factory. Provider families share one adapter file: OpenAI Whisper, `openai-compatible`, and Groq all dispatch into [src/transcription/openai.ts](src/transcription/openai.ts) with different base URLs; OpenAI GPT, `openai-compatible`, and Mistral all dispatch into [src/llm/openai.ts](src/llm/openai.ts).
|
||||
|
||||
API keys live in [src/secrets.ts](src/secrets.ts), keyed by *provider family* (`openai`, `anthropic`, `groq`, etc.) so that one OpenAI key serves both Whisper and GPT. Per-profile overrides exist on `EnvironmentProfile.transcriptionConfig.apiKey` / `llmConfig.apiKey`; the `openai-compatible` family has *no* global slot because its base URL is profile-specific. Keys are looked up via `resolveTranscriptionApiKey` / `resolveLLMApiKey` in [src/settings/index.ts](src/settings/index.ts).
|
||||
|
||||
## Settings
|
||||
|
||||
`GlobalSettings` (defined in [src/types.ts](src/types.ts)) is the shape of `data.json`. Loading flow:
|
||||
|
||||
1. `plugin.loadData()` returns `Partial<GlobalSettings> | null`.
|
||||
2. `mergeSettings(DEFAULT_SETTINGS, stored)` deep-merges, preferring stored values.
|
||||
3. If `merged.templates.length === 0`, [src/settings/default-templates.ts](src/settings/default-templates.ts) seeds the 5 starter templates and sets `defaultTemplateId` if unset. This handles first launch *and* migrations from any pre-Phase-11 install with an empty templates array.
|
||||
4. `hydrateSecrets()` reads keys from `secrets.json.nosync` and writes them into the in-memory `apiKeys` map + profile slots.
|
||||
|
||||
Saving flow strips secrets out of `data.json` and writes them to `secrets.json.nosync` instead (see [src/secrets.ts](src/secrets.ts)). Never persist API keys to `data.json`.
|
||||
|
||||
## Commands
|
||||
|
||||
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"): starts a recording immediately with a floating mini-UI (no modal). Second press toggles to Stop. On unconfigured profile or capture-API unavailability, opens the modal instead. On post-capture pipeline error, opens the modal so the user can retry (LLM-stage failures leave the raw transcript on the clipboard).
|
||||
|
||||
Plus an `addRibbonIcon('mic', 'ReWrite', ...)` that opens the modal.
|
||||
|
||||
## Code style
|
||||
|
||||
Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Matches the existing source.
|
||||
|
|
@ -43,19 +115,37 @@ 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 a stable identifier.
|
||||
- **Use `this.register*` helpers** (`registerEvent`, `registerDomEvent`, `registerInterval`) for anything that needs cleanup — otherwise reload/unload leaks.
|
||||
- **Mobile compatibility**: avoid Node/Electron APIs unless `manifest.json` sets `isDesktopOnly: true`. Current manifest is `false`, and the spec's mobile profile depends on this (Web Speech API path).
|
||||
- **Keep [src/main.ts](src/main.ts) minimal** — only plugin lifecycle (onload/onunload, command registration, settings tab registration). Feature logic belongs in dedicated modules.
|
||||
- **Defer heavy work**: no long tasks in `onload`. Lazy-init providers/recorders when first used.
|
||||
- **Network policy**: provider calls go to user-configured endpoints with user-provided keys. No telemetry, no auto-update of plugin code, no fetch+eval.
|
||||
- **Never change `manifest.json`'s `id` after release.** It's `rewrite-plugin`. Locked.
|
||||
- **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.
|
||||
- **Defer heavy work**: no long tasks in `onload`. Providers/recorders lazy-init when first used.
|
||||
- **Network policy**: provider calls go to user-configured endpoints with user-provided keys. No telemetry, no auto-update of plugin code, no `fetch`+`eval`.
|
||||
- **Releases**: GitHub release tag must exactly match `manifest.json`'s `version` (no leading `v`). Attach `main.js`, `manifest.json`, `styles.css` as individual binary assets (not zipped).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`requestUrl` multipart bodies are hand-built.** `requestUrl` does not accept `FormData`. [src/http.ts](src/http.ts) exports `buildMultipart(parts)` which produces a `Uint8Array` with a random boundary; transcription adapters (Whisper, Rev.ai) call into it. If you add a multipart-LLM provider, reuse this rather than reaching for `FormData`.
|
||||
- **`requestUrl` uses `throw: false` + status check.** All adapters surface non-2xx as `ProviderError` with `status` and `body`, so users see provider-attributed errors instead of opaque network failures.
|
||||
- **`safeStorage` is lazy-required inside a `Platform.isDesktop` guard** in [src/secrets.ts](src/secrets.ts). Importing `electron` at module top would crash on mobile load (it's marked `external` in esbuild). Any failure is treated as "encryption unavailable", which is also the mobile path.
|
||||
- **No baked-in model defaults.** Both profiles ship with `model: ""`. The modal renders an inline setup card that blocks recording/paste until the active profile has a provider, model, key, and (for `openai-compatible`) base URL. If you add a provider, do not bake a default model string; surface it as placeholder hint text.
|
||||
- **`openai-compatible` base URL asymmetry** (literal interpretation of the spec): transcription appends `/v1/audio/transcriptions` to a *root* URL (`http://localhost:8080`); LLM appends `/chat/completions` to a URL that *already includes* `/v1` (`http://localhost:11434/v1`). The settings UI hint text and setup card both guide users; do not "normalize" one to match the other.
|
||||
- **Web Speech adapter throws "should not be called".** The pipeline short-circuits when `source.kind === 'webspeech'` and uses the transcript captured live by `src/webspeech.ts` during recording. The factory still has the case to keep the switch exhaustive. On mobile WKWebView (iOS) `SpeechRecognition` is undefined, so [src/platform.ts](src/platform.ts) exports `isWebSpeechAvailable()`; the modal/setup-card and Quick Record both check it before starting a Web Speech session.
|
||||
- **`setHeading()` instead of manual `<h2>`** inside settings tabs. `obsidianmd/settings-tab/no-manual-html-headings` forbids manual headings. Same applies anywhere else inside a settings tab that needs a section header.
|
||||
- **`window.confirm` is banned** by ESLint's `no-alert`. [src/settings/tab.ts](src/settings/tab.ts) ships a small in-file `ConfirmModal extends Modal` used for template deletion; reuse that pattern if another phase needs confirmation, don't reach for `window.confirm`.
|
||||
- **Sentence-case lint covers brand and acronym lists** in [eslint.config.mts](eslint.config.mts). Adding a new provider, model family, or product name means adding it to `REWRITE_BRANDS` (or `REWRITE_ACRONYMS` for things like `LLM`). Dropdown option labels also pass through the rule; in [src/settings/tab.ts](src/settings/tab.ts) and [src/ui/setup-card.ts](src/ui/setup-card.ts), labels are iterated via `opt.label` (member access) to dodge the literal-string check.
|
||||
- **Provider option arrays appear in both setup-card.ts and tab.ts.** Intentionally duplicated rather than extracted, per the "don't refactor beyond what the task requires" rule. If the lists drift, fix the user-visible inconsistency, not the duplication.
|
||||
- **Settings tab re-renders the entire container on dropdown changes** that toggle conditional fields (provider, insertMode, activeProfileOverride). Text fields call `saveSettings()` on change but do not redraw, so focus is preserved while typing. Preserve this pattern when adding new conditional fields.
|
||||
- **Default templates re-seed when `templates.length === 0`**, not via a `hasSeeded` flag. This is deliberate: it migrates pre-Phase-11 installs and gives a user who deleted everything a way back to a working state. Stable IDs (`tpl-default-...`) mean re-seed produces no duplicates.
|
||||
- **Quick Record uses a custom floating div, not a `Notice`.** Obsidian `Notice` does not support real interactive buttons. The floater is a `position: fixed` div on `document.body`, owned by `QuickRecordController`, with `cancel()` wired into `onunload`.
|
||||
- **`secrets.json.nosync` uses the `.nosync` suffix on purpose**: iCloud Drive natively skips any file or folder whose name ends in `.nosync`. The README documents per-tool sync exclusion for other tools.
|
||||
|
||||
## Local install for testing
|
||||
|
||||
Build, then place/symlink `main.js`, `manifest.json`, and `styles.css` into `<Vault>/.obsidian/plugins/<plugin-id>/` and reload Obsidian (Settings → Community plugins).
|
||||
|
||||
Build, then place/symlink `main.js`, `manifest.json`, and `styles.css` into `<Vault>/.obsidian/plugins/rewrite-plugin/` and reload Obsidian (Settings, Community plugins).
|
||||
|
||||
## Never use em dashes in your own writing.
|
||||
Do not use the em dash character (—) in any prose, lists, code comments, or analysis you produce. Use commas, periods, parentheses, semicolons, or colons instead, whichever fits the sentence best. Exception: when directly quoting a source inside quotation marks, preserve em dashes exactly as they appear. Do not silently edit quoted text.
|
||||
|
||||
Do not use the em dash character in any prose, lists, code comments, or analysis you produce. Use commas, periods, parentheses, semicolons, or colons instead, whichever fits the sentence best. Exception: when directly quoting a source inside quotation marks, preserve em dashes exactly as they appear. Do not silently edit quoted text.
|
||||
|
||||
Why: Consistent formatting preference for original writing, while keeping quoted material faithful to the source.
|
||||
|
|
|
|||
203
README.md
203
README.md
|
|
@ -1,90 +1,145 @@
|
|||
# Obsidian Sample Plugin
|
||||
# ReWrite (Voice Notes)
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
An Obsidian plugin that captures speech (live recording or pasted transcript), runs it through a transcription provider, cleans and structures it with an LLM, and inserts the result into your vault.
|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
You bring your own provider keys. Nothing is sent to a ReWrite server; the plugin only talks to the endpoints you configure.
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open modal (simple)" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
## Features
|
||||
|
||||
## First time developing plugins?
|
||||
- Record audio directly in Obsidian, or paste a pre-existing transcript.
|
||||
- 6 transcription providers: OpenAI Whisper, OpenAI-compatible (whisper.cpp, faster-whisper-server, etc.), Groq, AssemblyAI, Deepgram, Rev.ai, and the browser-native Web Speech API.
|
||||
- 5 LLM providers for cleanup: Anthropic Claude, OpenAI GPT, OpenAI-compatible (Ollama, LM Studio), Google Gemini, Mistral.
|
||||
- Desktop and Mobile profiles, auto-selected by environment with a manual override.
|
||||
- 5 starter templates (General cleanup, Todo list, Daily note, Meeting notes, Idea capture); fully editable and reorderable.
|
||||
- Quick Record command for one-shot capture with no modal: ribbon icon, command palette, or a custom hotkey.
|
||||
- Three insert modes: at the cursor, append to the active note, or create a new note with `{{date}}` / `{{time}}` filename templating.
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
## Install
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
### Manual install (current method)
|
||||
|
||||
## Releasing new releases
|
||||
Until the plugin is in the community plugin directory, install manually:
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
1. Build or download the release artifacts: `main.js`, `manifest.json`, `styles.css`.
|
||||
2. Create the folder `<YourVault>/.obsidian/plugins/rewrite-plugin/`.
|
||||
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.
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
### Building from source
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
|
||||
## How to use
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
|
||||
## Manually installing the plugin
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
|
||||
## Improve code quality with eslint
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- This project already has eslint preconfigured, you can invoke a check by running`npm run lint`
|
||||
- Together with a custom eslint [plugin](https://github.com/obsidianmd/eslint-plugin) for Obsidan specific code guidelines.
|
||||
- A GitHub action is preconfigured to automatically lint every commit on all branches.
|
||||
|
||||
## Funding URL
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```bash
|
||||
git clone https://github.com/<your-fork>/rewrite-plugin.git
|
||||
cd rewrite-plugin
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
`main.js`, `styles.css`, and `manifest.json` will be at the repo root. Copy them into your vault as described above.
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
## 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.
|
||||
|
||||
On desktop, the file contains keys encrypted with Electron's `safeStorage` API, which is tied to the user account on that specific machine. The encrypted blob cannot be decrypted on another desktop, on mobile, or in a fresh OS profile. On mobile, keys are stored in plaintext because `safeStorage` is not available.
|
||||
|
||||
For both of those reasons, **you should exclude `secrets.json.nosync` from any vault sync mechanism** and enter keys once per device. Configure the exclusion **before the first sync**, since files already uploaded usually remain on the remote.
|
||||
|
||||
The path to exclude is always:
|
||||
|
||||
```
|
||||
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
### Obsidian Sync (official)
|
||||
|
||||
See https://docs.obsidian.md
|
||||
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).
|
||||
- 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
|
||||
|
||||
Add to `.stignore` in the synced folder root:
|
||||
|
||||
```
|
||||
// ReWrite plugin secrets, never sync API keys
|
||||
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
|
||||
```
|
||||
|
||||
If the vault is not at the Syncthing folder root, omit the leading slash from any patterns.
|
||||
|
||||
### Resilio Sync
|
||||
|
||||
Add this line to `.sync/IgnoreList` on each peer:
|
||||
|
||||
```
|
||||
.obsidian/plugins/rewrite-plugin/secrets.json.nosync
|
||||
```
|
||||
|
||||
### Git / GitHub
|
||||
|
||||
Add to the vault's `.gitignore`:
|
||||
|
||||
```gitignore
|
||||
# ReWrite plugin, never commit API keys
|
||||
.obsidian/plugins/rewrite-plugin/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"
|
||||
```
|
||||
|
||||
### Dropbox
|
||||
|
||||
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.
|
||||
|
||||
Alternatively, delete `secrets.json.nosync` from Dropbox via the web interface after setup; the plugin will recreate it locally when you next enter keys.
|
||||
|
||||
### iCloud Drive
|
||||
|
||||
No configuration needed. iCloud Drive automatically skips any file or folder whose name ends in `.nosync`, which is why the plugin uses that suffix.
|
||||
|
||||
### FolderSync (Android)
|
||||
|
||||
In each sync pair, go to Filters, Excluded files, and add the pattern:
|
||||
|
||||
```
|
||||
secrets.json.nosync
|
||||
```
|
||||
|
||||
## Mobile limitations
|
||||
|
||||
Obsidian on iOS and Android runs in a constrained WebView. A few things behave differently from desktop:
|
||||
|
||||
- **Web Speech (default mobile transcription provider)** is unavailable on iOS Obsidian (WKWebView does not implement `SpeechRecognition`). On Android, support is patchy. If Web Speech is unavailable, the modal surfaces a notice and you can switch to the Paste tab or pick a different transcription provider on your Mobile profile.
|
||||
- **iOS screen-off**: `MediaRecorder` silently stops capturing audio when the screen turns off on iOS. The plugin cannot prevent this; keep the screen on while recording, or use the Paste tab with an OS-level dictation keyboard.
|
||||
- **API keys are stored in plaintext on mobile** because Electron's `safeStorage` is not available. The `secrets.json.nosync` file still uses the `.nosync` filename so iCloud Drive will skip it, but for other sync tools you must apply the exclusion rules above.
|
||||
- **Recording size limit**: clips over 25 MB are rejected. This is a transcription-API limit, not an Obsidian one, and is most likely to bite on long mobile recordings.
|
||||
|
||||
## Known limitations (v1)
|
||||
|
||||
- No audio playback or waveform display.
|
||||
- Raw audio is not saved to the vault, only the cleaned transcript.
|
||||
- LLM responses are not streamed; you see the cleaned output once the whole response arrives.
|
||||
- No speaker diarization.
|
||||
- No long-audio chunking; clips over 25 MB error out instead of being split.
|
||||
- The OpenAI-compatible transcription endpoint expects a server that mirrors Whisper's `/v1/audio/transcriptions` shape (whisper.cpp, faster-whisper-server). The OpenAI-compatible LLM endpoint expects a `/chat/completions` shape (Ollama, LM Studio, etc.).
|
||||
- Anthropic Claude calls go through Obsidian's `requestUrl`, which bypasses browser CORS. If you reuse the same endpoint from another tool that uses `fetch`, you will need their browser-direct-access header.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [Magic Mic](https://github.com/drewmcdonald/obsidian-magic-mic) by Drew McDonald (MIT, archived): originator of the MIME-type fallback and `MediaRecorder` patterns this plugin borrows.
|
||||
- [Scribe](https://github.com/Mikodin/obsidian-scribe) by Mike Alicea: prior art for the record, transcribe, cleanup, and templated-insert flow inside Obsidian.
|
||||
- [obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin): the scaffold this repository was built on.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
|
|
|
|||
Loading…
Reference in a new issue