bear-notes-mcp 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = '2.4.0';
1
+ export const APP_VERSION = '2.4.1';
2
2
  export const BEAR_URL_SCHEME = 'bear://x-callback-url/';
3
3
  export const CORE_DATA_EPOCH_OFFSET = 978307200; // 2001-01-01 to Unix epoch
4
4
  export const DEFAULT_SEARCH_LIMIT = 50;
package/dist/database.js CHANGED
@@ -29,7 +29,7 @@ export function openBearDatabase() {
29
29
  const databasePath = getBearDatabasePath();
30
30
  logger.info(`Opening Bear database at: ${databasePath}`);
31
31
  try {
32
- const db = new DatabaseSync(databasePath);
32
+ const db = new DatabaseSync(databasePath, { readOnly: true });
33
33
  logger.debug('Bear database opened successfully');
34
34
  return db;
35
35
  }
package/dist/notes.js CHANGED
@@ -1,6 +1,23 @@
1
1
  import { DEFAULT_SEARCH_LIMIT } from './config.js';
2
2
  import { convertCoreDataTimestamp, convertDateToCoreDataTimestamp, logAndThrow, logger, parseDateString, } from './utils.js';
3
3
  import { openBearDatabase } from './database.js';
4
+ // SQL equivalent of decodeTagName() in tags.ts — both MUST apply the same transformations
5
+ const DECODED_TAG_TITLE = "LOWER(TRIM(REPLACE(t.ZTITLE, '+', ' ')))";
6
+ /**
7
+ * Builds a SQL WHERE clause that matches a tag exactly or its nested children.
8
+ * Escapes LIKE wildcards (%, _) in the tag name to prevent unintended pattern matching.
9
+ */
10
+ function buildTagMatchClause(tag) {
11
+ const normalizedTag = tag.trim().toLowerCase();
12
+ const escapedTag = normalizedTag.replace(/[%_\\]/g, '\\$&');
13
+ return {
14
+ sql: ` AND (
15
+ ${DECODED_TAG_TITLE} = ?
16
+ OR ${DECODED_TAG_TITLE} LIKE ? || '/%' ESCAPE '\\'
17
+ )`,
18
+ params: [normalizedTag, escapedTag],
19
+ };
20
+ }
4
21
  function formatBearNote(row) {
5
22
  const title = row.title || 'Untitled';
6
23
  const identifier = row.identifier;
@@ -150,6 +167,11 @@ export function searchNotes(searchTerm, tag, limit, dateFilter, pinned) {
150
167
  innerQuery += `
151
168
  JOIN Z_5PINNEDINTAGS pt ON pt.Z_5PINNEDNOTES = note.Z_PK
152
169
  JOIN ZSFNOTETAG t ON t.Z_PK = pt.Z_13PINNEDINTAGS`;
170
+ }
171
+ else if (hasTag) {
172
+ innerQuery += `
173
+ JOIN Z_5TAGS nt ON nt.Z_5NOTES = note.Z_PK
174
+ JOIN ZSFNOTETAG t ON t.Z_PK = nt.Z_13TAGS`;
153
175
  }
154
176
  innerQuery += `
155
177
  WHERE note.ZARCHIVED = 0
@@ -162,24 +184,17 @@ export function searchNotes(searchTerm, tag, limit, dateFilter, pinned) {
162
184
  innerQuery += ' AND (note.ZTITLE LIKE ? OR note.ZTEXT LIKE ? OR f.ZSEARCHTEXT LIKE ?)';
163
185
  queryParams.push(searchPattern, searchPattern, searchPattern);
164
186
  }
165
- // Pinned and tag filtering - behavior depends on combination
166
- if (hasPinnedFilter && hasTag) {
167
- // Notes pinned within specific tag view (via Z_5PINNEDINTAGS)
168
- const tagPattern = `%${tag.trim()}%`;
169
- innerQuery += ' AND t.ZTITLE LIKE ?';
170
- queryParams.push(tagPattern);
187
+ // Tag clause applies to both pinned+tag and tag-only paths (JOINs differ above)
188
+ if (hasTag) {
189
+ const tagClause = buildTagMatchClause(tag);
190
+ innerQuery += tagClause.sql;
191
+ queryParams.push(...tagClause.params);
171
192
  }
172
193
  else if (hasPinnedFilter) {
173
194
  // All pinned notes: globally pinned OR pinned in any tag (matches Bear's "Pinned" section)
174
195
  innerQuery +=
175
196
  ' AND (note.ZPINNED = 1 OR EXISTS (SELECT 1 FROM Z_5PINNEDINTAGS pt WHERE pt.Z_5PINNEDNOTES = note.Z_PK))';
176
197
  }
177
- else if (hasTag) {
178
- // Text-based tag search
179
- const tagPattern = `%#${tag.trim()}%`;
180
- innerQuery += ' AND note.ZTEXT LIKE ?';
181
- queryParams.push(tagPattern);
182
- }
183
198
  // Add date filtering
184
199
  if (hasDateFilter && dateFilter) {
185
200
  if (dateFilter.createdAfter) {
package/dist/tags.js CHANGED
@@ -5,6 +5,7 @@ import { openBearDatabase } from './database.js';
5
5
  * - Replaces '+' with spaces (Bear's URL encoding)
6
6
  * - Converts to lowercase (matches Bear UI behavior)
7
7
  * - Trims whitespace
8
+ * Keep in sync with DECODED_TAG_TITLE in notes.ts — both MUST apply the same transformations.
8
9
  */
9
10
  function decodeTagName(encodedName) {
10
11
  return encodedName.replace(/\+/g, ' ').trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bear-notes-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Bear Notes MCP server with TypeScript and native SQLite",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -30,9 +30,9 @@
30
30
  "@modelcontextprotocol/inspector": "^0.20.0",
31
31
  "@types/debug": "^4.1.12",
32
32
  "@types/node": "^24.10.13",
33
- "@typescript-eslint/eslint-plugin": "^8.55.0",
34
- "@typescript-eslint/parser": "^8.55.0",
35
- "eslint": "^9.39.2",
33
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
34
+ "@typescript-eslint/parser": "^8.56.0",
35
+ "eslint": "^9.39.3",
36
36
  "eslint-plugin-import": "^2.32.0",
37
37
  "prettier": "^3.8.1",
38
38
  "tsx": "^4.21.0",