@novis10813/secondbrain-cli 0.1.0 → 0.1.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.
Files changed (93) hide show
  1. package/README.md +74 -5
  2. package/dist/commands/backlinks.d.ts.map +1 -1
  3. package/dist/commands/backlinks.js +16 -33
  4. package/dist/commands/backlinks.js.map +1 -1
  5. package/dist/commands/capture.d.ts.map +1 -1
  6. package/dist/commands/capture.js +71 -57
  7. package/dist/commands/capture.js.map +1 -1
  8. package/dist/commands/config.d.ts.map +1 -1
  9. package/dist/commands/config.js +5 -20
  10. package/dist/commands/config.js.map +1 -1
  11. package/dist/commands/get.js +27 -26
  12. package/dist/commands/get.js.map +1 -1
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +21 -4
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/migrate.d.ts +3 -0
  17. package/dist/commands/migrate.d.ts.map +1 -0
  18. package/dist/commands/migrate.js +22 -0
  19. package/dist/commands/migrate.js.map +1 -0
  20. package/dist/commands/open.d.ts +3 -0
  21. package/dist/commands/open.d.ts.map +1 -0
  22. package/dist/commands/open.js +28 -0
  23. package/dist/commands/open.js.map +1 -0
  24. package/dist/commands/orphans.d.ts.map +1 -1
  25. package/dist/commands/orphans.js +9 -27
  26. package/dist/commands/orphans.js.map +1 -1
  27. package/dist/commands/outlinks.d.ts +3 -0
  28. package/dist/commands/outlinks.d.ts.map +1 -0
  29. package/dist/commands/outlinks.js +48 -0
  30. package/dist/commands/outlinks.js.map +1 -0
  31. package/dist/commands/search.d.ts +2 -0
  32. package/dist/commands/search.d.ts.map +1 -1
  33. package/dist/commands/search.js +57 -39
  34. package/dist/commands/search.js.map +1 -1
  35. package/dist/commands/stats.d.ts.map +1 -1
  36. package/dist/commands/stats.js +4 -18
  37. package/dist/commands/stats.js.map +1 -1
  38. package/dist/commands/sync.d.ts.map +1 -1
  39. package/dist/commands/sync.js +3 -17
  40. package/dist/commands/sync.js.map +1 -1
  41. package/dist/commands/template.d.ts +3 -0
  42. package/dist/commands/template.d.ts.map +1 -0
  43. package/dist/commands/template.js +63 -0
  44. package/dist/commands/template.js.map +1 -0
  45. package/dist/commands/vault.d.ts +3 -0
  46. package/dist/commands/vault.d.ts.map +1 -0
  47. package/dist/commands/vault.js +233 -0
  48. package/dist/commands/vault.js.map +1 -0
  49. package/dist/index.js +13 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/types/index.d.ts +134 -10
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/dist/utils/config.d.ts +3 -1
  54. package/dist/utils/config.d.ts.map +1 -1
  55. package/dist/utils/config.js +12 -0
  56. package/dist/utils/config.js.map +1 -1
  57. package/dist/utils/database.d.ts +58 -14
  58. package/dist/utils/database.d.ts.map +1 -1
  59. package/dist/utils/database.js +627 -207
  60. package/dist/utils/database.js.map +1 -1
  61. package/dist/utils/global-config.d.ts +53 -0
  62. package/dist/utils/global-config.d.ts.map +1 -0
  63. package/dist/utils/global-config.js +144 -0
  64. package/dist/utils/global-config.js.map +1 -0
  65. package/dist/utils/parser.d.ts +95 -2
  66. package/dist/utils/parser.d.ts.map +1 -1
  67. package/dist/utils/parser.js +436 -38
  68. package/dist/utils/parser.js.map +1 -1
  69. package/dist/utils/placeholder.d.ts +37 -0
  70. package/dist/utils/placeholder.d.ts.map +1 -0
  71. package/dist/utils/placeholder.js +113 -0
  72. package/dist/utils/placeholder.js.map +1 -0
  73. package/dist/utils/position.d.ts +10 -0
  74. package/dist/utils/position.d.ts.map +1 -0
  75. package/dist/utils/position.js +24 -0
  76. package/dist/utils/position.js.map +1 -0
  77. package/dist/utils/sqlite-adapter.d.ts +8 -0
  78. package/dist/utils/sqlite-adapter.d.ts.map +1 -0
  79. package/dist/utils/sqlite-adapter.js +14 -0
  80. package/dist/utils/sqlite-adapter.js.map +1 -0
  81. package/dist/utils/template.d.ts +2 -2
  82. package/dist/utils/template.d.ts.map +1 -1
  83. package/dist/utils/template.js +8 -3
  84. package/dist/utils/template.js.map +1 -1
  85. package/dist/utils/vault-resolve.d.ts +23 -0
  86. package/dist/utils/vault-resolve.d.ts.map +1 -0
  87. package/dist/utils/vault-resolve.js +86 -0
  88. package/dist/utils/vault-resolve.js.map +1 -0
  89. package/dist/utils/vault.d.ts +77 -10
  90. package/dist/utils/vault.d.ts.map +1 -1
  91. package/dist/utils/vault.js +253 -98
  92. package/dist/utils/vault.js.map +1 -1
  93. package/package.json +9 -3
@@ -1,261 +1,681 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DatabaseManager = void 0;
4
- const bun_sqlite_1 = require("bun:sqlite");
4
+ const sqlite_adapter_js_1 = require("./sqlite-adapter.js");
5
5
  class DatabaseManager {
6
6
  db;
7
7
  config;
8
8
  constructor(config) {
9
9
  this.config = config;
10
- this.db = new bun_sqlite_1.Database(config.dbPath);
10
+ this.db = (0, sqlite_adapter_js_1.createDatabase)(config.dbPath);
11
+ this.db.exec('PRAGMA foreign_keys = ON');
11
12
  this.initTables();
12
13
  }
13
14
  initTables() {
14
- // Notes table
15
+ // Drop legacy tables if they exist (breaking change - users need to re-sync)
15
16
  this.db.exec(`
16
- CREATE TABLE IF NOT EXISTS notes (
17
- id TEXT PRIMARY KEY,
18
- path TEXT UNIQUE NOT NULL,
19
- title TEXT NOT NULL,
20
- content TEXT NOT NULL,
21
- frontmatter TEXT NOT NULL,
22
- tags TEXT NOT NULL,
23
- hash TEXT NOT NULL,
24
- created_at TEXT NOT NULL,
25
- modified_at TEXT NOT NULL
17
+ DROP TABLE IF EXISTS links;
18
+ DROP TABLE IF EXISTS notes;
19
+ `);
20
+ this.db.exec(`
21
+ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)
22
+ `);
23
+ const versionRow = this.db.prepare('SELECT version FROM schema_version LIMIT 1').get();
24
+ const currentVersion = versionRow?.version ?? 0;
25
+ if (currentVersion === 0) {
26
+ this.db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(0);
27
+ }
28
+ // New Obsidian-aligned tables
29
+ this.initObsidianTables();
30
+ }
31
+ initObsidianTables() {
32
+ // Files table (FileInfo/TFile equivalent)
33
+ this.db.exec(`
34
+ CREATE TABLE IF NOT EXISTS files (
35
+ path TEXT PRIMARY KEY,
36
+ name TEXT NOT NULL,
37
+ basename TEXT NOT NULL,
38
+ extension TEXT NOT NULL,
39
+ parent TEXT,
40
+ ctime INTEGER NOT NULL,
41
+ mtime INTEGER NOT NULL,
42
+ size INTEGER NOT NULL,
43
+ content_hash TEXT NOT NULL
44
+ )
45
+ `);
46
+ // Content metadata table
47
+ this.db.exec(`
48
+ CREATE TABLE IF NOT EXISTS content_metadata (
49
+ file_path TEXT PRIMARY KEY,
50
+ content_hash TEXT NOT NULL,
51
+ frontmatter_start_line INTEGER,
52
+ frontmatter_start_col INTEGER,
53
+ frontmatter_start_offset INTEGER,
54
+ frontmatter_end_line INTEGER,
55
+ frontmatter_end_col INTEGER,
56
+ frontmatter_end_offset INTEGER,
57
+ FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
58
+ )
59
+ `);
60
+ // Links table with positions (new structure)
61
+ this.db.exec(`
62
+ CREATE TABLE IF NOT EXISTS links_with_positions (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ source_path TEXT NOT NULL,
65
+ target_path TEXT,
66
+ target_id TEXT,
67
+ link_target TEXT, -- Original link target (e.g. "Note Name")
68
+ original TEXT NOT NULL,
69
+ display_text TEXT,
70
+ start_line INTEGER NOT NULL,
71
+ start_col INTEGER NOT NULL,
72
+ start_offset INTEGER NOT NULL,
73
+ end_line INTEGER NOT NULL,
74
+ end_col INTEGER NOT NULL,
75
+ end_offset INTEGER NOT NULL,
76
+ FOREIGN KEY (source_path) REFERENCES files(path) ON DELETE CASCADE
77
+ )
78
+ `);
79
+ // Tags table with positions
80
+ this.db.exec(`
81
+ CREATE TABLE IF NOT EXISTS tags_with_positions (
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
83
+ file_path TEXT NOT NULL,
84
+ tag TEXT NOT NULL,
85
+ start_line INTEGER NOT NULL,
86
+ start_col INTEGER NOT NULL,
87
+ start_offset INTEGER NOT NULL,
88
+ end_line INTEGER NOT NULL,
89
+ end_col INTEGER NOT NULL,
90
+ end_offset INTEGER NOT NULL,
91
+ FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
26
92
  )
27
93
  `);
28
- // Links table (many-to-many)
94
+ // Headings table
29
95
  this.db.exec(`
30
- CREATE TABLE IF NOT EXISTS links (
31
- source_id TEXT NOT NULL,
32
- target_id TEXT NOT NULL,
33
- PRIMARY KEY (source_id, target_id),
34
- FOREIGN KEY (source_id) REFERENCES notes(id) ON DELETE CASCADE,
35
- FOREIGN KEY (target_id) REFERENCES notes(id) ON DELETE CASCADE
96
+ CREATE TABLE IF NOT EXISTS headings_with_positions (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ file_path TEXT NOT NULL,
99
+ heading TEXT NOT NULL,
100
+ level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 6),
101
+ start_line INTEGER NOT NULL,
102
+ start_col INTEGER NOT NULL,
103
+ start_offset INTEGER NOT NULL,
104
+ end_line INTEGER NOT NULL,
105
+ end_col INTEGER NOT NULL,
106
+ end_offset INTEGER NOT NULL,
107
+ FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
36
108
  )
37
109
  `);
38
- // Create indexes for performance
110
+ // Blocks table
39
111
  this.db.exec(`
40
- CREATE INDEX IF NOT EXISTS idx_notes_path ON notes(path);
41
- CREATE INDEX IF NOT EXISTS idx_notes_hash ON notes(hash);
42
- CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
43
- CREATE INDEX IF NOT EXISTS idx_links_target ON links(target_id);
112
+ CREATE TABLE IF NOT EXISTS blocks_with_positions (
113
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ file_path TEXT NOT NULL,
115
+ block_id TEXT NOT NULL,
116
+ start_line INTEGER NOT NULL,
117
+ start_col INTEGER NOT NULL,
118
+ start_offset INTEGER NOT NULL,
119
+ end_line INTEGER NOT NULL,
120
+ end_col INTEGER NOT NULL,
121
+ end_offset INTEGER NOT NULL,
122
+ FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE,
123
+ UNIQUE(file_path, block_id)
124
+ )
125
+ `);
126
+ // Embeds table
127
+ this.db.exec(`
128
+ CREATE TABLE IF NOT EXISTS embeds_with_positions (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ file_path TEXT NOT NULL,
131
+ target_path TEXT NOT NULL,
132
+ original TEXT NOT NULL,
133
+ display_text TEXT,
134
+ start_line INTEGER NOT NULL,
135
+ start_col INTEGER NOT NULL,
136
+ start_offset INTEGER NOT NULL,
137
+ end_line INTEGER NOT NULL,
138
+ end_col INTEGER NOT NULL,
139
+ end_offset INTEGER NOT NULL,
140
+ FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
141
+ )
142
+ `);
143
+ // Sections table (document sections: frontmatter, heading-bounded regions)
144
+ this.db.exec(`
145
+ CREATE TABLE IF NOT EXISTS sections_with_positions (
146
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
147
+ file_path TEXT NOT NULL,
148
+ section_id TEXT NOT NULL,
149
+ type TEXT NOT NULL,
150
+ start_line INTEGER NOT NULL,
151
+ start_col INTEGER NOT NULL,
152
+ start_offset INTEGER NOT NULL,
153
+ end_line INTEGER NOT NULL,
154
+ end_col INTEGER NOT NULL,
155
+ end_offset INTEGER NOT NULL,
156
+ FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
157
+ )
158
+ `);
159
+ // Create indexes for new tables
160
+ this.db.exec(`
161
+ CREATE INDEX IF NOT EXISTS idx_files_content_hash ON files(content_hash);
162
+ CREATE INDEX IF NOT EXISTS idx_files_basename ON files(basename COLLATE NOCASE);
163
+ CREATE INDEX IF NOT EXISTS idx_links_pos_source ON links_with_positions(source_path);
164
+ CREATE INDEX IF NOT EXISTS idx_links_pos_target ON links_with_positions(target_path);
165
+ CREATE INDEX IF NOT EXISTS idx_tags_pos_file ON tags_with_positions(file_path);
166
+ CREATE INDEX IF NOT EXISTS idx_tags_pos_tag ON tags_with_positions(tag);
167
+ CREATE INDEX IF NOT EXISTS idx_headings_pos_file ON headings_with_positions(file_path);
168
+ CREATE INDEX IF NOT EXISTS idx_blocks_pos_file ON blocks_with_positions(file_path);
169
+ CREATE INDEX IF NOT EXISTS idx_embeds_pos_file ON embeds_with_positions(file_path);
170
+ CREATE INDEX IF NOT EXISTS idx_sections_pos_file ON sections_with_positions(file_path);
44
171
  `);
45
172
  }
46
173
  close() {
47
174
  this.db.close();
48
175
  }
49
- // Note operations
50
- upsertNote(note) {
176
+ rowToFileInfo(row) {
177
+ return {
178
+ path: row.path,
179
+ name: row.name,
180
+ basename: row.basename,
181
+ extension: row.extension,
182
+ parent: row.parent,
183
+ stat: { ctime: row.ctime, mtime: row.mtime, size: row.size }
184
+ };
185
+ }
186
+ // FileInfo operations
187
+ upsertFile(file, contentHash) {
51
188
  const stmt = this.db.prepare(`
52
- INSERT INTO notes (id, path, title, content, frontmatter, tags, hash, created_at, modified_at)
189
+ INSERT INTO files (path, name, basename, extension, parent, ctime, mtime, size, content_hash)
53
190
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
54
191
  ON CONFLICT(path) DO UPDATE SET
55
- id = excluded.id,
56
- title = excluded.title,
57
- content = excluded.content,
58
- frontmatter = excluded.frontmatter,
59
- tags = excluded.tags,
60
- hash = excluded.hash,
61
- modified_at = excluded.modified_at
192
+ name = excluded.name,
193
+ basename = excluded.basename,
194
+ extension = excluded.extension,
195
+ parent = excluded.parent,
196
+ ctime = excluded.ctime,
197
+ mtime = excluded.mtime,
198
+ size = excluded.size,
199
+ content_hash = excluded.content_hash
62
200
  `);
63
- stmt.run(note.id, note.path, note.title, note.content, JSON.stringify(note.frontmatter), JSON.stringify(note.tags), note.hash, note.createdAt, note.modifiedAt);
64
- // Update links
65
- this.updateLinks(note.id, note.links);
201
+ stmt.run(file.path, file.name, file.basename, file.extension, file.parent || null, file.stat.ctime, file.stat.mtime, file.stat.size, contentHash || file.content_hash);
66
202
  }
67
- updateLinks(noteId, targetIds) {
68
- // Get existing links
69
- const existingLinks = this.db.prepare('SELECT target_id FROM links WHERE source_id = ?')
70
- .all(noteId)
71
- .map((row) => row.target_id);
72
- // Calculate diff
73
- const existingSet = new Set(existingLinks);
74
- const newSet = new Set(targetIds);
75
- // Links to add (in newSet but not in existingSet)
76
- const toAdd = targetIds.filter(id => !existingSet.has(id));
77
- // Links to remove (in existingSet but not in newSet)
78
- const toRemove = existingLinks.filter(id => !newSet.has(id));
79
- // Skip if no changes
80
- if (toAdd.length === 0 && toRemove.length === 0) {
81
- return;
82
- }
83
- // Remove links that no longer exist
84
- if (toRemove.length > 0) {
85
- const placeholders = toRemove.map(() => '?').join(',');
86
- this.db.prepare(`DELETE FROM links WHERE source_id = ? AND target_id IN (${placeholders})`)
87
- .run(noteId, ...toRemove);
88
- }
89
- // Add new links (only if target exists)
90
- if (toAdd.length > 0) {
91
- const insertStmt = this.db.prepare('INSERT OR IGNORE INTO links (source_id, target_id) VALUES (?, ?)');
92
- const targetExistsStmt = this.db.prepare('SELECT 1 FROM notes WHERE id = ?');
93
- for (const targetId of toAdd) {
94
- // Only insert if target exists
95
- const targetExists = targetExistsStmt.get(targetId);
96
- if (targetExists) {
97
- insertStmt.run(noteId, targetId);
98
- }
203
+ getFileByPath(path) {
204
+ const row = this.db.prepare('SELECT * FROM files WHERE path = ?').get(path);
205
+ return row ? this.rowToFileInfo(row) : null;
206
+ }
207
+ getFileByBasename(basename) {
208
+ const row = this.db.prepare('SELECT * FROM files WHERE basename = ? COLLATE NOCASE LIMIT 1').get(basename);
209
+ return row ? this.rowToFileInfo(row) : null;
210
+ }
211
+ deleteFile(path) {
212
+ this.db.prepare('DELETE FROM files WHERE path = ?').run(path);
213
+ }
214
+ getAllFiles() {
215
+ const rows = this.db.prepare('SELECT * FROM files').all();
216
+ return rows.map((row) => this.rowToFileInfo(row));
217
+ }
218
+ getFileContentHash(filePath) {
219
+ const row = this.db.prepare('SELECT content_hash FROM files WHERE path = ?').get(filePath);
220
+ return row?.content_hash ?? null;
221
+ }
222
+ /**
223
+ * Update link target in links_with_positions table.
224
+ * Finds link by source_path and start_offset, then updates target_path and target_id.
225
+ */
226
+ updateLinkTarget(sourcePath, startOffset, targetPath, targetId) {
227
+ const stmt = this.db.prepare(`
228
+ UPDATE links_with_positions
229
+ SET target_path = ?, target_id = ?
230
+ WHERE source_path = ? AND start_offset = ?
231
+ `);
232
+ stmt.run(targetPath, targetId, sourcePath, startOffset);
233
+ }
234
+ /** Get files that link to the given file path (new structure). */
235
+ getBacklinksByPath(filePath) {
236
+ const rows = this.db
237
+ .prepare(`
238
+ SELECT f.path, f.name, f.basename, f.extension, f.parent, f.ctime, f.mtime, f.size
239
+ FROM files f
240
+ INNER JOIN (SELECT DISTINCT source_path FROM links_with_positions WHERE target_path = ?) l
241
+ ON f.path = l.source_path
242
+ `)
243
+ .all(filePath);
244
+ return rows.map((row) => this.rowToFileInfo(row));
245
+ }
246
+ /** Get files that the given file links to (outgoing links, new structure). */
247
+ getOutlinksByPath(filePath) {
248
+ const rows = this.db
249
+ .prepare(`
250
+ SELECT f.path, f.name, f.basename, f.extension, f.parent, f.ctime, f.mtime, f.size
251
+ FROM files f
252
+ INNER JOIN (
253
+ SELECT DISTINCT target_path FROM links_with_positions
254
+ WHERE source_path = ? AND target_path IS NOT NULL AND target_path != ''
255
+ ) l ON f.path = l.target_path
256
+ `)
257
+ .all(filePath);
258
+ return rows.map((row) => this.rowToFileInfo(row));
259
+ }
260
+ /**
261
+ * Get start position of a heading in a file. Matches by exact heading text or Obsidian-style slug.
262
+ * @returns { line, col } (1-based) or null
263
+ */
264
+ getHeadingPosition(filePath, headingFragment) {
265
+ const slug = (s) => s.toLowerCase().trim().replace(/\s+/g, '-');
266
+ const fragmentSlug = slug(headingFragment);
267
+ const rows = this.db.prepare('SELECT heading, start_line, start_col FROM headings_with_positions WHERE file_path = ?').all(filePath);
268
+ for (const row of rows) {
269
+ if (row.heading === headingFragment || slug(row.heading) === fragmentSlug) {
270
+ return { line: row.start_line, col: row.start_col };
99
271
  }
100
272
  }
273
+ return null;
101
274
  }
102
- getNoteById(id) {
103
- const row = this.db.prepare('SELECT * FROM notes WHERE id = ?').get(id);
104
- if (!row)
105
- return null;
106
- return this.rowToNote(row);
275
+ /**
276
+ * Get start position of a block (^block-id) in a file.
277
+ * @returns { line, col } (1-based) or null
278
+ */
279
+ getBlockPosition(filePath, blockId) {
280
+ const row = this.db.prepare('SELECT start_line, start_col FROM blocks_with_positions WHERE file_path = ? AND block_id = ?').get(filePath, blockId);
281
+ return row ? { line: row.start_line, col: row.start_col } : null;
107
282
  }
108
- getNoteByPath(path) {
109
- const row = this.db.prepare('SELECT * FROM notes WHERE path = ?').get(path);
110
- if (!row)
111
- return null;
112
- return this.rowToNote(row);
113
- }
114
- getNoteByTitle(title) {
115
- const row = this.db.prepare('SELECT * FROM notes WHERE title = ? COLLATE NOCASE').get(title);
116
- if (!row)
117
- return null;
118
- return this.rowToNote(row);
283
+ /** Get files with no incoming or outgoing links (new structure). */
284
+ getOrphanFiles() {
285
+ const rows = this.db
286
+ .prepare(`
287
+ SELECT * FROM files f
288
+ WHERE NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.source_path = f.path)
289
+ AND NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.target_path = f.path)
290
+ `)
291
+ .all();
292
+ return rows.map((row) => this.rowToFileInfo(row));
119
293
  }
120
- searchNotes(query, tags, limit = 20) {
121
- let sql = 'SELECT * FROM notes WHERE (title LIKE ? OR content LIKE ?)';
122
- const params = [`%${query}%`, `%${query}%`];
294
+ /** Search files by path/basename, optional tags, path prefix, links-to target, heading, or mtime (new structure). */
295
+ searchFiles(query, tags, limit = 20, pathPrefix, linksToPath, headingQuery, modifiedAfter, modifiedBefore) {
296
+ const like = `%${query}%`;
297
+ let sql = `
298
+ SELECT f.*, (SELECT group_concat(DISTINCT tag) FROM tags_with_positions WHERE file_path = f.path) AS tags_str
299
+ FROM files f
300
+ WHERE (f.path LIKE ? OR f.basename LIKE ?)
301
+ `;
302
+ const params = [like, like];
303
+ if (pathPrefix !== undefined && pathPrefix !== '') {
304
+ sql += ` AND (f.path LIKE ? OR f.parent = ?)`;
305
+ params.push(`${pathPrefix}%`, pathPrefix);
306
+ }
123
307
  if (tags && tags.length > 0) {
124
- sql += ' AND (' + tags.map(() => 'tags LIKE ?').join(' OR ') + ')';
125
- params.push(...tags.map(tag => `%"${tag}"%`));
308
+ sql += ` AND f.path IN (SELECT file_path FROM tags_with_positions WHERE tag IN (${tags.map(() => '?').join(',')}))`;
309
+ params.push(...tags);
310
+ }
311
+ if (linksToPath !== undefined && linksToPath !== '') {
312
+ sql += ` AND f.path IN (SELECT source_path FROM links_with_positions WHERE target_path = ?)`;
313
+ params.push(linksToPath);
314
+ }
315
+ if (headingQuery !== undefined && headingQuery !== '') {
316
+ sql += ` AND f.path IN (SELECT file_path FROM headings_with_positions WHERE heading LIKE ?)`;
317
+ params.push(`%${headingQuery}%`);
318
+ }
319
+ if (modifiedAfter !== undefined && Number.isFinite(modifiedAfter)) {
320
+ sql += ` AND f.mtime >= ?`;
321
+ params.push(modifiedAfter);
126
322
  }
127
- sql += ' ORDER BY modified_at DESC LIMIT ?';
323
+ if (modifiedBefore !== undefined && Number.isFinite(modifiedBefore)) {
324
+ sql += ` AND f.mtime <= ?`;
325
+ params.push(modifiedBefore);
326
+ }
327
+ sql += ` ORDER BY f.mtime DESC LIMIT ?`;
128
328
  params.push(limit);
129
329
  const rows = this.db.prepare(sql).all(...params);
130
- return this.rowsToNotes(rows);
330
+ return rows.map((row) => ({
331
+ file: this.rowToFileInfo(row),
332
+ tags: row.tags_str ? row.tags_str.split(',') : []
333
+ }));
334
+ }
335
+ // ContentMetadata operations
336
+ // ContentMetadata operations
337
+ upsertContentMetadata(filePath, metadata, contentHash) {
338
+ const transaction = this.db.transaction(() => {
339
+ this.doUpsertContentMetadata(filePath, metadata, contentHash);
340
+ });
341
+ transaction();
342
+ }
343
+ doUpsertContentMetadata(filePath, metadata, contentHash) {
344
+ // Prepare statements inside (potentially) outer transaction
345
+ const metadataStmt = this.db.prepare(`
346
+ INSERT INTO content_metadata (
347
+ file_path, content_hash,
348
+ frontmatter_start_line, frontmatter_start_col, frontmatter_start_offset,
349
+ frontmatter_end_line, frontmatter_end_col, frontmatter_end_offset
350
+ )
351
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
352
+ ON CONFLICT(file_path) DO UPDATE SET
353
+ content_hash = excluded.content_hash,
354
+ frontmatter_start_line = excluded.frontmatter_start_line,
355
+ frontmatter_start_col = excluded.frontmatter_start_col,
356
+ frontmatter_start_offset = excluded.frontmatter_start_offset,
357
+ frontmatter_end_line = excluded.frontmatter_end_line,
358
+ frontmatter_end_col = excluded.frontmatter_end_col,
359
+ frontmatter_end_offset = excluded.frontmatter_end_offset
360
+ `);
361
+ const deleteLinksStmt = this.db.prepare('DELETE FROM links_with_positions WHERE source_path = ?');
362
+ const deleteTagsStmt = this.db.prepare('DELETE FROM tags_with_positions WHERE file_path = ?');
363
+ const deleteHeadingsStmt = this.db.prepare('DELETE FROM headings_with_positions WHERE file_path = ?');
364
+ const deleteBlocksStmt = this.db.prepare('DELETE FROM blocks_with_positions WHERE file_path = ?');
365
+ const deleteEmbedsStmt = this.db.prepare('DELETE FROM embeds_with_positions WHERE file_path = ?');
366
+ const deleteSectionsStmt = this.db.prepare('DELETE FROM sections_with_positions WHERE file_path = ?');
367
+ const frontmatter = metadata.frontmatter;
368
+ metadataStmt.run(filePath, contentHash, frontmatter?.position.start.line ?? null, frontmatter?.position.start.col ?? null, frontmatter?.position.start.offset ?? null, frontmatter?.position.end.line ?? null, frontmatter?.position.end.col ?? null, frontmatter?.position.end.offset ?? null);
369
+ // Delete existing metadata items
370
+ deleteLinksStmt.run(filePath);
371
+ deleteTagsStmt.run(filePath);
372
+ deleteHeadingsStmt.run(filePath);
373
+ deleteBlocksStmt.run(filePath);
374
+ deleteEmbedsStmt.run(filePath);
375
+ deleteSectionsStmt.run(filePath);
376
+ // Prepare INSERT statements
377
+ const linkStmt = this.db.prepare(`
378
+ INSERT INTO links_with_positions (
379
+ source_path, target_path, target_id, link_target, original, display_text,
380
+ start_line, start_col, start_offset,
381
+ end_line, end_col, end_offset
382
+ )
383
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
384
+ `);
385
+ const tagStmt = this.db.prepare(`
386
+ INSERT INTO tags_with_positions (
387
+ file_path, tag,
388
+ start_line, start_col, start_offset,
389
+ end_line, end_col, end_offset
390
+ )
391
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
392
+ `);
393
+ const headingStmt = this.db.prepare(`
394
+ INSERT INTO headings_with_positions (
395
+ file_path, heading, level,
396
+ start_line, start_col, start_offset,
397
+ end_line, end_col, end_offset
398
+ )
399
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
400
+ `);
401
+ const blockStmt = this.db.prepare(`
402
+ INSERT INTO blocks_with_positions (
403
+ file_path, block_id,
404
+ start_line, start_col, start_offset,
405
+ end_line, end_col, end_offset
406
+ )
407
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
408
+ `);
409
+ const embedStmt = this.db.prepare(`
410
+ INSERT INTO embeds_with_positions (
411
+ file_path, target_path, original, display_text,
412
+ start_line, start_col, start_offset,
413
+ end_line, end_col, end_offset
414
+ )
415
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
416
+ `);
417
+ const sectionStmt = this.db.prepare(`
418
+ INSERT INTO sections_with_positions (
419
+ file_path, section_id, type,
420
+ start_line, start_col, start_offset,
421
+ end_line, end_col, end_offset
422
+ )
423
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
424
+ `);
425
+ // Insert links
426
+ if (metadata.links && metadata.links.length > 0) {
427
+ for (const link of metadata.links) {
428
+ linkStmt.run(filePath, link.link, null, link.link, link.original, link.displayText ?? null, link.position.start.line, link.position.start.col, link.position.start.offset, link.position.end.line, link.position.end.col, link.position.end.offset);
429
+ }
430
+ }
431
+ // Insert tags
432
+ if (metadata.tags && metadata.tags.length > 0) {
433
+ for (const tag of metadata.tags) {
434
+ tagStmt.run(filePath, tag.tag, tag.position.start.line, tag.position.start.col, tag.position.start.offset, tag.position.end.line, tag.position.end.col, tag.position.end.offset);
435
+ }
436
+ }
437
+ // Insert headings
438
+ if (metadata.headings && metadata.headings.length > 0) {
439
+ for (const heading of metadata.headings) {
440
+ headingStmt.run(filePath, heading.heading, heading.level, heading.position.start.line, heading.position.start.col, heading.position.start.offset, heading.position.end.line, heading.position.end.col, heading.position.end.offset);
441
+ }
442
+ }
443
+ // Insert blocks
444
+ if (metadata.blocks && metadata.blocks.length > 0) {
445
+ for (const block of metadata.blocks) {
446
+ blockStmt.run(filePath, block.id, block.position.start.line, block.position.start.col, block.position.start.offset, block.position.end.line, block.position.end.col, block.position.end.offset);
447
+ }
448
+ }
449
+ // Insert embeds
450
+ if (metadata.embeds && metadata.embeds.length > 0) {
451
+ for (const embed of metadata.embeds) {
452
+ embedStmt.run(filePath, embed.link, embed.original, embed.displayText ?? null, embed.position.start.line, embed.position.start.col, embed.position.start.offset, embed.position.end.line, embed.position.end.col, embed.position.end.offset);
453
+ }
454
+ }
455
+ // Insert sections
456
+ if (metadata.sections && metadata.sections.length > 0) {
457
+ for (const section of metadata.sections) {
458
+ sectionStmt.run(filePath, section.id, section.type, section.position.start.line, section.position.start.col, section.position.start.offset, section.position.end.line, section.position.end.col, section.position.end.offset);
459
+ }
460
+ }
131
461
  }
132
- getBacklinks(noteId) {
133
- const sql = `
134
- SELECT n.* FROM notes n
135
- JOIN links l ON n.id = l.source_id
136
- WHERE l.target_id = ?
137
- `;
138
- const rows = this.db.prepare(sql).all(noteId);
139
- return this.rowsToNotes(rows);
462
+ getContentMetadata(filePath) {
463
+ const metadataRow = this.db.prepare('SELECT * FROM content_metadata WHERE file_path = ?').get(filePath);
464
+ if (!metadataRow)
465
+ return null;
466
+ const result = {};
467
+ // Frontmatter
468
+ if (metadataRow.frontmatter_start_line != null &&
469
+ metadataRow.frontmatter_start_offset != null &&
470
+ metadataRow.frontmatter_end_offset != null) {
471
+ result.frontmatter = {
472
+ position: {
473
+ start: {
474
+ line: metadataRow.frontmatter_start_line,
475
+ col: metadataRow.frontmatter_start_col,
476
+ offset: metadataRow.frontmatter_start_offset
477
+ },
478
+ end: {
479
+ line: metadataRow.frontmatter_end_line,
480
+ col: metadataRow.frontmatter_end_col,
481
+ offset: metadataRow.frontmatter_end_offset
482
+ }
483
+ }
484
+ };
485
+ }
486
+ // Links
487
+ const linkRows = this.db.prepare('SELECT * FROM links_with_positions WHERE source_path = ?').all(filePath);
488
+ if (linkRows.length > 0) {
489
+ result.links = linkRows.map(row => ({
490
+ link: row.link_target || row.target_path || row.target_id || '',
491
+ original: row.original,
492
+ displayText: row.display_text ?? undefined,
493
+ position: {
494
+ start: {
495
+ line: row.start_line,
496
+ col: row.start_col,
497
+ offset: row.start_offset
498
+ },
499
+ end: {
500
+ line: row.end_line,
501
+ col: row.end_col,
502
+ offset: row.end_offset
503
+ }
504
+ }
505
+ }));
506
+ }
507
+ // Tags
508
+ const tagRows = this.db.prepare('SELECT * FROM tags_with_positions WHERE file_path = ?').all(filePath);
509
+ if (tagRows.length > 0) {
510
+ result.tags = tagRows.map(row => ({
511
+ tag: row.tag,
512
+ position: {
513
+ start: {
514
+ line: row.start_line,
515
+ col: row.start_col,
516
+ offset: row.start_offset
517
+ },
518
+ end: {
519
+ line: row.end_line,
520
+ col: row.end_col,
521
+ offset: row.end_offset
522
+ }
523
+ }
524
+ }));
525
+ }
526
+ // Headings
527
+ const headingRows = this.db.prepare('SELECT * FROM headings_with_positions WHERE file_path = ?').all(filePath);
528
+ if (headingRows.length > 0) {
529
+ result.headings = headingRows.map(row => ({
530
+ heading: row.heading,
531
+ level: row.level,
532
+ position: {
533
+ start: {
534
+ line: row.start_line,
535
+ col: row.start_col,
536
+ offset: row.start_offset
537
+ },
538
+ end: {
539
+ line: row.end_line,
540
+ col: row.end_col,
541
+ offset: row.end_offset
542
+ }
543
+ }
544
+ }));
545
+ }
546
+ // Blocks
547
+ const blockRows = this.db.prepare('SELECT * FROM blocks_with_positions WHERE file_path = ?').all(filePath);
548
+ if (blockRows.length > 0) {
549
+ result.blocks = blockRows.map(row => ({
550
+ id: row.block_id,
551
+ position: {
552
+ start: {
553
+ line: row.start_line,
554
+ col: row.start_col,
555
+ offset: row.start_offset
556
+ },
557
+ end: {
558
+ line: row.end_line,
559
+ col: row.end_col,
560
+ offset: row.end_offset
561
+ }
562
+ }
563
+ }));
564
+ }
565
+ // Embeds
566
+ const embedRows = this.db.prepare('SELECT * FROM embeds_with_positions WHERE file_path = ?').all(filePath);
567
+ if (embedRows.length > 0) {
568
+ result.embeds = embedRows.map(row => ({
569
+ link: row.target_path,
570
+ original: row.original,
571
+ displayText: row.display_text ?? undefined,
572
+ position: {
573
+ start: {
574
+ line: row.start_line,
575
+ col: row.start_col,
576
+ offset: row.start_offset
577
+ },
578
+ end: {
579
+ line: row.end_line,
580
+ col: row.end_col,
581
+ offset: row.end_offset
582
+ }
583
+ }
584
+ }));
585
+ }
586
+ // Sections
587
+ const sectionRows = this.db.prepare('SELECT * FROM sections_with_positions WHERE file_path = ?').all(filePath);
588
+ if (sectionRows.length > 0) {
589
+ result.sections = sectionRows.map(row => ({
590
+ id: row.section_id,
591
+ type: row.type,
592
+ position: {
593
+ start: {
594
+ line: row.start_line,
595
+ col: row.start_col,
596
+ offset: row.start_offset
597
+ },
598
+ end: {
599
+ line: row.end_line,
600
+ col: row.end_col,
601
+ offset: row.end_offset
602
+ }
603
+ }
604
+ }));
605
+ }
606
+ return result;
140
607
  }
141
- getOrphans() {
142
- const sql = `
143
- SELECT n.* FROM notes n
144
- LEFT JOIN links l1 ON n.id = l1.source_id
145
- LEFT JOIN links l2 ON n.id = l2.target_id
146
- WHERE l1.source_id IS NULL AND l2.target_id IS NULL
147
- `;
148
- const rows = this.db.prepare(sql).all();
149
- return this.rowsToNotes(rows);
608
+ /**
609
+ * Get sections for a file (section-level query). Returns empty array if file has no metadata.
610
+ */
611
+ getSectionsForFile(filePath) {
612
+ const meta = this.getContentMetadata(filePath);
613
+ return meta?.sections ?? [];
150
614
  }
151
- getAllNotes() {
152
- const rows = this.db.prepare('SELECT * FROM notes').all();
153
- return this.rowsToNotes(rows);
615
+ // Batch operations
616
+ upsertFilesBatch(files) {
617
+ if (files.length === 0)
618
+ return;
619
+ const stmt = this.db.prepare(`
620
+ INSERT INTO files (path, name, basename, extension, parent, ctime, mtime, size, content_hash)
621
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
622
+ ON CONFLICT(path) DO UPDATE SET
623
+ name = excluded.name,
624
+ basename = excluded.basename,
625
+ extension = excluded.extension,
626
+ parent = excluded.parent,
627
+ ctime = excluded.ctime,
628
+ mtime = excluded.mtime,
629
+ size = excluded.size,
630
+ content_hash = excluded.content_hash
631
+ `);
632
+ const transaction = this.db.transaction(() => {
633
+ for (const { file, contentHash } of files) {
634
+ stmt.run(file.path, file.name, file.basename, file.extension, file.parent, file.stat.ctime, file.stat.mtime, file.stat.size, contentHash);
635
+ }
636
+ });
637
+ transaction();
154
638
  }
155
- deleteNoteByPath(path) {
156
- this.db.prepare('DELETE FROM notes WHERE path = ?').run(path);
639
+ upsertContentMetadataBatch(items) {
640
+ if (items.length === 0)
641
+ return;
642
+ const transaction = this.db.transaction(() => {
643
+ for (const { filePath, metadata, contentHash } of items) {
644
+ this.doUpsertContentMetadata(filePath, metadata, contentHash);
645
+ }
646
+ });
647
+ transaction();
157
648
  }
158
649
  getStats() {
159
- const totalNotes = this.db.prepare('SELECT COUNT(*) as count FROM notes').get().count;
160
- const totalLinks = this.db.prepare('SELECT COUNT(*) as count FROM links').get().count;
161
- const orphans = this.db.prepare('SELECT COUNT(*) as count FROM notes n LEFT JOIN links l1 ON n.id = l1.source_id LEFT JOIN links l2 ON n.id = l2.target_id WHERE l1.source_id IS NULL AND l2.target_id IS NULL').get().count;
650
+ const totalNotes = this.db.prepare('SELECT COUNT(*) AS count FROM files').get().count;
651
+ const totalLinks = this.db.prepare('SELECT COUNT(*) AS count FROM links_with_positions').get().count;
652
+ const orphans = this.db.prepare(`
653
+ SELECT COUNT(*) AS count FROM files f
654
+ WHERE NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.source_path = f.path)
655
+ AND NOT EXISTS (SELECT 1 FROM links_with_positions l WHERE l.target_path = f.path)
656
+ `).get().count;
162
657
  return { totalNotes, totalLinks, orphans };
163
658
  }
659
+ /** Graph data from new structure (files + links_with_positions). */
164
660
  getGraphData() {
165
- const nodes = this.db.prepare('SELECT id, title, path, tags FROM notes').all();
166
- const edges = this.db.prepare('SELECT source_id, target_id FROM links').all();
661
+ const fileRows = this.db.prepare(`
662
+ SELECT f.path, f.basename,
663
+ (SELECT group_concat(DISTINCT tag) FROM tags_with_positions WHERE file_path = f.path) AS tags_str
664
+ FROM files f
665
+ `).all();
666
+ const edgeRows = this.db.prepare(`
667
+ SELECT DISTINCT source_path, target_path FROM links_with_positions WHERE target_path IS NOT NULL
668
+ `).all();
167
669
  return {
168
- nodes: nodes.map((n) => ({
169
- id: n.id,
170
- title: n.title,
670
+ nodes: fileRows.map(n => ({
671
+ id: n.path,
672
+ title: n.basename,
171
673
  path: n.path,
172
- tags: JSON.parse(n.tags)
674
+ tags: n.tags_str ? n.tags_str.split(',') : []
173
675
  })),
174
- edges: edges.map((e) => ({
175
- source: e.source_id,
176
- target: e.target_id
177
- }))
178
- };
179
- }
180
- rowToNote(row) {
181
- const links = this.db.prepare('SELECT target_id FROM links WHERE source_id = ?').all(row.id);
182
- const backlinks = this.db.prepare('SELECT source_id FROM links WHERE target_id = ?').all(row.id);
183
- return {
184
- id: row.id,
185
- path: row.path,
186
- title: row.title,
187
- content: row.content,
188
- frontmatter: JSON.parse(row.frontmatter),
189
- tags: JSON.parse(row.tags),
190
- links: links.map((l) => l.target_id),
191
- backlinks: backlinks.map((l) => l.source_id),
192
- hash: row.hash,
193
- createdAt: row.created_at,
194
- modifiedAt: row.modified_at
676
+ edges: edgeRows.map(e => ({ source: e.source_path, target: e.target_path }))
195
677
  };
196
678
  }
197
- getNotesWithLinksBatch(noteIds) {
198
- if (noteIds.length === 0)
199
- return new Map();
200
- const placeholders = noteIds.map(() => '?').join(',');
201
- // Single query to get all links for all notes
202
- const linksSql = `
203
- SELECT source_id, json_group_array(target_id) as targets
204
- FROM links
205
- WHERE source_id IN (${placeholders})
206
- GROUP BY source_id
207
- `;
208
- const backlinksSql = `
209
- SELECT target_id, json_group_array(source_id) as sources
210
- FROM links
211
- WHERE target_id IN (${placeholders})
212
- GROUP BY target_id
213
- `;
214
- const result = new Map();
215
- // Initialize with empty arrays
216
- for (const id of noteIds) {
217
- result.set(id, { links: [], backlinks: [] });
218
- }
219
- // Populate links
220
- const linksRows = this.db.prepare(linksSql).all(...noteIds);
221
- for (const row of linksRows) {
222
- const targets = JSON.parse(row.targets);
223
- result.get(row.source_id).links = targets;
224
- }
225
- // Populate backlinks
226
- const backlinksRows = this.db.prepare(backlinksSql).all(...noteIds);
227
- for (const row of backlinksRows) {
228
- const sources = JSON.parse(row.sources);
229
- result.get(row.target_id).backlinks = sources;
230
- }
231
- return result;
232
- }
233
- rowsToNotes(rows) {
234
- if (rows.length === 0)
235
- return [];
236
- // Extract all note IDs
237
- const noteIds = rows.map(row => row.id);
238
- // Batch load all links and backlinks in 2 queries
239
- const linkData = this.getNotesWithLinksBatch(noteIds);
240
- // Map rows to notes using batched link data
241
- return rows.map(row => {
242
- const links = linkData.get(row.id)?.links || [];
243
- const backlinks = linkData.get(row.id)?.backlinks || [];
244
- return {
245
- id: row.id,
246
- path: row.path,
247
- title: row.title,
248
- content: row.content,
249
- frontmatter: JSON.parse(row.frontmatter),
250
- tags: JSON.parse(row.tags),
251
- links,
252
- backlinks,
253
- hash: row.hash,
254
- createdAt: row.created_at,
255
- modifiedAt: row.modified_at,
256
- };
257
- });
258
- }
259
679
  }
260
680
  exports.DatabaseManager = DatabaseManager;
261
681
  //# sourceMappingURL=database.js.map