@dinoxx/dinox-cli 1.0.0

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 (90) hide show
  1. package/README.md +294 -0
  2. package/dist/auth/userInfo.d.ts +14 -0
  3. package/dist/auth/userInfo.js +115 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +32 -0
  6. package/dist/cliTypes.d.ts +6 -0
  7. package/dist/cliTypes.js +1 -0
  8. package/dist/commands/auth/index.d.ts +2 -0
  9. package/dist/commands/auth/index.js +193 -0
  10. package/dist/commands/boxes/index.d.ts +2 -0
  11. package/dist/commands/boxes/index.js +107 -0
  12. package/dist/commands/boxes/repo.d.ts +21 -0
  13. package/dist/commands/boxes/repo.js +154 -0
  14. package/dist/commands/config/index.d.ts +2 -0
  15. package/dist/commands/config/index.js +67 -0
  16. package/dist/commands/info/index.d.ts +2 -0
  17. package/dist/commands/info/index.js +20 -0
  18. package/dist/commands/notes/index.d.ts +2 -0
  19. package/dist/commands/notes/index.js +271 -0
  20. package/dist/commands/notes/repo.d.ts +70 -0
  21. package/dist/commands/notes/repo.js +674 -0
  22. package/dist/commands/notes/searchTime.d.ts +9 -0
  23. package/dist/commands/notes/searchTime.js +85 -0
  24. package/dist/commands/prompt/index.d.ts +2 -0
  25. package/dist/commands/prompt/index.js +51 -0
  26. package/dist/commands/prompt/repo.d.ts +6 -0
  27. package/dist/commands/prompt/repo.js +18 -0
  28. package/dist/commands/sync.d.ts +2 -0
  29. package/dist/commands/sync.js +68 -0
  30. package/dist/commands/tags/index.d.ts +2 -0
  31. package/dist/commands/tags/index.js +120 -0
  32. package/dist/commands/tags/repo.d.ts +14 -0
  33. package/dist/commands/tags/repo.js +247 -0
  34. package/dist/config/keys.d.ts +9 -0
  35. package/dist/config/keys.js +17 -0
  36. package/dist/config/paths.d.ts +4 -0
  37. package/dist/config/paths.js +39 -0
  38. package/dist/config/resolve.d.ts +2 -0
  39. package/dist/config/resolve.js +56 -0
  40. package/dist/config/serviceEndpoints.d.ts +3 -0
  41. package/dist/config/serviceEndpoints.js +3 -0
  42. package/dist/config/store.d.ts +5 -0
  43. package/dist/config/store.js +87 -0
  44. package/dist/config/types.d.ts +51 -0
  45. package/dist/config/types.js +1 -0
  46. package/dist/dinox.d.ts +2 -0
  47. package/dist/dinox.js +50 -0
  48. package/dist/powersync/connector.d.ts +21 -0
  49. package/dist/powersync/connector.js +58 -0
  50. package/dist/powersync/runtime.d.ts +37 -0
  51. package/dist/powersync/runtime.js +107 -0
  52. package/dist/powersync/schema/content.d.ts +76 -0
  53. package/dist/powersync/schema/content.js +76 -0
  54. package/dist/powersync/schema/index.d.ts +371 -0
  55. package/dist/powersync/schema/index.js +35 -0
  56. package/dist/powersync/schema/local.d.ts +68 -0
  57. package/dist/powersync/schema/local.js +83 -0
  58. package/dist/powersync/schema/note.d.ts +34 -0
  59. package/dist/powersync/schema/note.js +34 -0
  60. package/dist/powersync/schema/notesExtras.d.ts +62 -0
  61. package/dist/powersync/schema/notesExtras.js +71 -0
  62. package/dist/powersync/schema/projects.d.ts +101 -0
  63. package/dist/powersync/schema/projects.js +101 -0
  64. package/dist/powersync/schema/tags.d.ts +37 -0
  65. package/dist/powersync/schema/tags.js +37 -0
  66. package/dist/powersync/tokenIndex.d.ts +17 -0
  67. package/dist/powersync/tokenIndex.js +202 -0
  68. package/dist/powersync/uploader.d.ts +7 -0
  69. package/dist/powersync/uploader.js +134 -0
  70. package/dist/utils/argValue.d.ts +1 -0
  71. package/dist/utils/argValue.js +17 -0
  72. package/dist/utils/errors.d.ts +10 -0
  73. package/dist/utils/errors.js +17 -0
  74. package/dist/utils/id.d.ts +1 -0
  75. package/dist/utils/id.js +4 -0
  76. package/dist/utils/output.d.ts +2 -0
  77. package/dist/utils/output.js +10 -0
  78. package/dist/utils/redact.d.ts +1 -0
  79. package/dist/utils/redact.js +10 -0
  80. package/dist/utils/text.d.ts +1 -0
  81. package/dist/utils/text.js +35 -0
  82. package/dist/utils/time.d.ts +1 -0
  83. package/dist/utils/time.js +3 -0
  84. package/dist/utils/tiptapMarkdown.d.ts +6 -0
  85. package/dist/utils/tiptapMarkdown.js +149 -0
  86. package/dist/utils/tokenize.d.ts +1 -0
  87. package/dist/utils/tokenize.js +56 -0
  88. package/dist/utils/version.d.ts +1 -0
  89. package/dist/utils/version.js +21 -0
  90. package/package.json +63 -0
@@ -0,0 +1,674 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { nowIsoUtc } from '../../utils/time.js';
3
+ import { tokenizeWithJieba } from '../../utils/tokenize.js';
4
+ import { listBoxes } from '../boxes/repo.js';
5
+ import { listTags } from '../tags/repo.js';
6
+ export async function searchNotes(db, query, options) {
7
+ const normalizedQuery = query.trim();
8
+ const tagFilter = buildTagFilter(options.tagsExpression, 'n');
9
+ const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
10
+ let rows = [];
11
+ if (!options.includeDeleted) {
12
+ const ftsMatch = buildFtsMatchQuery(normalizedQuery);
13
+ if (ftsMatch) {
14
+ try {
15
+ rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter);
16
+ return enrichSearchRows(db, rows);
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ const normalized = message.toLowerCase();
21
+ // FTS table may not be initialized yet, or query syntax may fail.
22
+ if (normalized.includes('no such table') ||
23
+ normalized.includes('fts5') ||
24
+ normalized.includes('malformed match') ||
25
+ normalized.includes('syntax error')) {
26
+ // Fallback to legacy LIKE search below.
27
+ }
28
+ else {
29
+ throw error;
30
+ }
31
+ }
32
+ }
33
+ }
34
+ const whereClauses = [
35
+ options.includeDeleted ? '1 = 1' : '(n.is_del IS NULL OR n.is_del = 0)',
36
+ ];
37
+ const params = [];
38
+ if (normalizedQuery.length > 0) {
39
+ const like = `%${normalizedQuery}%`;
40
+ whereClauses.push(`
41
+ (
42
+ (n.title IS NOT NULL AND n.title LIKE ?)
43
+ OR (n.content_text IS NOT NULL AND n.content_text LIKE ?)
44
+ )
45
+ `);
46
+ params.push(like, like);
47
+ }
48
+ if (tagFilter) {
49
+ whereClauses.push(tagFilter.sql);
50
+ params.push(...tagFilter.params);
51
+ }
52
+ if (createdAtFilter) {
53
+ whereClauses.push(createdAtFilter.sql);
54
+ params.push(...createdAtFilter.params);
55
+ }
56
+ rows = await db.getAll(`
57
+ SELECT
58
+ n.id,
59
+ n.title,
60
+ n.summary,
61
+ n.content_text,
62
+ n.zettel_boxes,
63
+ n.updated_at,
64
+ n.is_del,
65
+ n.version
66
+ FROM c_note n
67
+ WHERE ${whereClauses.join('\n AND ')}
68
+ ORDER BY COALESCE(n.updated_at, n.created_at) DESC
69
+ `, params);
70
+ return enrichSearchRows(db, rows);
71
+ }
72
+ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter) {
73
+ const whereClauses = ['(n.is_del IS NULL OR n.is_del = 0)', 'note_local_fts MATCH ?'];
74
+ const params = [matchQuery];
75
+ if (tagFilter) {
76
+ whereClauses.push(tagFilter.sql);
77
+ params.push(...tagFilter.params);
78
+ }
79
+ if (createdAtFilter) {
80
+ whereClauses.push(createdAtFilter.sql);
81
+ params.push(...createdAtFilter.params);
82
+ }
83
+ return db.getAll(`
84
+ SELECT
85
+ n.id,
86
+ n.title,
87
+ n.summary,
88
+ n.content_text,
89
+ n.zettel_boxes,
90
+ n.updated_at,
91
+ n.is_del,
92
+ n.version
93
+ FROM note_local_fts f
94
+ JOIN c_note n ON n.id = f.note_id
95
+ WHERE ${whereClauses.join('\n AND ')}
96
+ ORDER BY bm25(note_local_fts, 8.0, 4.0, 1.0) ASC, COALESCE(n.updated_at, n.created_at) DESC
97
+ `, params);
98
+ }
99
+ async function enrichSearchRows(db, rows) {
100
+ const allBoxIds = Array.from(new Set(rows.flatMap((row) => parseZettelBoxIds(row.zettel_boxes))));
101
+ const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
102
+ return rows.map((row) => {
103
+ const summary = resolveSummary(row.summary, row.content_text);
104
+ const zettelBoxes = parseZettelBoxIds(row.zettel_boxes)
105
+ .map((id) => boxNameMap.get(id))
106
+ .filter((name) => typeof name === 'string' && name.length > 0);
107
+ return {
108
+ id: row.id,
109
+ title: row.title?.trim() ?? '',
110
+ summary,
111
+ updated_at: row.updated_at,
112
+ is_del: row.is_del,
113
+ version: row.version,
114
+ zettel_boxes: zettelBoxes,
115
+ };
116
+ });
117
+ }
118
+ function resolveSummary(summary, contentText) {
119
+ const summaryValue = summary?.trim() ?? '';
120
+ if (summaryValue.length > 0) {
121
+ return summaryValue;
122
+ }
123
+ return contentText?.trim() ?? '';
124
+ }
125
+ function parseZettelBoxIds(raw) {
126
+ const value = raw?.trim();
127
+ if (!value) {
128
+ return [];
129
+ }
130
+ try {
131
+ const parsed = JSON.parse(value);
132
+ if (!Array.isArray(parsed)) {
133
+ return [];
134
+ }
135
+ return Array.from(new Set(parsed
136
+ .map((entry) => String(entry).trim())
137
+ .filter((entry) => entry.length > 0)));
138
+ }
139
+ catch {
140
+ return [];
141
+ }
142
+ }
143
+ async function loadZettelBoxNameMap(db, ids) {
144
+ const map = new Map();
145
+ if (ids.length === 0) {
146
+ return map;
147
+ }
148
+ const chunkSize = 500;
149
+ for (let i = 0; i < ids.length; i += chunkSize) {
150
+ const batch = ids.slice(i, i + chunkSize);
151
+ const placeholders = batch.map(() => '?').join(', ');
152
+ const rows = await db.getAll(`
153
+ SELECT id, name
154
+ FROM c_zettel_box
155
+ WHERE (is_del IS NULL OR is_del = 0)
156
+ AND id IN (${placeholders})
157
+ `, batch);
158
+ for (const row of rows) {
159
+ const id = row.id?.trim() ?? '';
160
+ if (!id) {
161
+ continue;
162
+ }
163
+ map.set(id, row.name?.trim() ?? '');
164
+ }
165
+ }
166
+ return map;
167
+ }
168
+ function buildFtsMatchQuery(query) {
169
+ const tokens = tokenizeWithJieba(query, 32)
170
+ .map((token) => token.trim())
171
+ .filter((token) => token.length > 0);
172
+ if (tokens.length === 0) {
173
+ return null;
174
+ }
175
+ const unique = Array.from(new Set(tokens));
176
+ return unique.map((token) => `"${escapeFtsToken(token)}"`).join(' AND ');
177
+ }
178
+ function escapeFtsToken(token) {
179
+ return token.replace(/"/g, '""');
180
+ }
181
+ function buildTagFilter(tagsExpression, noteAlias) {
182
+ const expression = tagsExpression?.trim() ?? '';
183
+ if (!expression) {
184
+ return null;
185
+ }
186
+ let ast;
187
+ try {
188
+ ast = parseTagExpression(expression);
189
+ }
190
+ catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ throw new DinoxError(`Invalid --tags expression: ${message}`);
193
+ }
194
+ const params = [];
195
+ const sql = compileTagExpression(ast, noteAlias, params);
196
+ return { sql: `(${sql})`, params };
197
+ }
198
+ function buildCreatedAtFilter(createdAtFrom, createdAtTo, noteAlias) {
199
+ const clauses = [];
200
+ const params = [];
201
+ if (createdAtFrom) {
202
+ clauses.push(`(${noteAlias}.created_at IS NOT NULL AND ${noteAlias}.created_at >= ?)`);
203
+ params.push(createdAtFrom);
204
+ }
205
+ if (createdAtTo) {
206
+ clauses.push(`(${noteAlias}.created_at IS NOT NULL AND ${noteAlias}.created_at <= ?)`);
207
+ params.push(createdAtTo);
208
+ }
209
+ if (clauses.length === 0) {
210
+ return null;
211
+ }
212
+ return {
213
+ sql: `(${clauses.join(' AND ')})`,
214
+ params,
215
+ };
216
+ }
217
+ function parseTagExpression(expression) {
218
+ const tokens = tokenizeTagExpression(expression);
219
+ if (tokens.length === 0) {
220
+ throw new Error('Expression is empty');
221
+ }
222
+ let index = 0;
223
+ const peek = () => tokens[index] ?? null;
224
+ const consume = (expected) => {
225
+ const token = peek();
226
+ if (!token) {
227
+ throw new Error('Unexpected end of expression');
228
+ }
229
+ if (expected && token.type !== expected) {
230
+ throw new Error(`Expected ${expected} at position ${token.position + 1}`);
231
+ }
232
+ index += 1;
233
+ return token;
234
+ };
235
+ const parsePrimary = () => {
236
+ const token = peek();
237
+ if (!token) {
238
+ throw new Error('Unexpected end of expression');
239
+ }
240
+ if (token.type === 'TERM') {
241
+ consume('TERM');
242
+ const value = normalizeTagValue(token.value);
243
+ if (!value) {
244
+ throw new Error(`Empty tag at position ${token.position + 1}`);
245
+ }
246
+ return { kind: 'tag', value };
247
+ }
248
+ if (token.type === 'LPAREN') {
249
+ consume('LPAREN');
250
+ const node = parseOr();
251
+ const close = peek();
252
+ if (!close || close.type !== 'RPAREN') {
253
+ throw new Error(`Missing ')' for '(' at position ${token.position + 1}`);
254
+ }
255
+ consume('RPAREN');
256
+ return node;
257
+ }
258
+ throw new Error(`Unexpected token '${token.value}' at position ${token.position + 1}`);
259
+ };
260
+ const parseUnary = () => {
261
+ const token = peek();
262
+ if (token?.type === 'NOT') {
263
+ consume('NOT');
264
+ return { kind: 'not', child: parseUnary() };
265
+ }
266
+ return parsePrimary();
267
+ };
268
+ const parseAnd = () => {
269
+ let node = parseUnary();
270
+ while (peek()?.type === 'AND') {
271
+ consume('AND');
272
+ node = { kind: 'and', left: node, right: parseUnary() };
273
+ }
274
+ return node;
275
+ };
276
+ const parseOr = () => {
277
+ let node = parseAnd();
278
+ while (peek()?.type === 'OR') {
279
+ consume('OR');
280
+ node = { kind: 'or', left: node, right: parseAnd() };
281
+ }
282
+ return node;
283
+ };
284
+ const ast = parseOr();
285
+ const extra = peek();
286
+ if (extra) {
287
+ throw new Error(`Unexpected token '${extra.value}' at position ${extra.position + 1}`);
288
+ }
289
+ return ast;
290
+ }
291
+ function tokenizeTagExpression(expression) {
292
+ const tokens = [];
293
+ let i = 0;
294
+ while (i < expression.length) {
295
+ const ch = expression[i];
296
+ if (/\s/.test(ch)) {
297
+ i += 1;
298
+ continue;
299
+ }
300
+ if (ch === '(') {
301
+ tokens.push({ type: 'LPAREN', value: ch, position: i });
302
+ i += 1;
303
+ continue;
304
+ }
305
+ if (ch === ')') {
306
+ tokens.push({ type: 'RPAREN', value: ch, position: i });
307
+ i += 1;
308
+ continue;
309
+ }
310
+ if (ch === '"' || ch === "'") {
311
+ const quoted = readQuotedToken(expression, i);
312
+ tokens.push({ type: 'TERM', value: quoted.value, position: i });
313
+ i = quoted.next;
314
+ continue;
315
+ }
316
+ let j = i;
317
+ while (j < expression.length) {
318
+ const next = expression[j];
319
+ if (/\s/.test(next) || next === '(' || next === ')') {
320
+ break;
321
+ }
322
+ j += 1;
323
+ }
324
+ const raw = expression.slice(i, j);
325
+ const upper = raw.toUpperCase();
326
+ if (upper === 'AND' || upper === 'OR' || upper === 'NOT') {
327
+ tokens.push({ type: upper, value: raw, position: i });
328
+ }
329
+ else {
330
+ tokens.push({ type: 'TERM', value: raw, position: i });
331
+ }
332
+ i = j;
333
+ }
334
+ return tokens;
335
+ }
336
+ function readQuotedToken(expression, start) {
337
+ const quote = expression[start];
338
+ let i = start + 1;
339
+ let value = '';
340
+ while (i < expression.length) {
341
+ const ch = expression[i];
342
+ if (ch === '\\') {
343
+ if (i + 1 >= expression.length) {
344
+ throw new Error(`Invalid escape at position ${i + 1}`);
345
+ }
346
+ value += expression[i + 1];
347
+ i += 2;
348
+ continue;
349
+ }
350
+ if (ch === quote) {
351
+ return { value, next: i + 1 };
352
+ }
353
+ value += ch;
354
+ i += 1;
355
+ }
356
+ throw new Error(`Unclosed quote at position ${start + 1}`);
357
+ }
358
+ function compileTagExpression(node, noteAlias, params) {
359
+ if (node.kind === 'tag') {
360
+ params.push(node.value);
361
+ return `
362
+ EXISTS (
363
+ SELECT 1
364
+ FROM json_each(
365
+ CASE
366
+ WHEN json_valid(COALESCE(${noteAlias}.tags, '')) THEN ${noteAlias}.tags
367
+ ELSE '[]'
368
+ END
369
+ ) tag_item
370
+ WHERE LOWER(TRIM(CAST(tag_item.value AS TEXT))) = ?
371
+ )
372
+ `;
373
+ }
374
+ if (node.kind === 'not') {
375
+ return `NOT (${compileTagExpression(node.child, noteAlias, params)})`;
376
+ }
377
+ if (node.kind === 'and') {
378
+ return `(${compileTagExpression(node.left, noteAlias, params)}) AND (${compileTagExpression(node.right, noteAlias, params)})`;
379
+ }
380
+ return `(${compileTagExpression(node.left, noteAlias, params)}) OR (${compileTagExpression(node.right, noteAlias, params)})`;
381
+ }
382
+ function normalizeTagValue(value) {
383
+ return value.trim().toLowerCase();
384
+ }
385
+ export async function getNote(db, id) {
386
+ return db.getOptional(`
387
+ SELECT
388
+ id,
389
+ title,
390
+ content_md,
391
+ content_json,
392
+ content_text,
393
+ created_at,
394
+ updated_at,
395
+ is_del,
396
+ version
397
+ FROM c_note
398
+ WHERE id = ?
399
+ `, [id]);
400
+ }
401
+ export async function getNoteDetail(db, id) {
402
+ const note = await db.getOptional(`
403
+ SELECT
404
+ id,
405
+ content,
406
+ audios,
407
+ images,
408
+ created_at,
409
+ updated_at,
410
+ title,
411
+ type,
412
+ source,
413
+ resource_id,
414
+ tags,
415
+ is_del,
416
+ content_md,
417
+ format_type,
418
+ zettel_boxes AS zettel_boxes_raw
419
+ FROM c_note
420
+ WHERE id = ?
421
+ `, [id]);
422
+ if (!note) {
423
+ return null;
424
+ }
425
+ const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
426
+ const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
427
+ const zettelBoxes = boxIds
428
+ .map((boxId) => boxNameMap.get(boxId))
429
+ .filter((name) => typeof name === 'string' && name.length > 0);
430
+ return {
431
+ id: note.id,
432
+ content: note.content,
433
+ audios: note.audios,
434
+ images: note.images,
435
+ created_at: note.created_at,
436
+ updated_at: note.updated_at,
437
+ title: note.title,
438
+ type: note.type,
439
+ source: note.source,
440
+ resource_id: note.resource_id,
441
+ tags: note.tags,
442
+ is_del: note.is_del,
443
+ content_md: note.content_md,
444
+ format_type: note.format_type,
445
+ zettel_boxes: zettelBoxes,
446
+ };
447
+ }
448
+ export async function createNote(db, input) {
449
+ const userId = input.userId.trim();
450
+ if (!userId) {
451
+ throw new DinoxError('userId is required to create a note');
452
+ }
453
+ const now = nowIsoUtc();
454
+ const contentJson = JSON.stringify(input.contentJson);
455
+ const tags = JSON.stringify(input.tags);
456
+ const zettelBoxes = JSON.stringify(input.zettelBoxIds);
457
+ const columns = await db.getAll('PRAGMA table_info(c_note)', []);
458
+ const columnNames = new Set(columns
459
+ .map((row) => row.name?.trim().toLowerCase() ?? '')
460
+ .filter((name) => name.length > 0));
461
+ const userColumns = [];
462
+ if (columnNames.has('user_id')) {
463
+ userColumns.push('user_id');
464
+ }
465
+ if (columnNames.has('user_no')) {
466
+ userColumns.push('user_no');
467
+ }
468
+ if (userColumns.length === 0) {
469
+ throw new DinoxError('c_note table is missing both user_id and user_no columns');
470
+ }
471
+ if (!columnNames.has('type')) {
472
+ throw new DinoxError('c_note table is missing type column');
473
+ }
474
+ const requiredColumns = [
475
+ 'id',
476
+ 'title',
477
+ 'type',
478
+ 'content_md',
479
+ 'content_json',
480
+ 'content_text',
481
+ 'tags',
482
+ 'zettel_boxes',
483
+ 'created_at',
484
+ 'updated_at',
485
+ 'is_del',
486
+ 'version',
487
+ ];
488
+ const missingRequired = requiredColumns.filter((name) => !columnNames.has(name));
489
+ if (missingRequired.length > 0) {
490
+ throw new DinoxError(`c_note table is missing required columns: ${missingRequired.join(', ')}`);
491
+ }
492
+ const fullInsertEntries = [
493
+ ['id', input.id],
494
+ ['title', input.title],
495
+ ['type', input.noteType],
496
+ ['user_no', userId],
497
+ ['user_id', userId],
498
+ ['created_at', now],
499
+ ['updated_at', now],
500
+ ['is_del', 0],
501
+ ['status', 0],
502
+ ['version', 1],
503
+ ['content', ''],
504
+ ['content_json', contentJson],
505
+ ['content_text', input.contentText],
506
+ ['content_md', input.contentMd],
507
+ ['summary', ''],
508
+ ['tags', tags],
509
+ ['zettel_boxes', zettelBoxes],
510
+ ['images', '[]'],
511
+ ['files', '[]'],
512
+ ['audios', '[]'],
513
+ ['audio_detail', '{}'],
514
+ ['image_detail', '{}'],
515
+ ['source', ''],
516
+ ['resource_id', null],
517
+ ['note_ids', '[]'],
518
+ ['extra_data', '{}'],
519
+ ['format_type', ''],
520
+ ['comments', '[]'],
521
+ ['is_top', 0],
522
+ ['pinned_at', '1997-01-01T00:00:00.000Z'],
523
+ ['chat_context', '{}'],
524
+ ['bid_links', '[]'],
525
+ ];
526
+ const insertEntries = fullInsertEntries.filter(([column]) => columnNames.has(column));
527
+ const insertColumns = insertEntries.map(([column]) => column);
528
+ const placeholders = insertEntries.map(() => '?').join(', ');
529
+ const params = insertEntries.map(([, value]) => value);
530
+ await db.execute(`
531
+ INSERT INTO c_note (
532
+ ${insertColumns.join(', ')}
533
+ ) VALUES (${placeholders})
534
+ `, params);
535
+ return { id: input.id };
536
+ }
537
+ function normalizeLookupKey(value) {
538
+ return value.trim().toLowerCase();
539
+ }
540
+ export async function validateTagsForCreate(db, requestedTags) {
541
+ if (requestedTags.length === 0) {
542
+ return [];
543
+ }
544
+ const tags = await listTags(db);
545
+ const existingTagMap = new Map();
546
+ for (const tag of tags) {
547
+ const key = normalizeLookupKey(tag);
548
+ if (!key || existingTagMap.has(key)) {
549
+ continue;
550
+ }
551
+ existingTagMap.set(key, tag);
552
+ }
553
+ const resolved = [];
554
+ const seen = new Set();
555
+ const missing = [];
556
+ for (const requested of requestedTags) {
557
+ const key = normalizeLookupKey(requested);
558
+ if (!key || seen.has(key)) {
559
+ continue;
560
+ }
561
+ seen.add(key);
562
+ const matched = existingTagMap.get(key);
563
+ if (!matched) {
564
+ missing.push(requested);
565
+ continue;
566
+ }
567
+ resolved.push(matched);
568
+ }
569
+ if (missing.length > 0) {
570
+ throw new DinoxError(`Unknown tags: ${missing.join(', ')}`, {
571
+ details: {
572
+ type: 'missing_tags',
573
+ missing,
574
+ knownCount: tags.length,
575
+ knownSample: tags.slice(0, 20),
576
+ question: 'Do you want to add these missing tags and retry `dino note create`?',
577
+ },
578
+ });
579
+ }
580
+ return resolved;
581
+ }
582
+ export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
583
+ if (requestedNames.length === 0) {
584
+ return [];
585
+ }
586
+ const boxes = await listBoxes(db);
587
+ const availableNames = Array.from(new Set(boxes.map((box) => box.name.trim()).filter(Boolean)));
588
+ const boxByName = new Map();
589
+ for (const box of boxes) {
590
+ const name = box.name.trim();
591
+ const key = normalizeLookupKey(name);
592
+ if (!key) {
593
+ continue;
594
+ }
595
+ const current = boxByName.get(key);
596
+ if (!current) {
597
+ boxByName.set(key, {
598
+ name,
599
+ ids: [box.id],
600
+ });
601
+ continue;
602
+ }
603
+ if (!current.ids.includes(box.id)) {
604
+ current.ids.push(box.id);
605
+ }
606
+ }
607
+ const resolvedIds = [];
608
+ const seenIds = new Set();
609
+ const missing = [];
610
+ const ambiguous = [];
611
+ const seenAmbiguous = new Set();
612
+ for (const requested of requestedNames) {
613
+ const key = normalizeLookupKey(requested);
614
+ if (!key) {
615
+ continue;
616
+ }
617
+ const lookup = boxByName.get(key);
618
+ if (!lookup) {
619
+ missing.push(requested);
620
+ continue;
621
+ }
622
+ if (lookup.ids.length > 1) {
623
+ const marker = `${requested.toLowerCase()}::${lookup.ids.join(',')}`;
624
+ if (!seenAmbiguous.has(marker)) {
625
+ seenAmbiguous.add(marker);
626
+ ambiguous.push({
627
+ name: requested,
628
+ ids: lookup.ids,
629
+ });
630
+ }
631
+ continue;
632
+ }
633
+ const id = lookup.ids[0];
634
+ if (!seenIds.has(id)) {
635
+ seenIds.add(id);
636
+ resolvedIds.push(id);
637
+ }
638
+ }
639
+ if (ambiguous.length > 0) {
640
+ throw new DinoxError(`Ambiguous zettel box names: ${ambiguous.map((item) => `${item.name} -> [${item.ids.join(', ')}]`).join('; ')}`, {
641
+ details: {
642
+ type: 'ambiguous_zettel_boxes',
643
+ ambiguous,
644
+ availableNamesSample: availableNames.slice(0, 20),
645
+ question: 'Do you want to specify unique box names (or IDs) and retry `dino note create`?',
646
+ },
647
+ });
648
+ }
649
+ if (missing.length > 0) {
650
+ throw new DinoxError(`Unknown zettel box names: ${missing.join(', ')}`, {
651
+ details: {
652
+ type: 'missing_zettel_boxes',
653
+ missing,
654
+ availableCount: availableNames.length,
655
+ availableNamesSample: availableNames.slice(0, 20),
656
+ question: 'Do you want to add these missing boxes and retry `dino note create`?',
657
+ },
658
+ });
659
+ }
660
+ return resolvedIds;
661
+ }
662
+ export async function softDeleteNote(db, id) {
663
+ const existing = await db.getOptional('SELECT id FROM c_note WHERE id = ?', [id]);
664
+ if (!existing) {
665
+ throw new DinoxError(`Note not found: ${id}`);
666
+ }
667
+ await db.execute(`
668
+ UPDATE c_note
669
+ SET is_del = 1,
670
+ updated_at = ?,
671
+ version = COALESCE(version, 0) + 1
672
+ WHERE id = ?
673
+ `, [nowIsoUtc(), id]);
674
+ }
@@ -0,0 +1,9 @@
1
+ export type ParsedCreatedAtRange = {
2
+ createdAtFrom?: string;
3
+ createdAtTo?: string;
4
+ };
5
+ export declare function parseCreatedAtRange(input: {
6
+ from?: unknown;
7
+ to?: unknown;
8
+ days?: unknown;
9
+ }, now?: Date): ParsedCreatedAtRange;