@cardmagic/notes 1.0.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/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/attachments.d.ts +10 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +122 -0
- package/dist/attachments.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +180 -0
- package/dist/cli.js.map +1 -0
- package/dist/formatter.d.ts +12 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +129 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +24 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +483 -0
- package/dist/indexer.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +274 -0
- package/dist/mcp.js.map +1 -0
- package/dist/searcher.d.ts +17 -0
- package/dist/searcher.d.ts.map +1 -0
- package/dist/searcher.js +203 -0
- package/dist/searcher.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function formatDate(unixTimestamp) {
|
|
3
|
+
if (!unixTimestamp || unixTimestamp <= 0) {
|
|
4
|
+
return 'Unknown';
|
|
5
|
+
}
|
|
6
|
+
return new Date(unixTimestamp * 1000).toLocaleDateString('en-US', {
|
|
7
|
+
year: 'numeric',
|
|
8
|
+
month: 'short',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
hour: '2-digit',
|
|
11
|
+
minute: '2-digit',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function formatNote(note, showBody = false) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
// Title with pin indicator
|
|
17
|
+
const titlePrefix = note.isPinned ? chalk.yellow('📌 ') : '';
|
|
18
|
+
const lockSuffix = note.isLocked ? chalk.red(' 🔒') : '';
|
|
19
|
+
lines.push(titlePrefix + chalk.bold.cyan(note.title || 'Untitled') + lockSuffix);
|
|
20
|
+
// Folder
|
|
21
|
+
lines.push(chalk.dim(`📁 ${note.folder}`));
|
|
22
|
+
// Snippet
|
|
23
|
+
if (note.snippet) {
|
|
24
|
+
lines.push(chalk.gray(note.snippet));
|
|
25
|
+
}
|
|
26
|
+
// Body preview if requested
|
|
27
|
+
if (showBody && note.body) {
|
|
28
|
+
const bodyPreview = note.body.slice(0, 200).replace(/\n/g, ' ');
|
|
29
|
+
lines.push(chalk.dim(bodyPreview + (note.body.length > 200 ? '...' : '')));
|
|
30
|
+
}
|
|
31
|
+
// Modified date
|
|
32
|
+
lines.push(chalk.dim(`Modified: ${formatDate(note.modifiedAt)}`));
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
export function formatSearchResult(result, query) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
// Match indicator and title
|
|
38
|
+
const titlePrefix = result.isPinned ? chalk.yellow('📌 ') : '';
|
|
39
|
+
const lockSuffix = result.isLocked ? chalk.red(' 🔒') : '';
|
|
40
|
+
const highlightedTitle = highlightMatches(result.title || 'Untitled', query);
|
|
41
|
+
lines.push(chalk.green('▶ ') + titlePrefix + chalk.bold.cyan(highlightedTitle) + lockSuffix);
|
|
42
|
+
// Folder
|
|
43
|
+
lines.push(chalk.dim(` 📁 ${result.folder}`));
|
|
44
|
+
// Highlighted snippet
|
|
45
|
+
if (result.snippet) {
|
|
46
|
+
const highlightedSnippet = highlightMatches(result.snippet, query);
|
|
47
|
+
lines.push(` ${highlightedSnippet}`);
|
|
48
|
+
}
|
|
49
|
+
// Score and matched terms
|
|
50
|
+
lines.push(chalk.dim(` Score: ${result.score.toFixed(2)} | Terms: ${result.matchedTerms.join(', ')}`));
|
|
51
|
+
// Modified date
|
|
52
|
+
lines.push(chalk.dim(` Modified: ${formatDate(result.modifiedAt)}`));
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
function highlightMatches(text, query) {
|
|
56
|
+
const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 0);
|
|
57
|
+
let highlighted = text;
|
|
58
|
+
for (const term of terms) {
|
|
59
|
+
const regex = new RegExp(`(${escapeRegex(term)})`, 'gi');
|
|
60
|
+
highlighted = highlighted.replace(regex, chalk.bgYellow.black('$1'));
|
|
61
|
+
}
|
|
62
|
+
return highlighted;
|
|
63
|
+
}
|
|
64
|
+
function escapeRegex(str) {
|
|
65
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
66
|
+
}
|
|
67
|
+
export function formatFolder(folder) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
lines.push(chalk.bold.magenta(`📁 ${folder.name}`));
|
|
70
|
+
lines.push(chalk.dim(` ${folder.noteCount} notes | Last modified: ${formatDate(folder.lastModified)}`));
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
export function formatStats(stats) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push(chalk.bold.cyan('📊 Notes Index Statistics'));
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(` Total Notes: ${chalk.bold(stats.totalNotes.toString())}`);
|
|
78
|
+
lines.push(` Total Folders: ${chalk.bold(stats.totalFolders.toString())}`);
|
|
79
|
+
lines.push(` Indexed At: ${chalk.dim(stats.indexedAt)}`);
|
|
80
|
+
if (stats.oldestNote) {
|
|
81
|
+
lines.push(` Oldest Note: ${chalk.dim(stats.oldestNote)}`);
|
|
82
|
+
}
|
|
83
|
+
if (stats.newestNote) {
|
|
84
|
+
lines.push(` Newest Note: ${chalk.dim(stats.newestNote)}`);
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
export function formatIndexProgress(progress) {
|
|
89
|
+
const { phase, current, total } = progress;
|
|
90
|
+
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
91
|
+
const barWidth = 30;
|
|
92
|
+
const filledWidth = Math.round((percent / 100) * barWidth);
|
|
93
|
+
const bar = '█'.repeat(filledWidth) + '░'.repeat(barWidth - filledWidth);
|
|
94
|
+
let phaseText;
|
|
95
|
+
switch (phase) {
|
|
96
|
+
case 'extracting-pdfs':
|
|
97
|
+
phaseText = chalk.magenta('Extracting PDFs...');
|
|
98
|
+
break;
|
|
99
|
+
case 'reading':
|
|
100
|
+
phaseText = chalk.yellow('Reading notes...');
|
|
101
|
+
break;
|
|
102
|
+
case 'indexing':
|
|
103
|
+
phaseText = chalk.blue('Building index...');
|
|
104
|
+
break;
|
|
105
|
+
case 'done':
|
|
106
|
+
phaseText = chalk.green('Done!');
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
return `${phaseText}\n[${chalk.cyan(bar)}] ${percent}% (${current}/${total})`;
|
|
110
|
+
}
|
|
111
|
+
export function formatNoteList(notes) {
|
|
112
|
+
if (notes.length === 0) {
|
|
113
|
+
return chalk.dim('No notes found.');
|
|
114
|
+
}
|
|
115
|
+
return notes.map(note => formatNote(note)).join('\n\n');
|
|
116
|
+
}
|
|
117
|
+
export function formatSearchResults(results, query) {
|
|
118
|
+
if (results.length === 0) {
|
|
119
|
+
return chalk.dim(`No results found for "${query}".`);
|
|
120
|
+
}
|
|
121
|
+
return results.map(result => formatSearchResult(result, query)).join('\n\n');
|
|
122
|
+
}
|
|
123
|
+
export function formatFolderList(folders) {
|
|
124
|
+
if (folders.length === 0) {
|
|
125
|
+
return chalk.dim('No folders found.');
|
|
126
|
+
}
|
|
127
|
+
return folders.map(folder => formatFolder(folder)).join('\n\n');
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../src/formatter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const args = process.argv.slice(2);
|
|
3
|
+
if (args.includes('--mcp')) {
|
|
4
|
+
// Run as MCP server
|
|
5
|
+
import('./mcp.js').then(({ runMcpServer }) => {
|
|
6
|
+
runMcpServer().catch(console.error);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// Run as CLI
|
|
11
|
+
import('./cli.js').then(({ runCli }) => {
|
|
12
|
+
runCli();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;IAC3B,oBAAoB;IACpB,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE;QAC3C,YAAY,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,CAAC;IACN,aAAa;IACb,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;QACrC,MAAM,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IndexStats } from './types.js';
|
|
2
|
+
export declare function indexNeedsRebuild(): boolean;
|
|
3
|
+
export interface IndexProgress {
|
|
4
|
+
phase: 'reading' | 'extracting-pdfs' | 'indexing' | 'done';
|
|
5
|
+
current: number;
|
|
6
|
+
total: number;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Incremental index update - only processes changed notes
|
|
11
|
+
*/
|
|
12
|
+
export declare function updateIndex(onProgress?: (progress: IndexProgress) => void): Promise<IndexStats & {
|
|
13
|
+
updated: number;
|
|
14
|
+
deleted: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Full index rebuild - processes all notes from scratch
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildIndex(onProgress?: (progress: IndexProgress) => void): Promise<IndexStats>;
|
|
20
|
+
export declare function ensureIndex(): Promise<void>;
|
|
21
|
+
export declare function getStats(): IndexStats | null;
|
|
22
|
+
export declare function getIndexDbPath(): string;
|
|
23
|
+
export declare function getFuzzyIndexPath(): string;
|
|
24
|
+
//# sourceMappingURL=indexer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAwB,UAAU,EAAE,MAAM,YAAY,CAAC;AAkJnE,wBAAgB,iBAAiB,IAAI,OAAO,CAY3C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,GAAG,iBAAiB,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA2FD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,GAC7C,OAAO,CAAC,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAmK5D;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,GAC7C,OAAO,CAAC,UAAU,CAAC,CA0HrB;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAQjD;AAED,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAS5C;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
package/dist/indexer.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { gunzipSync } from 'node:zlib';
|
|
6
|
+
import MiniSearch from 'minisearch';
|
|
7
|
+
import { appleToUnix } from './types.js';
|
|
8
|
+
import { extractAllPdfText, getNoteKey } from './attachments.js';
|
|
9
|
+
const NOTES_DB_PATH = join(homedir(), 'Library/Group Containers/group.com.apple.notes/NoteStore.sqlite');
|
|
10
|
+
const INDEX_DIR = join(homedir(), '.notes');
|
|
11
|
+
const INDEX_DB_PATH = join(INDEX_DIR, 'index.db');
|
|
12
|
+
const FUZZY_INDEX_PATH = join(INDEX_DIR, 'fuzzy.json');
|
|
13
|
+
const STATS_PATH = join(INDEX_DIR, 'stats.json');
|
|
14
|
+
// Entity type constants from Z_PRIMARYKEY
|
|
15
|
+
const ENTITY_NOTE = 12;
|
|
16
|
+
const ENTITY_FOLDER = 15;
|
|
17
|
+
function ensureIndexDir() {
|
|
18
|
+
if (!existsSync(INDEX_DIR)) {
|
|
19
|
+
mkdirSync(INDEX_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function extractTextFromNoteData(data) {
|
|
23
|
+
if (!data || data.length === 0) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
// The data is gzip compressed
|
|
28
|
+
const decompressed = gunzipSync(data);
|
|
29
|
+
// Extract readable text from the protobuf-like format
|
|
30
|
+
// The text is stored as UTF-8 strings within the binary data
|
|
31
|
+
const text = extractStringsFromBuffer(decompressed);
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function extractStringsFromBuffer(buffer) {
|
|
39
|
+
const strings = [];
|
|
40
|
+
let currentString = '';
|
|
41
|
+
let inString = false;
|
|
42
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
43
|
+
const byte = buffer[i];
|
|
44
|
+
// Check if this is a printable ASCII or common UTF-8 character
|
|
45
|
+
if ((byte >= 0x20 && byte <= 0x7E) || byte === 0x0A || byte === 0x0D || byte === 0x09) {
|
|
46
|
+
currentString += String.fromCharCode(byte);
|
|
47
|
+
inString = true;
|
|
48
|
+
}
|
|
49
|
+
else if (byte >= 0xC0 && byte <= 0xF7 && i + 1 < buffer.length) {
|
|
50
|
+
// Handle UTF-8 multi-byte sequences
|
|
51
|
+
let charLen = 1;
|
|
52
|
+
if ((byte & 0xE0) === 0xC0)
|
|
53
|
+
charLen = 2;
|
|
54
|
+
else if ((byte & 0xF0) === 0xE0)
|
|
55
|
+
charLen = 3;
|
|
56
|
+
else if ((byte & 0xF8) === 0xF0)
|
|
57
|
+
charLen = 4;
|
|
58
|
+
if (i + charLen <= buffer.length) {
|
|
59
|
+
try {
|
|
60
|
+
const utf8Char = buffer.slice(i, i + charLen).toString('utf8');
|
|
61
|
+
if (utf8Char.length === 1 && utf8Char.charCodeAt(0) >= 0x80) {
|
|
62
|
+
currentString += utf8Char;
|
|
63
|
+
i += charLen - 1;
|
|
64
|
+
inString = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Not valid UTF-8
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (inString && currentString.length >= 3) {
|
|
73
|
+
strings.push(currentString.trim());
|
|
74
|
+
}
|
|
75
|
+
currentString = '';
|
|
76
|
+
inString = false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (inString && currentString.length >= 3) {
|
|
80
|
+
strings.push(currentString.trim());
|
|
81
|
+
}
|
|
82
|
+
currentString = '';
|
|
83
|
+
inString = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (currentString.length >= 3) {
|
|
87
|
+
strings.push(currentString.trim());
|
|
88
|
+
}
|
|
89
|
+
// Join strings and clean up
|
|
90
|
+
const text = strings
|
|
91
|
+
.filter(s => s.length >= 2)
|
|
92
|
+
.join('\n')
|
|
93
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
94
|
+
// Remove any remaining non-printable characters except newlines/tabs
|
|
95
|
+
.replace(/[^\x20-\x7E\n\t\u00A0-\uFFFF]/g, '')
|
|
96
|
+
.trim();
|
|
97
|
+
// Filter out garbage lines and detect where garbage starts
|
|
98
|
+
const lines = text.split('\n');
|
|
99
|
+
// Helper to detect if a line is garbage
|
|
100
|
+
const isGarbageLine = (line) => {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (trimmed.length === 0)
|
|
103
|
+
return false; // Empty lines aren't garbage
|
|
104
|
+
// Lines with characters outside basic Latin + common punctuation
|
|
105
|
+
const hasExoticChars = /[^\x20-\x7E\u00A0-\u00FF]/.test(trimmed);
|
|
106
|
+
// Short lines with exotic chars are definitely garbage
|
|
107
|
+
if (hasExoticChars && trimmed.length < 20)
|
|
108
|
+
return true;
|
|
109
|
+
// Lines that are mostly non-alphanumeric
|
|
110
|
+
const alphanumCount = (trimmed.match(/[a-zA-Z0-9]/g) || []).length;
|
|
111
|
+
if (alphanumCount < trimmed.length * 0.4 && trimmed.length < 15)
|
|
112
|
+
return true;
|
|
113
|
+
// Short alphanumeric-only codes that repeat (like "F>LL", "MbH", etc.)
|
|
114
|
+
if (trimmed.length <= 5 && /^[A-Za-z0-9>\\]+$/.test(trimmed))
|
|
115
|
+
return true;
|
|
116
|
+
return false;
|
|
117
|
+
};
|
|
118
|
+
// Find where a garbage block starts (3+ consecutive garbage lines)
|
|
119
|
+
let cutoffIndex = lines.length;
|
|
120
|
+
let consecutiveGarbage = 0;
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
if (isGarbageLine(lines[i])) {
|
|
123
|
+
consecutiveGarbage++;
|
|
124
|
+
if (consecutiveGarbage >= 3) {
|
|
125
|
+
// Found garbage block, cut from where it started
|
|
126
|
+
cutoffIndex = i - consecutiveGarbage + 1;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (lines[i].trim().length > 0) {
|
|
131
|
+
consecutiveGarbage = 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return lines.slice(0, cutoffIndex).join('\n').trim();
|
|
135
|
+
}
|
|
136
|
+
export function indexNeedsRebuild() {
|
|
137
|
+
if (!existsSync(INDEX_DB_PATH) || !existsSync(FUZZY_INDEX_PATH)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const sourceStats = statSync(NOTES_DB_PATH);
|
|
142
|
+
const indexStats = statSync(INDEX_DB_PATH);
|
|
143
|
+
return sourceStats.mtimeMs > indexStats.mtimeMs;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function getExtendedStats() {
|
|
150
|
+
try {
|
|
151
|
+
if (!existsSync(STATS_PATH)) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return JSON.parse(readFileSync(STATS_PATH, 'utf-8'));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function createIndexTables(db) {
|
|
161
|
+
db.exec(`
|
|
162
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
163
|
+
id INTEGER PRIMARY KEY,
|
|
164
|
+
title TEXT,
|
|
165
|
+
snippet TEXT,
|
|
166
|
+
body TEXT,
|
|
167
|
+
folder TEXT,
|
|
168
|
+
created_at INTEGER,
|
|
169
|
+
modified_at INTEGER,
|
|
170
|
+
is_pinned INTEGER,
|
|
171
|
+
is_locked INTEGER
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
175
|
+
title,
|
|
176
|
+
snippet,
|
|
177
|
+
body,
|
|
178
|
+
folder,
|
|
179
|
+
content='notes',
|
|
180
|
+
content_rowid='id'
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(folder);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_notes_modified ON notes(modified_at);
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
function processNote(raw, pdfTextByNote) {
|
|
188
|
+
let body = extractTextFromNoteData(raw.data);
|
|
189
|
+
const noteKey = getNoteKey(raw.rowid);
|
|
190
|
+
const pdfText = pdfTextByNote.get(noteKey);
|
|
191
|
+
if (pdfText) {
|
|
192
|
+
body = body ? `${body}\n\n${pdfText}` : pdfText;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
id: raw.rowid,
|
|
196
|
+
title: raw.title || '',
|
|
197
|
+
snippet: raw.snippet || '',
|
|
198
|
+
body,
|
|
199
|
+
folder: raw.folderName || 'Notes',
|
|
200
|
+
createdAt: raw.creationDate ? appleToUnix(raw.creationDate) : 0,
|
|
201
|
+
modifiedAt: raw.modificationDate ? appleToUnix(raw.modificationDate) : 0,
|
|
202
|
+
isPinned: raw.isPinned === 1,
|
|
203
|
+
isLocked: raw.isPasswordProtected === 1,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function rebuildFuzzyIndex(indexDb) {
|
|
207
|
+
const allNotes = indexDb.prepare(`
|
|
208
|
+
SELECT id, title, snippet, body, folder, created_at as createdAt,
|
|
209
|
+
modified_at as modifiedAt, is_pinned as isPinned, is_locked as isLocked
|
|
210
|
+
FROM notes
|
|
211
|
+
`).all();
|
|
212
|
+
const miniSearch = new MiniSearch({
|
|
213
|
+
fields: ['title', 'snippet', 'body', 'folder'],
|
|
214
|
+
storeFields: ['id', 'title', 'snippet', 'folder', 'createdAt', 'modifiedAt', 'isPinned', 'isLocked'],
|
|
215
|
+
searchOptions: {
|
|
216
|
+
fuzzy: 0.2,
|
|
217
|
+
prefix: true,
|
|
218
|
+
boost: { title: 3, snippet: 2, body: 1, folder: 1 },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
miniSearch.addAll(allNotes);
|
|
222
|
+
writeFileSync(FUZZY_INDEX_PATH, JSON.stringify(miniSearch.toJSON()));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Incremental index update - only processes changed notes
|
|
226
|
+
*/
|
|
227
|
+
export async function updateIndex(onProgress) {
|
|
228
|
+
ensureIndexDir();
|
|
229
|
+
const existingStats = getExtendedStats();
|
|
230
|
+
const lastModTime = existingStats?.lastIndexedModTime || 0;
|
|
231
|
+
const sourceDb = new Database(NOTES_DB_PATH, { readonly: true });
|
|
232
|
+
const indexDb = new Database(INDEX_DB_PATH);
|
|
233
|
+
try {
|
|
234
|
+
createIndexTables(indexDb);
|
|
235
|
+
// Get current note IDs from source to detect deletions
|
|
236
|
+
const currentNoteIds = new Set(sourceDb.prepare(`
|
|
237
|
+
SELECT Z_PK as id FROM ZICCLOUDSYNCINGOBJECT
|
|
238
|
+
WHERE Z_ENT = ? AND ZMARKEDFORDELETION = 0
|
|
239
|
+
`).all(ENTITY_NOTE).map(r => r.id));
|
|
240
|
+
// Get indexed note IDs
|
|
241
|
+
const indexedNoteIds = new Set(indexDb.prepare('SELECT id FROM notes').all().map(r => r.id));
|
|
242
|
+
// Find deleted notes
|
|
243
|
+
const deletedIds = [];
|
|
244
|
+
for (const id of indexedNoteIds) {
|
|
245
|
+
if (!currentNoteIds.has(id)) {
|
|
246
|
+
deletedIds.push(id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Delete removed notes
|
|
250
|
+
if (deletedIds.length > 0) {
|
|
251
|
+
const deleteStmt = indexDb.prepare('DELETE FROM notes WHERE id = ?');
|
|
252
|
+
const deleteFtsStmt = indexDb.prepare('DELETE FROM notes_fts WHERE rowid = ?');
|
|
253
|
+
indexDb.exec('BEGIN TRANSACTION');
|
|
254
|
+
for (const id of deletedIds) {
|
|
255
|
+
deleteStmt.run(id);
|
|
256
|
+
deleteFtsStmt.run(id);
|
|
257
|
+
}
|
|
258
|
+
indexDb.exec('COMMIT');
|
|
259
|
+
}
|
|
260
|
+
// Query notes modified since last index
|
|
261
|
+
const modifiedNotesQuery = sourceDb.prepare(`
|
|
262
|
+
SELECT
|
|
263
|
+
n.Z_PK as rowid,
|
|
264
|
+
n.ZTITLE1 as title,
|
|
265
|
+
n.ZSNIPPET as snippet,
|
|
266
|
+
n.ZFOLDER as folderId,
|
|
267
|
+
f.ZTITLE2 as folderName,
|
|
268
|
+
COALESCE(n.ZCREATIONDATE, n.ZCREATIONDATE1, n.ZCREATIONDATE2, n.ZCREATIONDATE3) as creationDate,
|
|
269
|
+
COALESCE(n.ZMODIFICATIONDATE, n.ZMODIFICATIONDATE1, n.ZMODIFIEDDATE) as modificationDate,
|
|
270
|
+
n.ZISPINNED as isPinned,
|
|
271
|
+
n.ZISPASSWORDPROTECTED as isPasswordProtected,
|
|
272
|
+
nd.ZDATA as data
|
|
273
|
+
FROM ZICCLOUDSYNCINGOBJECT n
|
|
274
|
+
LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON n.ZFOLDER = f.Z_PK
|
|
275
|
+
LEFT JOIN ZICNOTEDATA nd ON nd.ZNOTE = n.Z_PK
|
|
276
|
+
WHERE n.Z_ENT = ?
|
|
277
|
+
AND n.ZMARKEDFORDELETION = 0
|
|
278
|
+
AND COALESCE(n.ZMODIFICATIONDATE, n.ZMODIFICATIONDATE1, n.ZMODIFIEDDATE) > ?
|
|
279
|
+
ORDER BY COALESCE(n.ZMODIFICATIONDATE, n.ZMODIFICATIONDATE1, n.ZMODIFIEDDATE) ASC
|
|
280
|
+
`);
|
|
281
|
+
const modifiedNotes = modifiedNotesQuery.all(ENTITY_NOTE, lastModTime);
|
|
282
|
+
if (modifiedNotes.length === 0 && deletedIds.length === 0) {
|
|
283
|
+
onProgress?.({ phase: 'done', current: 0, total: 0, message: 'Index up to date' });
|
|
284
|
+
return { ...existingStats, updated: 0, deleted: 0 };
|
|
285
|
+
}
|
|
286
|
+
onProgress?.({
|
|
287
|
+
phase: 'extracting-pdfs',
|
|
288
|
+
current: 0,
|
|
289
|
+
total: modifiedNotes.length,
|
|
290
|
+
message: `Found ${modifiedNotes.length} modified, ${deletedIds.length} deleted`
|
|
291
|
+
});
|
|
292
|
+
// Extract PDF text only for modified notes
|
|
293
|
+
let pdfTextByNote;
|
|
294
|
+
try {
|
|
295
|
+
pdfTextByNote = extractAllPdfText();
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
pdfTextByNote = new Map();
|
|
299
|
+
}
|
|
300
|
+
onProgress?.({ phase: 'reading', current: 0, total: modifiedNotes.length });
|
|
301
|
+
// Upsert modified notes
|
|
302
|
+
const upsertStmt = indexDb.prepare(`
|
|
303
|
+
INSERT OR REPLACE INTO notes (id, title, snippet, body, folder, created_at, modified_at, is_pinned, is_locked)
|
|
304
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
305
|
+
`);
|
|
306
|
+
const deleteFtsForUpdate = indexDb.prepare('DELETE FROM notes_fts WHERE rowid = ?');
|
|
307
|
+
const insertFtsStmt = indexDb.prepare(`
|
|
308
|
+
INSERT INTO notes_fts (rowid, title, snippet, body, folder)
|
|
309
|
+
VALUES (?, ?, ?, ?, ?)
|
|
310
|
+
`);
|
|
311
|
+
let maxModTime = lastModTime;
|
|
312
|
+
indexDb.exec('BEGIN TRANSACTION');
|
|
313
|
+
for (let i = 0; i < modifiedNotes.length; i++) {
|
|
314
|
+
const raw = modifiedNotes[i];
|
|
315
|
+
const note = processNote(raw, pdfTextByNote);
|
|
316
|
+
if (raw.modificationDate && raw.modificationDate > maxModTime) {
|
|
317
|
+
maxModTime = raw.modificationDate;
|
|
318
|
+
}
|
|
319
|
+
upsertStmt.run(note.id, note.title, note.snippet, note.body, note.folder, note.createdAt, note.modifiedAt, note.isPinned ? 1 : 0, note.isLocked ? 1 : 0);
|
|
320
|
+
// Update FTS (delete then insert)
|
|
321
|
+
deleteFtsForUpdate.run(note.id);
|
|
322
|
+
insertFtsStmt.run(note.id, note.title, note.snippet, note.body, note.folder);
|
|
323
|
+
if (i % 50 === 0) {
|
|
324
|
+
onProgress?.({ phase: 'reading', current: i, total: modifiedNotes.length });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
indexDb.exec('COMMIT');
|
|
328
|
+
onProgress?.({ phase: 'indexing', current: 0, total: 1, message: 'Rebuilding fuzzy index...' });
|
|
329
|
+
// Rebuild fuzzy index from SQLite
|
|
330
|
+
rebuildFuzzyIndex(indexDb);
|
|
331
|
+
// Update stats
|
|
332
|
+
const totalNotes = indexDb.prepare('SELECT COUNT(*) as count FROM notes').get().count;
|
|
333
|
+
const folderCount = sourceDb.prepare(`
|
|
334
|
+
SELECT COUNT(*) as count FROM ZICCLOUDSYNCINGOBJECT WHERE Z_ENT = ? AND ZMARKEDFORDELETION = 0
|
|
335
|
+
`).get(ENTITY_FOLDER).count;
|
|
336
|
+
const dateRange = indexDb.prepare(`
|
|
337
|
+
SELECT MIN(modified_at) as oldest, MAX(modified_at) as newest FROM notes WHERE modified_at > 0
|
|
338
|
+
`).get();
|
|
339
|
+
const stats = {
|
|
340
|
+
totalNotes,
|
|
341
|
+
totalFolders: folderCount,
|
|
342
|
+
indexedAt: new Date().toISOString(),
|
|
343
|
+
oldestNote: dateRange.oldest ? new Date(dateRange.oldest * 1000).toISOString() : null,
|
|
344
|
+
newestNote: dateRange.newest ? new Date(dateRange.newest * 1000).toISOString() : null,
|
|
345
|
+
lastIndexedModTime: maxModTime,
|
|
346
|
+
};
|
|
347
|
+
writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2));
|
|
348
|
+
onProgress?.({ phase: 'done', current: modifiedNotes.length, total: modifiedNotes.length });
|
|
349
|
+
return { ...stats, updated: modifiedNotes.length, deleted: deletedIds.length };
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
sourceDb.close();
|
|
353
|
+
indexDb.close();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Full index rebuild - processes all notes from scratch
|
|
358
|
+
*/
|
|
359
|
+
export async function buildIndex(onProgress) {
|
|
360
|
+
ensureIndexDir();
|
|
361
|
+
const sourceDb = new Database(NOTES_DB_PATH, { readonly: true });
|
|
362
|
+
// Remove old index and create fresh
|
|
363
|
+
if (existsSync(INDEX_DB_PATH)) {
|
|
364
|
+
try {
|
|
365
|
+
const oldDb = new Database(INDEX_DB_PATH);
|
|
366
|
+
oldDb.close();
|
|
367
|
+
}
|
|
368
|
+
catch { /* ignore */ }
|
|
369
|
+
}
|
|
370
|
+
const indexDb = new Database(INDEX_DB_PATH);
|
|
371
|
+
try {
|
|
372
|
+
// Drop and recreate tables for full rebuild
|
|
373
|
+
indexDb.exec(`
|
|
374
|
+
DROP TABLE IF EXISTS notes_fts;
|
|
375
|
+
DROP TABLE IF EXISTS notes;
|
|
376
|
+
`);
|
|
377
|
+
createIndexTables(indexDb);
|
|
378
|
+
const notesQuery = sourceDb.prepare(`
|
|
379
|
+
SELECT
|
|
380
|
+
n.Z_PK as rowid,
|
|
381
|
+
n.ZTITLE1 as title,
|
|
382
|
+
n.ZSNIPPET as snippet,
|
|
383
|
+
n.ZFOLDER as folderId,
|
|
384
|
+
f.ZTITLE2 as folderName,
|
|
385
|
+
COALESCE(n.ZCREATIONDATE, n.ZCREATIONDATE1, n.ZCREATIONDATE2, n.ZCREATIONDATE3) as creationDate,
|
|
386
|
+
COALESCE(n.ZMODIFICATIONDATE, n.ZMODIFICATIONDATE1, n.ZMODIFIEDDATE) as modificationDate,
|
|
387
|
+
n.ZISPINNED as isPinned,
|
|
388
|
+
n.ZISPASSWORDPROTECTED as isPasswordProtected,
|
|
389
|
+
nd.ZDATA as data
|
|
390
|
+
FROM ZICCLOUDSYNCINGOBJECT n
|
|
391
|
+
LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON n.ZFOLDER = f.Z_PK
|
|
392
|
+
LEFT JOIN ZICNOTEDATA nd ON nd.ZNOTE = n.Z_PK
|
|
393
|
+
WHERE n.Z_ENT = ?
|
|
394
|
+
AND n.ZMARKEDFORDELETION = 0
|
|
395
|
+
ORDER BY COALESCE(n.ZMODIFICATIONDATE, n.ZMODIFICATIONDATE1, n.ZMODIFIEDDATE) ASC
|
|
396
|
+
`);
|
|
397
|
+
const rawNotes = notesQuery.all(ENTITY_NOTE);
|
|
398
|
+
const totalNotes = rawNotes.length;
|
|
399
|
+
onProgress?.({ phase: 'extracting-pdfs', current: 0, total: totalNotes });
|
|
400
|
+
let pdfTextByNote;
|
|
401
|
+
try {
|
|
402
|
+
pdfTextByNote = extractAllPdfText();
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
pdfTextByNote = new Map();
|
|
406
|
+
}
|
|
407
|
+
onProgress?.({ phase: 'reading', current: 0, total: totalNotes });
|
|
408
|
+
const insertStmt = indexDb.prepare(`
|
|
409
|
+
INSERT INTO notes (id, title, snippet, body, folder, created_at, modified_at, is_pinned, is_locked)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
411
|
+
`);
|
|
412
|
+
const insertFtsStmt = indexDb.prepare(`
|
|
413
|
+
INSERT INTO notes_fts (rowid, title, snippet, body, folder)
|
|
414
|
+
VALUES (?, ?, ?, ?, ?)
|
|
415
|
+
`);
|
|
416
|
+
let maxModTime = 0;
|
|
417
|
+
indexDb.exec('BEGIN TRANSACTION');
|
|
418
|
+
for (let i = 0; i < rawNotes.length; i++) {
|
|
419
|
+
const raw = rawNotes[i];
|
|
420
|
+
const note = processNote(raw, pdfTextByNote);
|
|
421
|
+
if (raw.modificationDate && raw.modificationDate > maxModTime) {
|
|
422
|
+
maxModTime = raw.modificationDate;
|
|
423
|
+
}
|
|
424
|
+
insertStmt.run(note.id, note.title, note.snippet, note.body, note.folder, note.createdAt, note.modifiedAt, note.isPinned ? 1 : 0, note.isLocked ? 1 : 0);
|
|
425
|
+
insertFtsStmt.run(note.id, note.title, note.snippet, note.body, note.folder);
|
|
426
|
+
if (i % 100 === 0) {
|
|
427
|
+
onProgress?.({ phase: 'reading', current: i, total: totalNotes });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
indexDb.exec('COMMIT');
|
|
431
|
+
onProgress?.({ phase: 'indexing', current: 0, total: totalNotes });
|
|
432
|
+
rebuildFuzzyIndex(indexDb);
|
|
433
|
+
const folderCount = sourceDb.prepare(`
|
|
434
|
+
SELECT COUNT(*) as count FROM ZICCLOUDSYNCINGOBJECT WHERE Z_ENT = ? AND ZMARKEDFORDELETION = 0
|
|
435
|
+
`).get(ENTITY_FOLDER).count;
|
|
436
|
+
const dateRange = indexDb.prepare(`
|
|
437
|
+
SELECT MIN(modified_at) as oldest, MAX(modified_at) as newest FROM notes WHERE modified_at > 0
|
|
438
|
+
`).get();
|
|
439
|
+
const stats = {
|
|
440
|
+
totalNotes,
|
|
441
|
+
totalFolders: folderCount,
|
|
442
|
+
indexedAt: new Date().toISOString(),
|
|
443
|
+
oldestNote: dateRange.oldest ? new Date(dateRange.oldest * 1000).toISOString() : null,
|
|
444
|
+
newestNote: dateRange.newest ? new Date(dateRange.newest * 1000).toISOString() : null,
|
|
445
|
+
lastIndexedModTime: maxModTime,
|
|
446
|
+
};
|
|
447
|
+
writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2));
|
|
448
|
+
onProgress?.({ phase: 'done', current: totalNotes, total: totalNotes });
|
|
449
|
+
return stats;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
sourceDb.close();
|
|
453
|
+
indexDb.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
export async function ensureIndex() {
|
|
457
|
+
if (!existsSync(INDEX_DB_PATH) || !existsSync(FUZZY_INDEX_PATH)) {
|
|
458
|
+
// No index exists, do full build
|
|
459
|
+
await buildIndex();
|
|
460
|
+
}
|
|
461
|
+
else if (indexNeedsRebuild()) {
|
|
462
|
+
// Index exists but outdated, do incremental update
|
|
463
|
+
await updateIndex();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
export function getStats() {
|
|
467
|
+
try {
|
|
468
|
+
if (!existsSync(STATS_PATH)) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return JSON.parse(readFileSync(STATS_PATH, 'utf-8'));
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
export function getIndexDbPath() {
|
|
478
|
+
return INDEX_DB_PATH;
|
|
479
|
+
}
|
|
480
|
+
export function getFuzzyIndexPath() {
|
|
481
|
+
return FUZZY_INDEX_PATH;
|
|
482
|
+
}
|
|
483
|
+
//# sourceMappingURL=indexer.js.map
|