Skip to content

release: merge develop into main#238

Merged
github-actions[bot] merged 205 commits into
mainfrom
develop
Apr 24, 2026
Merged

release: merge develop into main#238
github-actions[bot] merged 205 commits into
mainfrom
develop

Conversation

@tomymaritano

@tomymaritano tomymaritano commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Release PR

Merges develop into main to trigger a new release via semantic-release.

Highlights since last release

MCP server: sql.js → better-sqlite3

  • FTS5 triggers now work (was crashing with no such module: fts5)
  • WAL mode for safe concurrent access with the desktop app
  • Runtime FTS5 check at startup with descriptive error

Plugin system fixes (from code review)

  • Built-in plugins no longer briefly activate before enabled state loads
  • IPC listener cleanup no longer nukes unrelated listeners

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Built-in plugins can now be toggled on/off in settings; enabled/disabled states persist across sessions
    • Improved markdown editor URL auto-linking for more reliable link creation
  • Bug Fixes

    • Fixed URL auto-linking to correctly identify URLs when document content changes
  • Tests

    • Added comprehensive test suite for database trigger functionality
  • Chores

    • Migrated database backend for improved performance and stability

tomymaritano and others added 30 commits March 11, 2026 01:41
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Notebooks now sync before notes in syncNow() to ensure note-notebook
dependencies are satisfied. Adds pullNotebooks/pushNotebooks methods
and applyRemoteNotebookChange for bidirectional notebook sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move validateNotebookTree from inline test definition to a shared module
so it can be reused by the API route and other consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add conflict state to SyncStatusIndicator with amber warning icon
and count. Conflicts now take priority over idle state so users
discover them without navigating to Settings.

Also export ConflictResolver from sync components barrel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DatabaseConnection.transaction() already calls the inner fn — no need
for extra () at call site.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix pullNotebooks() to only advance cursor to last successfully
  applied change (prevents skipping failed changes on retry)
- Fix tree validation snapshot to properly exclude deleted notebooks
  (prevents ghost parent references in validation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add bidirectional notebook sync
test: add sync-core unit tests (62 tests)
feat: surface sync conflicts in status indicator
# Conflicts:
#	apps/desktop/src/main/services/apiClient.ts
#	apps/desktop/src/main/services/syncService.ts
#	packages/api/src/db/schema.ts
#	packages/api/src/routes/sync.ts
#	packages/storage-sqlite/src/migrations/index.ts
Configure automated code review with path-specific instructions
for core, storage, desktop, and API packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ration

Add optional metadata (name, version, priority) to registerRemarkPlugin
and registerRehypePlugin signatures for debugging and execution ordering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tomymaritano and others added 5 commits April 23, 2026 21:22
lucide-react v1.0 removed brand icons. Local SVG replacements.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…231)

## Summary

### New Features
- **Local HTTP API** (port 29168): REST API with bearer auth for
Alfred/Raycast/curl integration
- **Quick Capture** (Cmd+Shift+N): frameless floating window,
always-on-top, auto-focus
- **Mermaid diagrams**: code block renderer with "Open in Mermaid Live"
button
- **Math/LaTeX**: styled code block renderer with copy button
- **Vim mode**: plugin stub with toggle command (ready for
@codemirror/vim)

### Bug Fix
- **ASAR crash fix**: `ERR_PACKAGE_PATH_NOT_EXPORTED` — workspace
packages now bundled by electron-vite instead of externalized to
node_modules

### Verified
- Note window already shows only editor (no sidebar) — confirmed correct

## Test plan
- [x] `pnpm typecheck` — 17/17 pass
- [x] `pnpm build` (desktop) — builds successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Quick Capture window with Cmd/Ctrl+Shift+N for rapid note entry
(floating, frameless).
* Local HTTP server controls (start/stop/status/get token) accessible
from the app.
  * Math & LaTeX plugin: render math blocks and copy LaTeX.
  * Mermaid plugin: render Mermaid blocks and copy source.
  * Vim Mode plugin: toggleable Vim Mode command and status indicator.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes ERR_PACKAGE_PATH_NOT_EXPORTED crash. Workspace packages already
bundled by electron-vite.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

Moves workspace packages to devDependencies so electron-builder won't
copy them into the ASAR.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

## Summary

- **Allow disabling built-in plugins** — users can now toggle built-in
plugins on/off from settings
- **Fix MCP server FTS5 crash** — replaced `sql.js` (WASM, no FTS5) with
`better-sqlite3` (native, FTS5 included). Write operations
(create/update/trash notes) were failing with `no such module: fts5`
because the FTS5 triggers in the database fired against a runtime that
didn't support FTS5
- **FTS5 runtime check** — MCP server now verifies FTS5 availability at
startup and fails with a clear error message instead of a cryptic
trigger failure
- **WAL mode** — enables safe concurrent DB access between the Electron
app and MCP server (the old `sql.js` approach loaded the entire DB into
memory, risking data loss on concurrent writes)

## Test plan

- [x] `pnpm test` — all 17 suites pass
- [x] `pnpm typecheck` — clean across all packages
- [x] `pnpm build` — builds successfully
- [x] FTS5 regression tests: INSERT/UPDATE/DELETE triggers execute
without error
- [ ] Manual: run MCP server and verify `readied_create_note` /
`readied_update_note` work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Built-in plugins can now be individually enabled or disabled, with
state persistence across reloads.
* Improved search functionality with full-text search indexing support.

* **Tests**
* Added comprehensive test coverage for search index synchronization and
database operations.

* **Chores**
  * Upgraded database layer for enhanced performance and reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
readide Error Error Apr 24, 2026 5:10pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@tomymaritano has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 22 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b88d331c-e859-44cd-9e14-66ae93aafdd3

📥 Commits

Reviewing files that changed from the base of the PR and between 32f522a and 3c7fab8.

📒 Files selected for processing (3)
  • packages/mcp-server/src/__tests__/fts5-triggers.test.ts
  • packages/mcp-server/src/db.ts
  • packages/mcp-server/src/index.ts
📝 Walkthrough

Walkthrough

The pull request migrates the MCP server from sql.js to better-sqlite3, implements built-in plugin toggle functionality in the desktop settings UI, and improves error handling and behavior in the preload API and markdown editor.

Changes

Cohort / File(s) Summary
Built-in Plugin Toggle Feature
apps/desktop/src/renderer/App.tsx, apps/desktop/src/renderer/pages/settings/sections/plugins/PluginCard.tsx, apps/desktop/src/renderer/pages/settings/sections/plugins/index.tsx
Built-in plugins now load enabled/disabled state from the database, filter based on that state in the plugin list, and allow toggling via the UI with persistence and main window reload.
Preload API Enhancements
apps/desktop/src/preload/api/localServer.ts, apps/desktop/src/preload/api/settings.ts
getToken now validates the IPC response shape and throws on error; event handler registration switched to named function cleanup instead of clearing all listeners.
Markdown URL Auto-link Improvement
apps/desktop/src/renderer/components/MarkdownEditor.tsx
URL range tracking improved by recording exact insertion positions and verifying document content at the recorded range rather than searching for moved text.
Database Migration (sql.js → better-sqlite3)
packages/mcp-server/src/db.ts, packages/mcp-server/src/index.ts, packages/mcp-server/src/sql.js.d.ts, packages/mcp-server/package.json
Database layer switched from async sql.js (WASM) to synchronous better-sqlite3, removing persistence calls and FTS5 availability check added on startup.
Database Tests & Configuration
packages/mcp-server/src/__tests__/fts5-triggers.test.ts, packages/mcp-server/tsconfig.json
New FTS5 trigger test suite for better-sqlite3; TypeScript config now excludes test files from compilation.
Changelog Update
CHANGELOG.md
Formatting change: bullet markers updated from * to - in v0.14.0–v0.14.2 entries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ci

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'release: merge develop into main' accurately describes the PR's primary purpose: merging the develop branch into main to trigger a semantic release.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…er cleanup (#239)

## Summary

This commit was pushed to #237 after the squash-merge, so it didn't make
it into develop. Cherry-picked here.

- **P1: gate built-in plugins until enabled state loads** —
`builtInEnabledMap` started as `{}` so `!== false` passed for every
plugin, briefly activating disabled plugins on launch. Now starts as
`null` and built-in plugins are excluded from `PluginHost` until
`listState()` resolves
- **P2: fix IPC listener cleanup** — `ipc.on()` cleanup used
`removeAllListeners(channel)` which nuked other listeners on the same
channel (e.g. `pluginRuntimeStore`). Changed to `removeListener` with
the specific handler reference

## Test plan

- [x] `pnpm typecheck` — clean
- [x] `pnpm test` — all pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	apps/desktop/src/preload/api/settings.ts
#	apps/desktop/src/renderer/pages/settings/sections/plugins/PluginCard.tsx
#	apps/desktop/src/renderer/pages/settings/sections/plugins/index.tsx
#	packages/mcp-server/package.json
#	pnpm-lock.yaml
@github-actions github-actions Bot enabled auto-merge (squash) April 24, 2026 16:43
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file app:desktop size/XL labels Apr 24, 2026
@tomymaritano tomymaritano disabled auto-merge April 24, 2026 16:44
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

https://github.com/tomymaritano/readide/blob/272c85fd69c1fea84b52af473df7cf999566b858/apps/desktop/src/preload/api/localServer.ts#L13
P2 Badge Align local server token API with IPC payload

LocalServerAPI.getToken is declared as Promise<string>, but createLocalServerApi() returns the raw ipcRenderer.invoke('localServer:getToken') result, and the main handler returns an object ({ ok, value } or { ok, error }). Any caller that treats this as a string will send invalid bearer tokens (for example Bearer [object Object]) and fail local API auth. Either unwrap value in preload or change the exposed type/contract to the object shape.


https://github.com/tomymaritano/readide/blob/272c85fd69c1fea84b52af473df7cf999566b858/apps/desktop/src/renderer/components/MarkdownEditor.tsx#L585
P2 Badge Replace URL using exact pasted range

The auto-link flow finds the just-pasted URL with currentDoc.indexOf(plainText, from > 20 ? from - 20 : 0), which can match an earlier identical URL near the cursor (or after concurrent edits) and rewrite the wrong text. In that case the existing URL is converted to markdown while the newly pasted URL stays plain. Use the inserted range/transaction anchor instead of a fuzzy document search.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…#240)

## Summary

Fixes two P2 findings from Codex review on PR #238.

- **`localServer.getToken()` type mismatch** — IPC handler returns `{
ok, value }` but preload declared `Promise<string>` and passed the raw
object through. Any caller would send `Bearer [object Object]`. Now
unwraps `value` in preload and throws on error.
- **Auto-link paste rewrites wrong URL** — `indexOf()` could match an
earlier duplicate URL in the document. Now tracks the exact insert
position (`from`/`to`) and verifies the text at that range still matches
before replacing with the markdown link.

## Test plan

- [x] `pnpm typecheck` — clean
- [x] `pnpm test` — all pass
- [ ] Manual: paste a URL in the editor, verify auto-link replaces the
correct occurrence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
CHANGELOG.md (1)

1-109: 🧹 Nitpick | 🔵 Trivial

Consider consolidating duplicate version sections.

The changelog contains duplicate entries for several versions:

  • v0.14.0 appears at lines 1-11 and 18-22
  • v0.13.0 appears at lines 24-34 and 35-44
  • v0.12.0 appears at lines 51-62 and 63-73
  • v0.9.1 appears at lines 99-104 and 105-109

This duplication reduces changelog readability. Consider consolidating each version into a single canonical section.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 1 - 109, The CHANGELOG contains duplicated release
sections (e.g., headings "## [0.14.0]", "## [0.13.0]", "## [0.12.0]", "##
[0.9.1]"); remove the redundant duplicate blocks and consolidate each version
into a single canonical section preserving all unique entries and links (commit
hashes, PR/issue references) so information is not lost—locate the repeated
headings ("## [0.14.0]", "## [0.13.0]", "## [0.12.0]", "## [0.9.1]") and merge
their content into one section per version, deleting the duplicates.
apps/desktop/src/renderer/pages/settings/sections/plugins/index.tsx (1)

123-138: 🧹 Nitpick | 🔵 Trivial

Minor: shared handleToggle pollutes builtInEnabled with community plugin IDs.

Because this callback fires for both built-in and community toggles, line 129 inserts community plugin IDs into the builtInEnabled map (and conversely, line 127 is a no-op for built-in IDs). It's currently harmless since built-in cards only read builtInEnabled[plugin.id] by their own id, but it's an invariant leak that makes the map misleading.

Consider narrowing the update to the correct bucket:

♻️ Proposed refactor
-  const handleToggle = useCallback(async (pluginId: string, enabled: boolean) => {
+  const builtInIds = useMemo(() => new Set(builtInPlugins.map(p => p.id)), []);
+  const handleToggle = useCallback(async (pluginId: string, enabled: boolean) => {
     try {
       await window.readied.plugins.setEnabled(pluginId, enabled);
-      // Update community plugins state
-      setPlugins(prev => prev.map(p => (p.id === pluginId ? { ...p, enabled } : p)));
-      // Update built-in plugins state
-      setBuiltInEnabled(prev => ({ ...prev, [pluginId]: enabled }));
+      if (builtInIds.has(pluginId)) {
+        setBuiltInEnabled(prev => ({ ...prev, [pluginId]: enabled }));
+      } else {
+        setPlugins(prev => prev.map(p => (p.id === pluginId ? { ...p, enabled } : p)));
+      }
       // Trigger reload in main window so preview updates immediately
       window.readied.plugins.requestReload();
       toast.success(`Plugin ${enabled ? 'enabled' : 'disabled'}`);
     } catch (error) {
       ...
     }
-  }, []);
+  }, [builtInIds]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/pages/settings/sections/plugins/index.tsx` around
lines 123 - 138, The shared handleToggle callback updates both community state
(setPlugins) and the built-in map (setBuiltInEnabled), causing community plugin
IDs to be inserted into builtInEnabled; change handleToggle to detect whether
pluginId is a built-in or community plugin before updating each bucket: locate
handleToggle, setPlugins, setBuiltInEnabled and builtInEnabled and add a
conditional (e.g., check existence in builtInEnabled keys or a provided
isBuiltIn predicate) so only built-in IDs update setBuiltInEnabled and only
community IDs update setPlugins accordingly, leaving the other state untouched;
keep the existing window.readied plugins calls and toast behavior.
packages/mcp-server/src/index.ts (1)

286-298: ⚠️ Potential issue | 🟡 Minor

Top-level main() invocation runs on any import of this module.

Now that createServer is exported (line 298), anything importing @readied/mcp-server (e.g., tests, an embedding host) will also trigger main() at module load — which calls openDb() against the user's real SQLite file and attaches a StdioServerTransport. That's a nasty side effect for a library export.

Gate the side-effectful entry point on import.meta.url === pathToFileURL(process.argv[1]).href (or equivalent) so main() only runs when the file is invoked as the CLI binary.

🔧 Proposed fix
+import { pathToFileURL } from 'url';
+
 ...
-main().catch(err => {
-  console.error('MCP server error:', err);
-  process.exit(1);
-});
+if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
+  main().catch(err => {
+    console.error('MCP server error:', err);
+    process.exit(1);
+  });
+}
 
 export { createServer };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp-server/src/index.ts` around lines 286 - 298, The module
currently calls main() at import which triggers openDb() and
StdioServerTransport; change it so main() only runs when the module is executed
as the CLI by gating the call with an entry-check (compare import.meta.url to
pathToFileURL(process.argv[1]).href or equivalent). Specifically, keep export {
createServer } as-is but move the main().catch(...) invocation behind a
conditional that verifies import.meta.url ===
pathToFileURL(process.argv[1]).href (import pathToFileURL from 'url' if needed),
so imports of the module do not run openDb(), createServer(...).connect(...) or
attach a StdioServerTransport.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/renderer/App.tsx`:
- Around line 443-469: The built-in plugin state mapping
(window.readied.plugins.listState → Record<string, boolean> →
setBuiltInEnabledMap) is duplicated in the initial effect and the plugins:reload
handler; extract a single async helper e.g. loadBuiltInEnabledMap that calls
window.readied.plugins.listState, builds the map from s.pluginId/s.enabled and
calls setBuiltInEnabledMap, then replace the inline IIFEs in the effect that
calls pluginRuntimeStore.getState().init() and in the handler passed to
window.readied.ipc.on('plugins:reload') to both invoke this helper so the logic
is centralized and stays in sync.
- Around line 443-454: The async IIFE in the first useEffect can call
setBuiltInEnabledMap after NotesApp unmounts; add a cancellation guard (e.g., a
local "mounted" boolean or AbortController) inside the effect and check it
before calling setBuiltInEnabledMap after awaiting
window.readied.plugins.listState(); also apply the same guard to the
"plugins:reload" handler in the second effect by ensuring the handler is removed
or checks the mounted flag before calling setState, and clean up the flag/abort
in the effect's return cleanup so no setState runs after unmount.

In `@packages/mcp-server/src/__tests__/fts5-triggers.test.ts`:
- Around line 151-157: The test uses expect(db).toBeTruthy() which only checks a
handle exists; instead verify FTS5 actually works by executing a simple FTS5
operation on the returned connection: use the openDb(':memory:') result and run
a short FTS5 statement (for example create a virtual FTS5 table, insert a row,
and run a query) via the same connection to assert the module is usable, then
close the DB; reference the openDb helper and the test block for where to add
the FTS5 create/insert/query assertions.

In `@packages/mcp-server/src/db.ts`:
- Around line 58-71: The current assertFts5Available function can leave a
persistent _fts5_check table if interrupted and also discards the original
error; update assertFts5Available to perform an idempotent, non-destructive
check and preserve the original error as the cause—either 1) wrap the
create/drop in a safe transaction and use CREATE VIRTUAL TABLE IF NOT EXISTS
_fts5_check ... then DROP TABLE IF EXISTS _fts5_check (so an interrupted run
cannot leave a fatal leftover), or 2) replace the schema-touching check with a
compile-time check using db.prepare("SELECT
sqlite_compileoption_used('ENABLE_FTS5') AS v").get() and throw if v is falsy;
in either case construct the thrown Error with the original caught error as the
cause (e.g., new Error(..., { cause: err })) so the original exception is
preserved.
- Around line 73-79: openDb currently sets db.pragma('journal_mode = WAL')
unconditionally which is a no-op for in-memory DBs; change the function (openDb)
to detect when resolvedPath equals ':memory:' (or when dbPath indicates an
in-memory DB) and skip calling db.pragma for that case so tests and health
checks don't misinterpret the journal mode; keep the pragma for all other paths
and reference resolvedPath and the db instance when implementing the
conditional.

In `@packages/mcp-server/src/index.ts`:
- Line 14: The docstring and query for readied_search_notes are incorrect:
instead of substring LIKE against notes, change the reader to query the FTS5
virtual table notes_fts using MATCH (join notes ON notes.rowid =
notes_fts.rowid) and filter notes.is_deleted = 0; return snippet(notes_fts, ...)
for hit excerpts and use bm25(notes_fts) as the ranking score, ordering by that
rank (ascending) and keeping the LIMIT parameter. Update the SQL in
readied_search_notes to bind the search term to the MATCH clause and adjust
returned columns to include snippet and rank so callers get FTS5
tokenization/ranking instead of plain LIKE results.

---

Outside diff comments:
In `@apps/desktop/src/renderer/pages/settings/sections/plugins/index.tsx`:
- Around line 123-138: The shared handleToggle callback updates both community
state (setPlugins) and the built-in map (setBuiltInEnabled), causing community
plugin IDs to be inserted into builtInEnabled; change handleToggle to detect
whether pluginId is a built-in or community plugin before updating each bucket:
locate handleToggle, setPlugins, setBuiltInEnabled and builtInEnabled and add a
conditional (e.g., check existence in builtInEnabled keys or a provided
isBuiltIn predicate) so only built-in IDs update setBuiltInEnabled and only
community IDs update setPlugins accordingly, leaving the other state untouched;
keep the existing window.readied plugins calls and toast behavior.

In `@CHANGELOG.md`:
- Around line 1-109: The CHANGELOG contains duplicated release sections (e.g.,
headings "## [0.14.0]", "## [0.13.0]", "## [0.12.0]", "## [0.9.1]"); remove the
redundant duplicate blocks and consolidate each version into a single canonical
section preserving all unique entries and links (commit hashes, PR/issue
references) so information is not lost—locate the repeated headings ("##
[0.14.0]", "## [0.13.0]", "## [0.12.0]", "## [0.9.1]") and merge their content
into one section per version, deleting the duplicates.

In `@packages/mcp-server/src/index.ts`:
- Around line 286-298: The module currently calls main() at import which
triggers openDb() and StdioServerTransport; change it so main() only runs when
the module is executed as the CLI by gating the call with an entry-check
(compare import.meta.url to pathToFileURL(process.argv[1]).href or equivalent).
Specifically, keep export { createServer } as-is but move the main().catch(...)
invocation behind a conditional that verifies import.meta.url ===
pathToFileURL(process.argv[1]).href (import pathToFileURL from 'url' if needed),
so imports of the module do not run openDb(), createServer(...).connect(...) or
attach a StdioServerTransport.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8da60b6d-2968-4797-a646-fedf36d578f0

📥 Commits

Reviewing files that changed from the base of the PR and between 6f3e5f1 and 32f522a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • CHANGELOG.md
  • apps/desktop/src/preload/api/localServer.ts
  • apps/desktop/src/preload/api/settings.ts
  • apps/desktop/src/renderer/App.tsx
  • apps/desktop/src/renderer/components/MarkdownEditor.tsx
  • apps/desktop/src/renderer/pages/settings/sections/plugins/PluginCard.tsx
  • apps/desktop/src/renderer/pages/settings/sections/plugins/index.tsx
  • packages/mcp-server/package.json
  • packages/mcp-server/src/__tests__/fts5-triggers.test.ts
  • packages/mcp-server/src/db.ts
  • packages/mcp-server/src/index.ts
  • packages/mcp-server/src/sql.js.d.ts
  • packages/mcp-server/tsconfig.json
💤 Files with no reviewable changes (2)
  • apps/desktop/src/renderer/pages/settings/sections/plugins/PluginCard.tsx
  • packages/mcp-server/src/sql.js.d.ts

Comment on lines 443 to 469
useEffect(() => {
void pluginRuntimeStore.getState().init();
// Load built-in plugin enabled states
void (async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
}, []);

// Re-check built-in enabled state when plugins reload
useEffect(() => {
const handler = () => {
void (async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
};
return window.readied.ipc.on('plugins:reload', handler);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Refactor: deduplicate the built-in state loader.

The same listState → Record<string, boolean> mapping is implemented twice (initial load and plugins:reload handler). Extract a small helper to keep them in sync if the shape ever changes.

♻️ Proposed refactor
+  const loadBuiltInEnabled = useCallback(async () => {
+    const stateList = await window.readied.plugins.listState();
+    const map: Record<string, boolean> = {};
+    for (const s of stateList) {
+      map[s.pluginId] = s.enabled;
+    }
+    setBuiltInEnabledMap(map);
+  }, []);
+
   useEffect(() => {
     void pluginRuntimeStore.getState().init();
-    // Load built-in plugin enabled states
-    void (async () => {
-      const stateList = await window.readied.plugins.listState();
-      const map: Record<string, boolean> = {};
-      for (const s of stateList) {
-        map[s.pluginId] = s.enabled;
-      }
-      setBuiltInEnabledMap(map);
-    })();
-  }, []);
+    void loadBuiltInEnabled();
+  }, [loadBuiltInEnabled]);

   // Re-check built-in enabled state when plugins reload
   useEffect(() => {
-    const handler = () => {
-      void (async () => {
-        const stateList = await window.readied.plugins.listState();
-        const map: Record<string, boolean> = {};
-        for (const s of stateList) {
-          map[s.pluginId] = s.enabled;
-        }
-        setBuiltInEnabledMap(map);
-      })();
-    };
-    return window.readied.ipc.on('plugins:reload', handler);
-  }, []);
+    return window.readied.ipc.on('plugins:reload', () => {
+      void loadBuiltInEnabled();
+    });
+  }, [loadBuiltInEnabled]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
void pluginRuntimeStore.getState().init();
// Load built-in plugin enabled states
void (async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
}, []);
// Re-check built-in enabled state when plugins reload
useEffect(() => {
const handler = () => {
void (async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
};
return window.readied.ipc.on('plugins:reload', handler);
}, []);
const loadBuiltInEnabled = useCallback(async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
}, []);
useEffect(() => {
void pluginRuntimeStore.getState().init();
void loadBuiltInEnabled();
}, [loadBuiltInEnabled]);
// Re-check built-in enabled state when plugins reload
useEffect(() => {
return window.readied.ipc.on('plugins:reload', () => {
void loadBuiltInEnabled();
});
}, [loadBuiltInEnabled]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 443 - 469, The built-in
plugin state mapping (window.readied.plugins.listState → Record<string, boolean>
→ setBuiltInEnabledMap) is duplicated in the initial effect and the
plugins:reload handler; extract a single async helper e.g. loadBuiltInEnabledMap
that calls window.readied.plugins.listState, builds the map from
s.pluginId/s.enabled and calls setBuiltInEnabledMap, then replace the inline
IIFEs in the effect that calls pluginRuntimeStore.getState().init() and in the
handler passed to window.readied.ipc.on('plugins:reload') to both invoke this
helper so the logic is centralized and stays in sync.

Comment on lines 443 to +454
useEffect(() => {
void pluginRuntimeStore.getState().init();
// Load built-in plugin enabled states
void (async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Guard against setState after unmount.

The async IIFE in the mount effect has no cancellation guard. If NotesApp unmounts before listState() resolves (e.g., during fast test teardown or HMR), setBuiltInEnabledMap will fire on an unmounted component. Low risk in production, but trivial to harden:

🛡️ Proposed fix
   useEffect(() => {
     void pluginRuntimeStore.getState().init();
-    // Load built-in plugin enabled states
-    void (async () => {
+    let cancelled = false;
+    void (async () => {
       const stateList = await window.readied.plugins.listState();
+      if (cancelled) return;
       const map: Record<string, boolean> = {};
       for (const s of stateList) {
         map[s.pluginId] = s.enabled;
       }
       setBuiltInEnabledMap(map);
     })();
+    return () => {
+      cancelled = true;
+    };
   }, []);

The same guard is worth applying to the plugins:reload handler in the second effect.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
void pluginRuntimeStore.getState().init();
// Load built-in plugin enabled states
void (async () => {
const stateList = await window.readied.plugins.listState();
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
}, []);
useEffect(() => {
void pluginRuntimeStore.getState().init();
let cancelled = false;
void (async () => {
const stateList = await window.readied.plugins.listState();
if (cancelled) return;
const map: Record<string, boolean> = {};
for (const s of stateList) {
map[s.pluginId] = s.enabled;
}
setBuiltInEnabledMap(map);
})();
return () => {
cancelled = true;
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 443 - 454, The async IIFE in
the first useEffect can call setBuiltInEnabledMap after NotesApp unmounts; add a
cancellation guard (e.g., a local "mounted" boolean or AbortController) inside
the effect and check it before calling setBuiltInEnabledMap after awaiting
window.readied.plugins.listState(); also apply the same guard to the
"plugins:reload" handler in the second effect by ensuring the handler is removed
or checks the mounted flag before calling setState, and clean up the flag/abort
in the effect's return cleanup so no setState runs after unmount.

Comment thread packages/mcp-server/src/__tests__/fts5-triggers.test.ts
Comment on lines +58 to +71
function assertFts5Available(db: Database.Database): void {
try {
db.prepare('CREATE VIRTUAL TABLE _fts5_check USING fts5(x)').run();
db.prepare('DROP TABLE _fts5_check').run();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(
`FTS5 module is not available in this SQLite build.\n` +
`The Readied database uses FTS5 for full-text search triggers.\n` +
`Without FTS5, write operations (create/update/delete notes) will fail.\n` +
`Original error: ${message}`
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

assertFts5Available can brick the DB on a crashed run.

CREATE VIRTUAL TABLE _fts5_check USING fts5(x) (line 60) is not wrapped in IF NOT EXISTS and is not in a transaction. If the process is killed between lines 60 and 61 (SIGKILL, OOM, power loss), _fts5_check will persist in the user's real readied.db. On the next startup:

  • Line 60 throws table _fts5_check already exists.
  • The catch block re-throws as FTS5 module is not available in this SQLite build. — a false negative that prevents the server from ever starting until the user manually drops the table.

On top of that, the thrown Error drops the original exception (ESLint preserve-caught-error is valid here). Both issues share a fix:

🛡️ Proposed fix — idempotent check + preserved cause
 function assertFts5Available(db: Database.Database): void {
   try {
-    db.prepare('CREATE VIRTUAL TABLE _fts5_check USING fts5(x)').run();
-    db.prepare('DROP TABLE _fts5_check').run();
+    db.prepare('CREATE VIRTUAL TABLE IF NOT EXISTS _fts5_check USING fts5(x)').run();
+    db.prepare('DROP TABLE IF EXISTS _fts5_check').run();
   } catch (err) {
     const message = err instanceof Error ? err.message : String(err);
     throw new Error(
       `FTS5 module is not available in this SQLite build.\n` +
         `The Readied database uses FTS5 for full-text search triggers.\n` +
         `Without FTS5, write operations (create/update/delete notes) will fail.\n` +
         `Original error: ${message}`,
+      { cause: err }
     );
   }
 }

A non-destructive alternative worth considering is:

const [{ v }] = db.prepare("SELECT sqlite_compileoption_used('ENABLE_FTS5') AS v").all() as { v: number }[];
if (!v) throw new Error('FTS5 not compiled in');

which avoids touching the user's schema entirely.

🧰 Tools
🪛 ESLint

[error] 64-69: There is no cause attached to the symptom error being thrown.

(preserve-caught-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp-server/src/db.ts` around lines 58 - 71, The current
assertFts5Available function can leave a persistent _fts5_check table if
interrupted and also discards the original error; update assertFts5Available to
perform an idempotent, non-destructive check and preserve the original error as
the cause—either 1) wrap the create/drop in a safe transaction and use CREATE
VIRTUAL TABLE IF NOT EXISTS _fts5_check ... then DROP TABLE IF EXISTS
_fts5_check (so an interrupted run cannot leave a fatal leftover), or 2) replace
the schema-touching check with a compile-time check using db.prepare("SELECT
sqlite_compileoption_used('ENABLE_FTS5') AS v").get() and throw if v is falsy;
in either case construct the thrown Error with the original caught error as the
cause (e.g., new Error(..., { cause: err })) so the original exception is
preserved.

Comment thread packages/mcp-server/src/db.ts
Comment thread packages/mcp-server/src/index.ts
## Summary

Addresses two major CodeRabbit findings on PR #238.

### 1. Non-destructive FTS5 check (was Major)
`assertFts5Available` used `CREATE VIRTUAL TABLE` + `DROP TABLE` to
probe FTS5. If the process crashed between those two statements, a stale
`_fts5_check` table would persist and block all future startups with a
false "FTS5 not available" error.

**Fix**: Use `sqlite_compileoption_used('ENABLE_FTS5')` — a read-only
query that never touches the schema.

### 2. Search now actually uses FTS5 (was Major)
The docstring and PR description said search used FTS5, but the query
was still `LIKE '%term%'`. The FTS5 triggers were maintaining the
`notes_fts` index on every write, but no reader ever queried it.

**Fix**: `readied_search_notes` now uses `notes_fts MATCH` with `bm25()`
ranking and `snippet()` highlights.

### Also
- Skip WAL pragma for `:memory:` databases (no-op but avoids confusion
in tests/health checks)

## Test plan

- [x] `pnpm --filter @readied/mcp-server build` — clean
- [x] `pnpm --filter @readied/mcp-server test` — 5/5 pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot enabled auto-merge (squash) April 24, 2026 17:08
@github-actions github-actions Bot merged commit 8a5c471 into main Apr 24, 2026
15 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:desktop dependencies Pull requests that update a dependency file size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant