@dinoxx/dinox-cli 2.0.0 → 2.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 (48) hide show
  1. package/dist/commands/notes/repo/read.d.ts +8 -0
  2. package/dist/commands/notes/repo/read.js +321 -0
  3. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  4. package/dist/commands/notes/repo/resolve.js +187 -0
  5. package/dist/commands/notes/repo/search.d.ts +42 -0
  6. package/dist/commands/notes/repo/search.js +513 -0
  7. package/dist/commands/notes/repo/shared.d.ts +190 -0
  8. package/dist/commands/notes/repo/shared.js +102 -0
  9. package/dist/commands/notes/repo/write.d.ts +24 -0
  10. package/dist/commands/notes/repo/write.js +251 -0
  11. package/dist/commands/notes/repo.d.ts +7 -186
  12. package/dist/commands/notes/repo.js +7 -1360
  13. package/dist/commands/schema/commandMetadata.js +4 -4
  14. package/dist/commands/schema/index.d.ts +1 -0
  15. package/dist/commands/schema/index.js +23 -2
  16. package/dist/commands/schema/renderSkills.d.ts +4 -0
  17. package/dist/commands/schema/renderSkills.js +41 -2
  18. package/dist/commands/skills/index.d.ts +26 -0
  19. package/dist/commands/skills/index.js +115 -11
  20. package/dist/commands/storage/repo/configs.d.ts +30 -0
  21. package/dist/commands/storage/repo/configs.js +259 -0
  22. package/dist/commands/storage/repo/operations.d.ts +25 -0
  23. package/dist/commands/storage/repo/operations.js +337 -0
  24. package/dist/commands/storage/repo/remote.d.ts +32 -0
  25. package/dist/commands/storage/repo/remote.js +132 -0
  26. package/dist/commands/storage/repo/resources.d.ts +12 -0
  27. package/dist/commands/storage/repo/resources.js +134 -0
  28. package/dist/commands/storage/repo/shared.d.ts +181 -0
  29. package/dist/commands/storage/repo/shared.js +36 -0
  30. package/dist/commands/storage/repo/targets.d.ts +12 -0
  31. package/dist/commands/storage/repo/targets.js +178 -0
  32. package/dist/commands/storage/repo.d.ts +3 -166
  33. package/dist/commands/storage/repo.js +4 -1034
  34. package/dist/contract/snapshot.d.ts +30 -0
  35. package/dist/contract/snapshot.js +114 -0
  36. package/dist/daemon/client.js +2 -2
  37. package/dist/powersync/uploader.js +27 -2
  38. package/dist/skills/commandRefs.d.ts +21 -0
  39. package/dist/skills/commandRefs.js +132 -0
  40. package/dist/skills/generate.d.ts +13 -0
  41. package/dist/skills/generate.js +80 -0
  42. package/dist/skills/registry.d.ts +13 -0
  43. package/dist/skills/registry.js +58 -0
  44. package/dist/utils/errorCodes.d.ts +311 -0
  45. package/dist/utils/errorCodes.js +92 -0
  46. package/dist/utils/errors.d.ts +13 -2
  47. package/dist/utils/errors.js +11 -0
  48. package/package.json +11 -4
@@ -1,1360 +1,7 @@
1
- import { DinoxError } from '../../utils/errors.js';
2
- import { nowIsoUtc } from '../../utils/time.js';
3
- import { tokenizeWithJieba } from '../../utils/tokenize.js';
4
- import { canonicalizeBoxPath, listBoxes } from '../boxes/repo.js';
5
- import { listTags } from '../tags/repo.js';
6
- import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
7
- import { buildNoteSearchSqlFilter } from './searchSql.js';
8
- const MAX_SEARCH_SUMMARY_CHARS = 200;
9
- function normalizeStoredFlag(value) {
10
- if (typeof value === 'number') {
11
- return value === 1;
12
- }
13
- if (typeof value === 'string') {
14
- return value.trim() === '1';
15
- }
16
- return false;
17
- }
18
- function normalizeStoredStarred(value) {
19
- return normalizeStoredFlag(value);
20
- }
21
- /**
22
- * Resolve box names to ids and run the note search. Shared by the local command path and
23
- * the daemon RPC handler so both produce identical results from one implementation.
24
- */
25
- export async function executeNoteSearch(db, params) {
26
- const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, params.boxValues);
27
- const result = await searchNotesPage(db, params.keyword, {
28
- includeDeleted: params.includeDeleted,
29
- tagsExpression: params.tagsExpression,
30
- createdAtFrom: params.createdAtFrom,
31
- createdAtTo: params.createdAtTo,
32
- starred: params.starred,
33
- zettelBoxIds,
34
- matchEmptyZettelBoxes: params.matchEmptyZettelBoxes,
35
- sqlExpression: params.sqlExpression,
36
- limit: params.limit,
37
- offset: params.offset,
38
- });
39
- return { rows: result.rows, totalCount: result.totalCount, zettelBoxIds };
40
- }
41
- export async function searchNotes(db, query, options) {
42
- return (await searchNotesPage(db, query, options)).rows;
43
- }
44
- export async function searchNotesPage(db, query, options) {
45
- const normalizedQuery = query.trim();
46
- const tagFilter = buildTagFilter(options.tagsExpression, 'n');
47
- const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
48
- const starredFilter = buildStarredFilter(options.starred, 'n');
49
- const zettelBoxFilter = buildZettelBoxFilter(options.zettelBoxIds, options.matchEmptyZettelBoxes, 'n');
50
- const sqlFilter = await buildNoteSearchSqlFilter({
51
- expression: options.sqlExpression,
52
- noteAlias: 'n',
53
- resolveZettelBoxNames: (names) => resolveZettelBoxIdsForSearch(db, names),
54
- });
55
- let rows = [];
56
- if (!options.includeDeleted) {
57
- const ftsMatch = buildFtsMatchQuery(normalizedQuery);
58
- if (ftsMatch) {
59
- try {
60
- const ftsResult = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter, starredFilter, zettelBoxFilter, sqlFilter, options.limit, options.offset);
61
- return {
62
- rows: await enrichSearchRows(db, ftsResult.rows),
63
- totalCount: ftsResult.totalCount,
64
- };
65
- }
66
- catch (error) {
67
- const message = error instanceof Error ? error.message : String(error);
68
- const normalized = message.toLowerCase();
69
- // FTS table may not be initialized yet, or query syntax may fail.
70
- if (normalized.includes('no such table') ||
71
- normalized.includes('fts5') ||
72
- normalized.includes('malformed match') ||
73
- normalized.includes('syntax error')) {
74
- // Fallback to legacy LIKE search below.
75
- }
76
- else {
77
- throw error;
78
- }
79
- }
80
- }
81
- }
82
- const whereClauses = [
83
- options.includeDeleted ? '1 = 1' : '(n.is_del IS NULL OR n.is_del = 0)',
84
- ];
85
- const params = [];
86
- if (normalizedQuery.length > 0) {
87
- const like = `%${escapeLikePattern(normalizedQuery)}%`;
88
- whereClauses.push(`
89
- (
90
- (n.title IS NOT NULL AND n.title LIKE ? ESCAPE '\\')
91
- OR (n.content_text IS NOT NULL AND n.content_text LIKE ? ESCAPE '\\')
92
- OR (n.content_md IS NOT NULL AND n.content_md LIKE ? ESCAPE '\\')
93
- )
94
- `);
95
- params.push(like, like, like);
96
- }
97
- if (tagFilter) {
98
- whereClauses.push(tagFilter.sql);
99
- params.push(...tagFilter.params);
100
- }
101
- if (createdAtFilter) {
102
- whereClauses.push(createdAtFilter.sql);
103
- params.push(...createdAtFilter.params);
104
- }
105
- if (starredFilter) {
106
- whereClauses.push(starredFilter.sql);
107
- params.push(...starredFilter.params);
108
- }
109
- if (zettelBoxFilter) {
110
- whereClauses.push(zettelBoxFilter.sql);
111
- params.push(...zettelBoxFilter.params);
112
- }
113
- if (sqlFilter) {
114
- whereClauses.push(sqlFilter.sql);
115
- params.push(...sqlFilter.params);
116
- }
117
- const baseFromWhere = `
118
- FROM c_note n
119
- WHERE ${whereClauses.join('\n AND ')}
120
- `;
121
- const paginated = typeof options.limit === 'number';
122
- const totalCount = paginated ? await countSearchRows(db, baseFromWhere, params) : null;
123
- if (totalCount === 0) {
124
- return { rows: [], totalCount };
125
- }
126
- rows = await db.getAll(`
127
- SELECT
128
- n.id,
129
- n.title,
130
- n.summary,
131
- n.content_text,
132
- n.tags,
133
- n.zettel_boxes,
134
- n.created_at,
135
- n.starred_at,
136
- CASE WHEN n.starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred
137
- ${baseFromWhere}
138
- ORDER BY COALESCE(n.updated_at, n.created_at) DESC, n.id DESC
139
- ${buildPaginationSql(options.limit, options.offset)}
140
- `, appendPaginationParams(params, options.limit, options.offset));
141
- return { rows: await enrichSearchRows(db, rows), totalCount: totalCount ?? rows.length };
142
- }
143
- async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, starredFilter, zettelBoxFilter, sqlFilter, limit, offset) {
144
- const whereClauses = ['(n.is_del IS NULL OR n.is_del = 0)', 'note_local_fts MATCH ?'];
145
- const params = [matchQuery];
146
- if (tagFilter) {
147
- whereClauses.push(tagFilter.sql);
148
- params.push(...tagFilter.params);
149
- }
150
- if (createdAtFilter) {
151
- whereClauses.push(createdAtFilter.sql);
152
- params.push(...createdAtFilter.params);
153
- }
154
- if (starredFilter) {
155
- whereClauses.push(starredFilter.sql);
156
- params.push(...starredFilter.params);
157
- }
158
- if (zettelBoxFilter) {
159
- whereClauses.push(zettelBoxFilter.sql);
160
- params.push(...zettelBoxFilter.params);
161
- }
162
- if (sqlFilter) {
163
- whereClauses.push(sqlFilter.sql);
164
- params.push(...sqlFilter.params);
165
- }
166
- const baseFromWhere = `
167
- FROM note_local_fts f
168
- JOIN c_note n ON n.id = f.note_id
169
- WHERE ${whereClauses.join('\n AND ')}
170
- `;
171
- const paginated = typeof limit === 'number';
172
- const totalCount = paginated ? await countSearchRows(db, baseFromWhere, params) : null;
173
- if (totalCount === 0) {
174
- return { rows: [], totalCount };
175
- }
176
- const rows = await db.getAll(`
177
- SELECT
178
- n.id,
179
- n.title,
180
- n.summary,
181
- n.content_text,
182
- n.tags,
183
- n.zettel_boxes,
184
- n.created_at,
185
- n.starred_at,
186
- CASE WHEN n.starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred
187
- ${baseFromWhere}
188
- ORDER BY bm25(note_local_fts, 0.0, 8.0, 4.0, 1.0) ASC, COALESCE(n.updated_at, n.created_at) DESC, n.id DESC
189
- ${buildPaginationSql(limit, offset)}
190
- `, appendPaginationParams(params, limit, offset));
191
- return { rows, totalCount: totalCount ?? rows.length };
192
- }
193
- function escapeLikePattern(value) {
194
- return value.replace(/[\\%_]/g, (match) => `\\${match}`);
195
- }
196
- async function countSearchRows(db, fromWhereSql, params) {
197
- const rows = await db.getAll(`
198
- SELECT COUNT(*) AS total_count
199
- ${fromWhereSql}
200
- `, params);
201
- const value = Number(rows[0]?.total_count ?? 0);
202
- return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
203
- }
204
- function buildPaginationSql(limit, offset) {
205
- return typeof limit === 'number' ? 'LIMIT ? OFFSET ?' : '';
206
- }
207
- function appendPaginationParams(params, limit, offset) {
208
- if (typeof limit !== 'number') {
209
- return params;
210
- }
211
- return [...params, limit, offset ?? 0];
212
- }
213
- async function enrichSearchRows(db, rows) {
214
- const allBoxIds = Array.from(new Set(rows.flatMap((row) => parseZettelBoxIds(row.zettel_boxes))));
215
- const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
216
- return rows.map((row) => {
217
- const summary = resolveSummary(row.summary, row.content_text);
218
- const zettelBoxes = parseZettelBoxIds(row.zettel_boxes)
219
- .map((id) => boxNameMap.get(id))
220
- .filter((name) => typeof name === 'string' && name.length > 0);
221
- return {
222
- id: row.id,
223
- title: row.title?.trim() ?? '',
224
- summary,
225
- tags: parseStoredTags(row.tags),
226
- created_at: row.created_at,
227
- starred_at: row.starred_at ?? null,
228
- is_starred: normalizeStoredStarred(row.is_starred),
229
- zettel_boxes: zettelBoxes,
230
- };
231
- });
232
- }
233
- function resolveSummary(summary, contentText) {
234
- const summaryValue = summary?.trim() ?? '';
235
- if (summaryValue.length > 0) {
236
- return truncateSearchSummary(summaryValue);
237
- }
238
- return truncateSearchSummary(contentText?.trim() ?? '');
239
- }
240
- function truncateSearchSummary(value) {
241
- const chars = Array.from(value);
242
- if (chars.length <= MAX_SEARCH_SUMMARY_CHARS) {
243
- return value;
244
- }
245
- return chars.slice(0, MAX_SEARCH_SUMMARY_CHARS).join('');
246
- }
247
- export function parseZettelBoxIds(raw) {
248
- const value = raw?.trim();
249
- if (!value) {
250
- return [];
251
- }
252
- try {
253
- const parsed = JSON.parse(value);
254
- if (!Array.isArray(parsed)) {
255
- return [];
256
- }
257
- return Array.from(new Set(parsed
258
- .map((entry) => String(entry).trim())
259
- .filter((entry) => entry.length > 0)));
260
- }
261
- catch {
262
- return [];
263
- }
264
- }
265
- async function loadZettelBoxNameMap(db, ids) {
266
- const map = new Map();
267
- if (ids.length === 0) {
268
- return map;
269
- }
270
- const chunkSize = 500;
271
- for (let i = 0; i < ids.length; i += chunkSize) {
272
- const batch = ids.slice(i, i + chunkSize);
273
- const placeholders = batch.map(() => '?').join(', ');
274
- const rows = await db.getAll(`
275
- SELECT id, name, path
276
- FROM c_zettel_box
277
- WHERE (is_del IS NULL OR is_del = 0)
278
- AND id IN (${placeholders})
279
- `, batch);
280
- for (const row of rows) {
281
- const id = row.id?.trim() ?? '';
282
- if (!id) {
283
- continue;
284
- }
285
- const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
286
- map.set(id, path || row.name?.trim() || '');
287
- }
288
- }
289
- return map;
290
- }
291
- function buildFtsMatchQuery(query) {
292
- const tokens = tokenizeWithJieba(query, 32)
293
- .map((token) => token.trim())
294
- .filter((token) => token.length > 0);
295
- if (tokens.length === 0) {
296
- return null;
297
- }
298
- const unique = Array.from(new Set(tokens));
299
- return unique.map((token) => `"${escapeFtsToken(token)}"`).join(' AND ');
300
- }
301
- function escapeFtsToken(token) {
302
- return token.replace(/"/g, '""');
303
- }
304
- function buildTagFilter(tagsExpression, noteAlias) {
305
- const expression = tagsExpression?.trim() ?? '';
306
- if (!expression) {
307
- return null;
308
- }
309
- if (isExplicitEmptyArrayLiteral(expression)) {
310
- return {
311
- sql: `NOT (${buildJsonArrayHasValuesSql(noteAlias, 'tags', 'tag_item')})`,
312
- params: [],
313
- };
314
- }
315
- let ast;
316
- try {
317
- ast = parseTagExpression(expression);
318
- }
319
- catch (error) {
320
- const message = error instanceof Error ? error.message : String(error);
321
- throw new DinoxError(`Invalid --tags expression: ${message}`);
322
- }
323
- const params = [];
324
- const sql = compileTagExpression(ast, noteAlias, params);
325
- return { sql: `(${sql})`, params };
326
- }
327
- function buildCreatedAtFilter(createdAtFrom, createdAtTo, noteAlias) {
328
- const clauses = [];
329
- const params = [];
330
- if (createdAtFrom) {
331
- clauses.push(`(${noteAlias}.created_at IS NOT NULL AND ${noteAlias}.created_at >= ?)`);
332
- params.push(createdAtFrom);
333
- }
334
- if (createdAtTo) {
335
- clauses.push(`(${noteAlias}.created_at IS NOT NULL AND ${noteAlias}.created_at <= ?)`);
336
- params.push(createdAtTo);
337
- }
338
- if (clauses.length === 0) {
339
- return null;
340
- }
341
- return {
342
- sql: `(${clauses.join(' AND ')})`,
343
- params,
344
- };
345
- }
346
- function buildStarredFilter(starred, noteAlias) {
347
- if (typeof starred !== 'boolean') {
348
- return null;
349
- }
350
- return {
351
- sql: starred ? `(${noteAlias}.starred_at IS NOT NULL)` : `(${noteAlias}.starred_at IS NULL)`,
352
- params: [],
353
- };
354
- }
355
- function buildZettelBoxFilter(zettelBoxIds, matchEmptyZettelBoxes, noteAlias) {
356
- const normalizedIds = Array.from(new Set((zettelBoxIds ?? [])
357
- .map((id) => id.trim())
358
- .filter((id) => id.length > 0)));
359
- const shouldMatchEmpty = Boolean(matchEmptyZettelBoxes);
360
- if (normalizedIds.length === 0 && !shouldMatchEmpty) {
361
- return null;
362
- }
363
- const clauses = [];
364
- const params = [];
365
- if (normalizedIds.length > 0) {
366
- const placeholders = normalizedIds.map(() => '?').join(', ');
367
- clauses.push(`
368
- EXISTS (
369
- SELECT 1
370
- FROM json_each(
371
- CASE
372
- WHEN json_valid(COALESCE(${noteAlias}.zettel_boxes, '')) THEN ${noteAlias}.zettel_boxes
373
- ELSE '[]'
374
- END
375
- ) box_item
376
- WHERE TRIM(CAST(box_item.value AS TEXT)) IN (${placeholders})
377
- )
378
- `);
379
- params.push(...normalizedIds);
380
- }
381
- if (shouldMatchEmpty) {
382
- clauses.push(`NOT (${buildJsonArrayHasValuesSql(noteAlias, 'zettel_boxes', 'box_item')})`);
383
- }
384
- if (clauses.length === 1) {
385
- return { sql: clauses[0], params };
386
- }
387
- return {
388
- sql: `(${clauses.join(' OR ')})`,
389
- params,
390
- };
391
- }
392
- function parseTagExpression(expression) {
393
- const tokens = tokenizeTagExpression(expression);
394
- if (tokens.length === 0) {
395
- throw new Error('Expression is empty');
396
- }
397
- let index = 0;
398
- const peek = () => tokens[index] ?? null;
399
- const consume = (expected) => {
400
- const token = peek();
401
- if (!token) {
402
- throw new Error('Unexpected end of expression');
403
- }
404
- if (expected && token.type !== expected) {
405
- throw new Error(`Expected ${expected} at position ${token.position + 1}`);
406
- }
407
- index += 1;
408
- return token;
409
- };
410
- const parsePrimary = () => {
411
- const token = peek();
412
- if (!token) {
413
- throw new Error('Unexpected end of expression');
414
- }
415
- if (token.type === 'TERM') {
416
- consume('TERM');
417
- const value = normalizeTagValue(token.value);
418
- if (!value) {
419
- throw new Error(`Empty tag at position ${token.position + 1}`);
420
- }
421
- return { kind: 'tag', value };
422
- }
423
- if (token.type === 'LPAREN') {
424
- consume('LPAREN');
425
- const node = parseOr();
426
- const close = peek();
427
- if (!close || close.type !== 'RPAREN') {
428
- throw new Error(`Missing ')' for '(' at position ${token.position + 1}`);
429
- }
430
- consume('RPAREN');
431
- return node;
432
- }
433
- throw new Error(`Unexpected token '${token.value}' at position ${token.position + 1}`);
434
- };
435
- const parseUnary = () => {
436
- const token = peek();
437
- if (token?.type === 'NOT') {
438
- consume('NOT');
439
- return { kind: 'not', child: parseUnary() };
440
- }
441
- return parsePrimary();
442
- };
443
- const parseAnd = () => {
444
- let node = parseUnary();
445
- while (peek()?.type === 'AND') {
446
- consume('AND');
447
- node = { kind: 'and', left: node, right: parseUnary() };
448
- }
449
- return node;
450
- };
451
- const parseOr = () => {
452
- let node = parseAnd();
453
- while (peek()?.type === 'OR') {
454
- consume('OR');
455
- node = { kind: 'or', left: node, right: parseAnd() };
456
- }
457
- return node;
458
- };
459
- const ast = parseOr();
460
- const extra = peek();
461
- if (extra) {
462
- throw new Error(`Unexpected token '${extra.value}' at position ${extra.position + 1}`);
463
- }
464
- return ast;
465
- }
466
- function tokenizeTagExpression(expression) {
467
- const tokens = [];
468
- let i = 0;
469
- while (i < expression.length) {
470
- const ch = expression[i];
471
- if (/\s/.test(ch)) {
472
- i += 1;
473
- continue;
474
- }
475
- if (ch === '(') {
476
- tokens.push({ type: 'LPAREN', value: ch, position: i });
477
- i += 1;
478
- continue;
479
- }
480
- if (ch === ')') {
481
- tokens.push({ type: 'RPAREN', value: ch, position: i });
482
- i += 1;
483
- continue;
484
- }
485
- if (ch === '"' || ch === "'") {
486
- const quoted = readQuotedToken(expression, i);
487
- tokens.push({ type: 'TERM', value: quoted.value, position: i });
488
- i = quoted.next;
489
- continue;
490
- }
491
- let j = i;
492
- while (j < expression.length) {
493
- const next = expression[j];
494
- if (/\s/.test(next) || next === '(' || next === ')') {
495
- break;
496
- }
497
- j += 1;
498
- }
499
- const raw = expression.slice(i, j);
500
- const upper = raw.toUpperCase();
501
- if (upper === 'AND' || upper === 'OR' || upper === 'NOT') {
502
- tokens.push({ type: upper, value: raw, position: i });
503
- }
504
- else {
505
- tokens.push({ type: 'TERM', value: raw, position: i });
506
- }
507
- i = j;
508
- }
509
- return tokens;
510
- }
511
- function readQuotedToken(expression, start) {
512
- const quote = expression[start];
513
- let i = start + 1;
514
- let value = '';
515
- while (i < expression.length) {
516
- const ch = expression[i];
517
- if (ch === '\\') {
518
- if (i + 1 >= expression.length) {
519
- throw new Error(`Invalid escape at position ${i + 1}`);
520
- }
521
- value += expression[i + 1];
522
- i += 2;
523
- continue;
524
- }
525
- if (ch === quote) {
526
- return { value, next: i + 1 };
527
- }
528
- value += ch;
529
- i += 1;
530
- }
531
- throw new Error(`Unclosed quote at position ${start + 1}`);
532
- }
533
- function compileTagExpression(node, noteAlias, params) {
534
- if (node.kind === 'tag') {
535
- params.push(node.value);
536
- return `
537
- EXISTS (
538
- SELECT 1
539
- FROM json_each(
540
- CASE
541
- WHEN json_valid(COALESCE(${noteAlias}.tags, '')) THEN ${noteAlias}.tags
542
- ELSE '[]'
543
- END
544
- ) tag_item
545
- WHERE LOWER(TRIM(CAST(tag_item.value AS TEXT))) = ?
546
- )
547
- `;
548
- }
549
- if (node.kind === 'not') {
550
- return `NOT (${compileTagExpression(node.child, noteAlias, params)})`;
551
- }
552
- if (node.kind === 'and') {
553
- return `(${compileTagExpression(node.left, noteAlias, params)}) AND (${compileTagExpression(node.right, noteAlias, params)})`;
554
- }
555
- return `(${compileTagExpression(node.left, noteAlias, params)}) OR (${compileTagExpression(node.right, noteAlias, params)})`;
556
- }
557
- function normalizeTagValue(value) {
558
- return value.trim().toLowerCase();
559
- }
560
- function isExplicitEmptyArrayLiteral(value) {
561
- if (!value.startsWith('[')) {
562
- return false;
563
- }
564
- try {
565
- const parsed = JSON.parse(value);
566
- return Array.isArray(parsed) && parsed.length === 0;
567
- }
568
- catch {
569
- return false;
570
- }
571
- }
572
- function buildJsonArrayHasValuesSql(noteAlias, field, itemAlias) {
573
- return `
574
- EXISTS (
575
- SELECT 1
576
- FROM json_each(
577
- CASE
578
- WHEN json_valid(COALESCE(${noteAlias}.${field}, '')) THEN ${noteAlias}.${field}
579
- ELSE '[]'
580
- END
581
- ) ${itemAlias}
582
- WHERE LENGTH(TRIM(CAST(${itemAlias}.value AS TEXT))) > 0
583
- )
584
- `;
585
- }
586
- export async function getNote(db, id) {
587
- return db.getOptional(`
588
- SELECT
589
- id,
590
- title,
591
- content_md,
592
- content_json,
593
- content_text,
594
- created_at,
595
- updated_at,
596
- is_del,
597
- starred_at,
598
- CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
599
- version
600
- FROM c_note
601
- WHERE id = ?
602
- `, [id]);
603
- }
604
- async function getLinkCount(db, direction, noteId) {
605
- const [column, boundColumn] = direction === 'in'
606
- ? ['src_note_id', 'dst_note_id']
607
- : ['dst_note_id', 'src_note_id'];
608
- const row = await db.getOptional(`
609
- SELECT COUNT(1) AS count
610
- FROM c_note_link l
611
- JOIN c_note n ON n.id = l.${column}
612
- WHERE l.${boundColumn} = ?
613
- AND (l.is_del IS NULL OR l.is_del = 0)
614
- AND (n.is_del IS NULL OR n.is_del = 0)
615
- `, [noteId]);
616
- return Number(row?.count ?? 0);
617
- }
618
- function buildPreviewText(contentMd, requestedLines) {
619
- const normalized = (contentMd ?? '').replace(/\r\n/g, '\n');
620
- if (!normalized) {
621
- return {
622
- preview: '',
623
- totalLines: 0,
624
- returnedLines: 0,
625
- truncated: false,
626
- };
627
- }
628
- const lines = normalized.split('\n');
629
- const selected = lines.slice(0, requestedLines);
630
- return {
631
- preview: selected.join('\n'),
632
- totalLines: lines.length,
633
- returnedLines: selected.length,
634
- truncated: selected.length < lines.length,
635
- };
636
- }
637
- export async function getNoteContext(db, id) {
638
- const row = await db.getOptional(`
639
- SELECT
640
- n.id,
641
- n.title,
642
- n.summary,
643
- n.content_text,
644
- n.tags,
645
- n.created_at,
646
- n.updated_at,
647
- n.is_del,
648
- n.starred_at,
649
- CASE WHEN n.starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
650
- n.zettel_boxes AS zettel_boxes_raw
651
- FROM c_note n
652
- WHERE n.id = ?
653
- `, [id]);
654
- if (!row) {
655
- return null;
656
- }
657
- const boxIds = parseZettelBoxIds(row.zettel_boxes_raw);
658
- const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
659
- const [backlinks, outlinks] = await Promise.all([
660
- getLinkCount(db, 'in', id),
661
- getLinkCount(db, 'out', id),
662
- ]);
663
- return {
664
- id: row.id,
665
- title: row.title?.trim() ?? '',
666
- summary: resolveSummary(row.summary, row.content_text),
667
- tags: parseStoredTags(row.tags),
668
- created_at: row.created_at,
669
- updated_at: row.updated_at,
670
- is_del: row.is_del,
671
- starred_at: row.starred_at ?? null,
672
- is_starred: normalizeStoredStarred(row.is_starred),
673
- zettel_boxes: boxIds
674
- .map((boxId) => boxNameMap.get(boxId))
675
- .filter((name) => typeof name === 'string' && name.length > 0),
676
- links: {
677
- backlinks,
678
- outlinks,
679
- },
680
- };
681
- }
682
- export async function getNotePreview(db, id, requestedLines) {
683
- const row = await db.getOptional(`
684
- SELECT
685
- n.id,
686
- n.title,
687
- n.content_md,
688
- n.updated_at
689
- FROM c_note n
690
- WHERE n.id = ?
691
- `, [id]);
692
- if (!row) {
693
- return null;
694
- }
695
- const preview = buildPreviewText(row.content_md, requestedLines);
696
- return {
697
- id: row.id,
698
- title: row.title?.trim() ?? '',
699
- requested_lines: requestedLines,
700
- total_lines: preview.totalLines,
701
- returned_lines: preview.returnedLines,
702
- truncated: preview.truncated,
703
- preview: preview.preview,
704
- updated_at: row.updated_at ?? null,
705
- };
706
- }
707
- function mapNoteDetailRow(note, boxNameMap) {
708
- const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
709
- const zettelBoxes = boxIds
710
- .map((boxId) => boxNameMap.get(boxId))
711
- .filter((name) => typeof name === 'string' && name.length > 0);
712
- return {
713
- id: note.id,
714
- created_at: note.created_at,
715
- updated_at: note.updated_at,
716
- title: note.title,
717
- type: note.type,
718
- source: note.source,
719
- tags: parseStoredTags(note.tags),
720
- is_del: note.is_del,
721
- is_pub: normalizeStoredFlag(note.is_pub),
722
- properties: parseStoredNoteProperties(note.properties),
723
- starred_at: note.starred_at ?? null,
724
- is_starred: normalizeStoredStarred(note.is_starred),
725
- content_md: note.content_md,
726
- content_text: note.content_text,
727
- content_json: parseStoredJsonValue(note.content_json),
728
- zettel_boxes: zettelBoxes,
729
- };
730
- }
731
- function parseStoredJsonValue(raw) {
732
- const value = raw?.trim() ?? '';
733
- if (!value) {
734
- return null;
735
- }
736
- try {
737
- return JSON.parse(value);
738
- }
739
- catch {
740
- return raw;
741
- }
742
- }
743
- function parseStoredNoteProperties(raw) {
744
- const value = raw?.trim() ?? '';
745
- if (!value) {
746
- return {};
747
- }
748
- let parsed;
749
- try {
750
- parsed = JSON.parse(value);
751
- }
752
- catch (cause) {
753
- throw new DinoxError('c_note.properties must be valid JSON', {
754
- code: 'NOTE_INVALID_PROPERTIES',
755
- details: { cause: cause instanceof Error ? cause.message : String(cause) },
756
- recoverable: false,
757
- });
758
- }
759
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
760
- throw new DinoxError('c_note.properties must be a JSON object', {
761
- code: 'NOTE_INVALID_PROPERTIES',
762
- recoverable: false,
763
- });
764
- }
765
- return Object.fromEntries(Object.entries(parsed));
766
- }
767
- function mapNoteExportRow(note, boxNameMap) {
768
- const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
769
- const boxes = boxIds
770
- .map((boxId) => boxNameMap.get(boxId))
771
- .filter((name) => typeof name === 'string' && name.length > 0);
772
- return {
773
- id: note.id,
774
- created_at: note.created_at,
775
- updated_at: note.updated_at,
776
- title: note.title,
777
- type: note.type,
778
- source: note.source,
779
- summary: note.summary,
780
- tags: parseStoredTags(note.tags),
781
- is_del: note.is_del,
782
- is_pub: normalizeStoredFlag(note.is_pub),
783
- properties: parseStoredNoteProperties(note.properties),
784
- starred_at: note.starred_at ?? null,
785
- is_starred: normalizeStoredStarred(note.is_starred),
786
- content_md: note.content_md ?? '',
787
- content_text: note.content_text,
788
- content_json: parseStoredJsonValue(note.content_json),
789
- box_ids: boxIds,
790
- boxes,
791
- };
792
- }
793
- export async function getNoteDetails(db, ids) {
794
- const targetIds = Array.from(new Set(ids
795
- .map((id) => id.trim())
796
- .filter((id) => id.length > 0)));
797
- if (targetIds.length === 0) {
798
- return [];
799
- }
800
- const noteRowsById = new Map();
801
- const chunkSize = 500;
802
- for (let index = 0; index < targetIds.length; index += chunkSize) {
803
- const batch = targetIds.slice(index, index + chunkSize);
804
- const placeholders = batch.map(() => '?').join(', ');
805
- const rows = await db.getAll(`
806
- SELECT
807
- id,
808
- created_at,
809
- updated_at,
810
- title,
811
- type,
812
- source,
813
- tags,
814
- is_del,
815
- is_pub,
816
- properties,
817
- starred_at,
818
- CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
819
- content_md,
820
- content_text,
821
- content_json,
822
- zettel_boxes AS zettel_boxes_raw
823
- FROM c_note
824
- WHERE id IN (${placeholders})
825
- `, batch);
826
- for (const row of rows) {
827
- const id = row.id?.trim() ?? '';
828
- if (!id || noteRowsById.has(id)) {
829
- continue;
830
- }
831
- noteRowsById.set(id, row);
832
- }
833
- }
834
- const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
835
- const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
836
- const details = [];
837
- for (const id of targetIds) {
838
- const row = noteRowsById.get(id);
839
- if (!row) {
840
- continue;
841
- }
842
- details.push(mapNoteDetailRow(row, boxNameMap));
843
- }
844
- return details;
845
- }
846
- export async function getNoteDetail(db, id) {
847
- const details = await getNoteDetails(db, [id]);
848
- return details[0] ?? null;
849
- }
850
- export async function getNoteExportItems(db, ids) {
851
- const targetIds = Array.from(new Set(ids
852
- .map((id) => id.trim())
853
- .filter((id) => id.length > 0)));
854
- if (targetIds.length === 0) {
855
- return [];
856
- }
857
- const noteRowsById = new Map();
858
- const chunkSize = 500;
859
- for (let index = 0; index < targetIds.length; index += chunkSize) {
860
- const batch = targetIds.slice(index, index + chunkSize);
861
- const placeholders = batch.map(() => '?').join(', ');
862
- const rows = await db.getAll(`
863
- SELECT
864
- id,
865
- created_at,
866
- updated_at,
867
- title,
868
- type,
869
- source,
870
- tags,
871
- summary,
872
- is_del,
873
- is_pub,
874
- properties,
875
- starred_at,
876
- CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
877
- content_md,
878
- content_text,
879
- content_json,
880
- zettel_boxes AS zettel_boxes_raw
881
- FROM c_note
882
- WHERE id IN (${placeholders})
883
- `, batch);
884
- for (const row of rows) {
885
- const id = row.id?.trim() ?? '';
886
- if (!id || noteRowsById.has(id)) {
887
- continue;
888
- }
889
- noteRowsById.set(id, row);
890
- }
891
- }
892
- const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
893
- const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
894
- const items = [];
895
- for (const id of targetIds) {
896
- const row = noteRowsById.get(id);
897
- if (!row) {
898
- continue;
899
- }
900
- items.push(mapNoteExportRow(row, boxNameMap));
901
- }
902
- return items;
903
- }
904
- export async function createNote(db, input) {
905
- const userId = input.userId.trim();
906
- if (!userId) {
907
- throw new DinoxError('userId is required to create a note');
908
- }
909
- const now = nowIsoUtc();
910
- const contentJson = JSON.stringify(input.contentJson);
911
- const tags = JSON.stringify(input.tags);
912
- const zettelBoxes = JSON.stringify(input.zettelBoxIds);
913
- const imageDetail = JSON.stringify(Array.isArray(input.imageDetail)
914
- ? input.imageDetail
915
- : deriveImageDetailFromContentJson(input.contentJson, input.id));
916
- const columns = await db.getAll('PRAGMA table_info(c_note)', []);
917
- const columnNames = new Set(columns
918
- .map((row) => row.name?.trim().toLowerCase() ?? '')
919
- .filter((name) => name.length > 0));
920
- const userColumns = [];
921
- if (columnNames.has('user_id')) {
922
- userColumns.push('user_id');
923
- }
924
- if (columnNames.has('user_no')) {
925
- userColumns.push('user_no');
926
- }
927
- if (userColumns.length === 0) {
928
- throw new DinoxError('c_note table is missing both user_id and user_no columns');
929
- }
930
- if (!columnNames.has('type')) {
931
- throw new DinoxError('c_note table is missing type column');
932
- }
933
- const requiredColumns = [
934
- 'id',
935
- 'title',
936
- 'type',
937
- 'content_md',
938
- 'content_json',
939
- 'content_text',
940
- 'tags',
941
- 'zettel_boxes',
942
- 'created_at',
943
- 'updated_at',
944
- 'is_del',
945
- 'version',
946
- ];
947
- const missingRequired = requiredColumns.filter((name) => !columnNames.has(name));
948
- if (missingRequired.length > 0) {
949
- throw new DinoxError(`c_note table is missing required columns: ${missingRequired.join(', ')}`);
950
- }
951
- const fullInsertEntries = [
952
- ['id', input.id],
953
- ['title', input.title],
954
- ['type', input.noteType],
955
- ['user_no', userId],
956
- ['user_id', userId],
957
- ['created_at', now],
958
- ['updated_at', now],
959
- ['is_del', 0],
960
- ['status', 0],
961
- ['version', 1],
962
- ['content', ''],
963
- ['content_json', contentJson],
964
- ['content_text', input.contentText],
965
- ['content_md', input.contentMd],
966
- ['summary', ''],
967
- ['tags', tags],
968
- ['zettel_boxes', zettelBoxes],
969
- ['images', '[]'],
970
- ['files', '[]'],
971
- ['audios', '[]'],
972
- ['audio_detail', '{}'],
973
- ['image_detail', imageDetail],
974
- ['source', ''],
975
- ['resource_id', null],
976
- ['note_ids', '[]'],
977
- ['extra_data', '{}'],
978
- ['properties', null],
979
- ['is_pub', 0],
980
- ['format_type', ''],
981
- ['comments', '[]'],
982
- ['is_top', 0],
983
- ['pinned_at', '1997-01-01T00:00:00.000Z'],
984
- ['starred_at', null],
985
- ['chat_context', '{}'],
986
- ['bid_links', '[]'],
987
- ];
988
- const insertEntries = fullInsertEntries.filter(([column]) => columnNames.has(column));
989
- const insertColumns = insertEntries.map(([column]) => column);
990
- const placeholders = insertEntries.map(() => '?').join(', ');
991
- const params = insertEntries.map(([, value]) => value);
992
- await db.execute(`
993
- INSERT INTO c_note (
994
- ${insertColumns.join(', ')}
995
- ) VALUES (${placeholders})
996
- `, params);
997
- return { id: input.id };
998
- }
999
- function normalizeLookupKey(value) {
1000
- return value.trim().toLowerCase();
1001
- }
1002
- function normalizeZettelBoxLookupKey(value) {
1003
- return canonicalizeBoxPath(value).toLowerCase();
1004
- }
1005
- function buildZettelBoxRetryQuestion(intent, type) {
1006
- const command = intent === 'search'
1007
- ? 'dino note search'
1008
- : intent === 'update'
1009
- ? 'dino note update'
1010
- : intent === 'bulk'
1011
- ? 'dino note bulk'
1012
- : 'dino note create';
1013
- if (type === 'ambiguous') {
1014
- return `Do you want to specify box paths and retry \`${command}\`?`;
1015
- }
1016
- return `Do you want to add these missing boxes and retry \`${command}\`?`;
1017
- }
1018
- export async function validateTagsForCreate(db, requestedTags, userId) {
1019
- return validateTagsForWrite(db, requestedTags, userId, 'dino note create');
1020
- }
1021
- export async function validateTagsForUpdate(db, requestedTags, userId) {
1022
- return validateTagsForWrite(db, requestedTags, userId, 'dino note update');
1023
- }
1024
- export async function validateTagsForBulk(db, requestedTags, userId) {
1025
- return validateTagsForWrite(db, requestedTags, userId, 'dino note bulk');
1026
- }
1027
- async function validateTagsForWrite(db, requestedTags, userId, retryCommand) {
1028
- const normalizedUserId = userId.trim();
1029
- if (!normalizedUserId) {
1030
- throw new DinoxError('Tag validation requires the current userId. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
1031
- }
1032
- if (requestedTags.length === 0) {
1033
- return [];
1034
- }
1035
- const tags = await listTags(db, { userId: normalizedUserId });
1036
- const existingTagMap = new Map();
1037
- for (const tag of tags) {
1038
- const key = normalizeLookupKey(tag);
1039
- if (!key || existingTagMap.has(key)) {
1040
- continue;
1041
- }
1042
- existingTagMap.set(key, tag);
1043
- }
1044
- const resolved = [];
1045
- const seen = new Set();
1046
- const missing = [];
1047
- for (const requested of requestedTags) {
1048
- const key = normalizeLookupKey(requested);
1049
- if (!key || seen.has(key)) {
1050
- continue;
1051
- }
1052
- seen.add(key);
1053
- const matched = existingTagMap.get(key);
1054
- if (!matched) {
1055
- missing.push(requested);
1056
- continue;
1057
- }
1058
- resolved.push(matched);
1059
- }
1060
- if (missing.length > 0) {
1061
- throw new DinoxError(`Unknown tags: ${missing.join(', ')}`, {
1062
- details: {
1063
- type: 'missing_tags',
1064
- missing,
1065
- knownCount: tags.length,
1066
- knownSample: tags.slice(0, 20),
1067
- question: `Do you want to add these missing tags and retry \`${retryCommand}\`?`,
1068
- },
1069
- });
1070
- }
1071
- return resolved;
1072
- }
1073
- export function parseStoredTags(raw) {
1074
- if (!raw) {
1075
- return [];
1076
- }
1077
- try {
1078
- const parsed = JSON.parse(raw);
1079
- if (!Array.isArray(parsed)) {
1080
- return [];
1081
- }
1082
- const result = [];
1083
- const seen = new Set();
1084
- for (const value of parsed) {
1085
- const normalized = String(value).trim();
1086
- const key = normalizeLookupKey(normalized);
1087
- if (!key || seen.has(key)) {
1088
- continue;
1089
- }
1090
- seen.add(key);
1091
- result.push(normalized);
1092
- }
1093
- return result;
1094
- }
1095
- catch {
1096
- return [];
1097
- }
1098
- }
1099
- function normalizeDistinctStrings(values) {
1100
- return Array.from(new Set(values
1101
- .map((value) => value.trim())
1102
- .filter((value) => value.length > 0)));
1103
- }
1104
- function sameTagSet(left, right) {
1105
- if (left.length !== right.length) {
1106
- return false;
1107
- }
1108
- const leftKeys = left.map(normalizeLookupKey).sort();
1109
- const rightKeys = right.map(normalizeLookupKey).sort();
1110
- return leftKeys.every((value, index) => value === rightKeys[index]);
1111
- }
1112
- function sameStringSet(left, right) {
1113
- if (left.length !== right.length) {
1114
- return false;
1115
- }
1116
- const leftSorted = [...left].sort();
1117
- const rightSorted = [...right].sort();
1118
- return leftSorted.every((value, index) => value === rightSorted[index]);
1119
- }
1120
- export async function updateNote(db, input) {
1121
- const id = input.id.trim();
1122
- if (!id) {
1123
- throw new DinoxError('Note id is required');
1124
- }
1125
- const shouldUpdateTags = Array.isArray(input.tags);
1126
- const shouldUpdateBoxes = Array.isArray(input.zettelBoxIds);
1127
- const shouldUpdateStarred = typeof input.starred === 'boolean';
1128
- if (!shouldUpdateTags && !shouldUpdateBoxes && !shouldUpdateStarred) {
1129
- throw new DinoxError('At least one field is required: tags, boxes, or starred');
1130
- }
1131
- const existing = await db.getOptional(`
1132
- SELECT id, tags, zettel_boxes, is_del, starred_at
1133
- FROM c_note
1134
- WHERE id = ?
1135
- `, [id]);
1136
- if (!existing) {
1137
- throw new DinoxError(`Note not found: ${id}`);
1138
- }
1139
- if (typeof existing.is_del === 'number' && existing.is_del !== 0) {
1140
- throw new DinoxError(`Note has been deleted: ${id}`);
1141
- }
1142
- const currentTags = parseStoredTags(existing.tags);
1143
- const currentZettelBoxIds = parseZettelBoxIds(existing.zettel_boxes);
1144
- const currentStarred = existing.starred_at !== null;
1145
- const nextTags = shouldUpdateTags ? normalizeDistinctStrings(input.tags) : currentTags;
1146
- const nextZettelBoxIds = shouldUpdateBoxes ? normalizeDistinctStrings(input.zettelBoxIds) : currentZettelBoxIds;
1147
- const nextStarred = shouldUpdateStarred ? input.starred : currentStarred;
1148
- const tagsChanged = shouldUpdateTags && !sameTagSet(currentTags, nextTags);
1149
- const boxesChanged = shouldUpdateBoxes && !sameStringSet(currentZettelBoxIds, nextZettelBoxIds);
1150
- const starredChanged = shouldUpdateStarred && currentStarred !== nextStarred;
1151
- if (!tagsChanged && !boxesChanged && !starredChanged) {
1152
- return {
1153
- id,
1154
- updated: false,
1155
- changed: [],
1156
- tags: currentTags,
1157
- zettelBoxIds: currentZettelBoxIds,
1158
- is_starred: currentStarred,
1159
- };
1160
- }
1161
- const setParts = [];
1162
- const params = [];
1163
- if (tagsChanged) {
1164
- setParts.push('tags = ?');
1165
- params.push(JSON.stringify(nextTags));
1166
- }
1167
- if (boxesChanged) {
1168
- setParts.push('zettel_boxes = ?');
1169
- params.push(JSON.stringify(nextZettelBoxIds));
1170
- }
1171
- if (starredChanged) {
1172
- setParts.push('starred_at = ?');
1173
- }
1174
- const updatedAt = nowIsoUtc();
1175
- if (starredChanged) {
1176
- params.push(nextStarred ? updatedAt : null);
1177
- }
1178
- setParts.push('updated_at = ?');
1179
- params.push(updatedAt);
1180
- setParts.push('version = COALESCE(version, 0) + 1');
1181
- params.push(id);
1182
- await db.execute(`
1183
- UPDATE c_note
1184
- SET ${setParts.join(',\n ')}
1185
- WHERE id = ?
1186
- `, params);
1187
- return {
1188
- id,
1189
- updated: true,
1190
- changed: [
1191
- ...(tagsChanged ? ['tags'] : []),
1192
- ...(boxesChanged ? ['boxes'] : []),
1193
- ...(starredChanged ? ['is_starred'] : []),
1194
- ],
1195
- tags: nextTags,
1196
- zettelBoxIds: nextZettelBoxIds,
1197
- is_starred: nextStarred,
1198
- };
1199
- }
1200
- export async function assertNotesUpdatable(db, noteIds) {
1201
- const ids = Array.from(new Set(noteIds
1202
- .map((id) => id.trim())
1203
- .filter((id) => id.length > 0)));
1204
- if (ids.length === 0) {
1205
- throw new DinoxError('At least one note id is required');
1206
- }
1207
- const existing = new Map();
1208
- const chunkSize = 500;
1209
- for (let index = 0; index < ids.length; index += chunkSize) {
1210
- const batch = ids.slice(index, index + chunkSize);
1211
- const placeholders = batch.map(() => '?').join(', ');
1212
- const rows = await db.getAll(`
1213
- SELECT id, is_del
1214
- FROM c_note
1215
- WHERE id IN (${placeholders})
1216
- `, batch);
1217
- for (const row of rows) {
1218
- const id = row.id?.trim();
1219
- if (!id) {
1220
- continue;
1221
- }
1222
- existing.set(id, row.is_del ?? null);
1223
- }
1224
- }
1225
- const missing = ids.filter((id) => !existing.has(id));
1226
- if (missing.length > 0) {
1227
- throw new DinoxError(`Notes not found: ${missing.join(', ')}`);
1228
- }
1229
- const deleted = ids.filter((id) => {
1230
- const isDel = existing.get(id);
1231
- return typeof isDel === 'number' && isDel !== 0;
1232
- });
1233
- if (deleted.length > 0) {
1234
- throw new DinoxError(`Notes have been deleted: ${deleted.join(', ')}`);
1235
- }
1236
- }
1237
- export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
1238
- return resolveZettelBoxIdsByName(db, requestedNames, 'create');
1239
- }
1240
- export async function resolveZettelBoxIdsForSearch(db, requestedNames) {
1241
- return resolveZettelBoxIdsByName(db, requestedNames, 'search');
1242
- }
1243
- export async function resolveZettelBoxIdsForUpdate(db, requestedNames) {
1244
- return resolveZettelBoxIdsByName(db, requestedNames, 'update');
1245
- }
1246
- export async function resolveZettelBoxIdsForBulk(db, requestedNames) {
1247
- return resolveZettelBoxIdsByName(db, requestedNames, 'bulk');
1248
- }
1249
- async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
1250
- if (requestedNames.length === 0) {
1251
- return [];
1252
- }
1253
- const boxes = await listBoxes(db);
1254
- const availablePaths = Array.from(new Set(boxes.map((box) => box.path.trim()).filter(Boolean)));
1255
- const boxByPath = new Map();
1256
- const boxByName = new Map();
1257
- for (const box of boxes) {
1258
- const name = box.name.trim();
1259
- const path = canonicalizeBoxPath(box.path || name);
1260
- const pathKey = normalizeZettelBoxLookupKey(path);
1261
- if (pathKey && !boxByPath.has(pathKey)) {
1262
- boxByPath.set(pathKey, box.id);
1263
- }
1264
- const nameKey = normalizeLookupKey(name);
1265
- if (!nameKey) {
1266
- continue;
1267
- }
1268
- const current = boxByName.get(nameKey);
1269
- if (!current) {
1270
- boxByName.set(nameKey, {
1271
- name,
1272
- ids: [box.id],
1273
- paths: path ? [path] : [],
1274
- });
1275
- continue;
1276
- }
1277
- if (!current.ids.includes(box.id)) {
1278
- current.ids.push(box.id);
1279
- }
1280
- if (path && !current.paths.includes(path)) {
1281
- current.paths.push(path);
1282
- }
1283
- }
1284
- const resolvedIds = [];
1285
- const seenIds = new Set();
1286
- const missing = [];
1287
- const ambiguous = [];
1288
- const seenAmbiguous = new Set();
1289
- for (const requested of requestedNames) {
1290
- const pathKey = normalizeZettelBoxLookupKey(requested);
1291
- if (!pathKey) {
1292
- continue;
1293
- }
1294
- const pathMatchId = boxByPath.get(pathKey);
1295
- if (pathMatchId) {
1296
- if (!seenIds.has(pathMatchId)) {
1297
- seenIds.add(pathMatchId);
1298
- resolvedIds.push(pathMatchId);
1299
- }
1300
- continue;
1301
- }
1302
- const lookup = boxByName.get(normalizeLookupKey(requested));
1303
- if (!lookup) {
1304
- missing.push(requested);
1305
- continue;
1306
- }
1307
- if (lookup.ids.length > 1) {
1308
- const marker = `${requested.toLowerCase()}::${lookup.ids.join(',')}`;
1309
- if (!seenAmbiguous.has(marker)) {
1310
- seenAmbiguous.add(marker);
1311
- ambiguous.push({
1312
- name: requested,
1313
- ids: lookup.ids,
1314
- paths: lookup.paths,
1315
- });
1316
- }
1317
- continue;
1318
- }
1319
- const id = lookup.ids[0];
1320
- if (!seenIds.has(id)) {
1321
- seenIds.add(id);
1322
- resolvedIds.push(id);
1323
- }
1324
- }
1325
- if (ambiguous.length > 0) {
1326
- throw new DinoxError(`Ambiguous zettel box names: ${ambiguous.map((item) => `${item.name} -> [${item.paths.join(', ')}]`).join('; ')}`, {
1327
- details: {
1328
- type: 'ambiguous_zettel_boxes',
1329
- ambiguous,
1330
- availablePathsSample: availablePaths.slice(0, 20),
1331
- question: buildZettelBoxRetryQuestion(intent, 'ambiguous'),
1332
- },
1333
- });
1334
- }
1335
- if (missing.length > 0) {
1336
- throw new DinoxError(`Unknown zettel box paths or names: ${missing.join(', ')}`, {
1337
- details: {
1338
- type: 'missing_zettel_boxes',
1339
- missing,
1340
- availableCount: availablePaths.length,
1341
- availablePathsSample: availablePaths.slice(0, 20),
1342
- question: buildZettelBoxRetryQuestion(intent, 'missing'),
1343
- },
1344
- });
1345
- }
1346
- return resolvedIds;
1347
- }
1348
- export async function softDeleteNote(db, id) {
1349
- const existing = await db.getOptional('SELECT id FROM c_note WHERE id = ?', [id]);
1350
- if (!existing) {
1351
- throw new DinoxError(`Note not found: ${id}`);
1352
- }
1353
- await db.execute(`
1354
- UPDATE c_note
1355
- SET is_del = 1,
1356
- updated_at = ?,
1357
- version = COALESCE(version, 0) + 1
1358
- WHERE id = ?
1359
- `, [nowIsoUtc(), id]);
1360
- }
1
+ // Public note repository API. Implementation lives in ./repo/*; this module re-exports
2
+ // only the original public surface so callers do not depend on the internal split.
3
+ export { parseStoredTags, parseZettelBoxIds, } from './repo/shared.js';
4
+ export { executeNoteSearch, searchNotes, searchNotesPage, } from './repo/search.js';
5
+ export { getNote, getNoteContext, getNoteDetail, getNoteDetails, getNoteExportItems, getNotePreview, } from './repo/read.js';
6
+ export { assertNotesUpdatable, createNote, softDeleteNote, updateNote, } from './repo/write.js';
7
+ export { resolveZettelBoxIdsForBulk, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, validateTagsForBulk, validateTagsForCreate, validateTagsForUpdate, } from './repo/resolve.js';