@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.
- package/dist/commands/notes/repo/read.d.ts +8 -0
- package/dist/commands/notes/repo/read.js +321 -0
- package/dist/commands/notes/repo/resolve.d.ts +8 -0
- package/dist/commands/notes/repo/resolve.js +187 -0
- package/dist/commands/notes/repo/search.d.ts +42 -0
- package/dist/commands/notes/repo/search.js +513 -0
- package/dist/commands/notes/repo/shared.d.ts +190 -0
- package/dist/commands/notes/repo/shared.js +102 -0
- package/dist/commands/notes/repo/write.d.ts +24 -0
- package/dist/commands/notes/repo/write.js +251 -0
- package/dist/commands/notes/repo.d.ts +7 -186
- package/dist/commands/notes/repo.js +7 -1360
- package/dist/commands/schema/commandMetadata.js +4 -4
- package/dist/commands/schema/index.d.ts +1 -0
- package/dist/commands/schema/index.js +23 -2
- package/dist/commands/schema/renderSkills.d.ts +4 -0
- package/dist/commands/schema/renderSkills.js +41 -2
- package/dist/commands/skills/index.d.ts +26 -0
- package/dist/commands/skills/index.js +115 -11
- package/dist/commands/storage/repo/configs.d.ts +30 -0
- package/dist/commands/storage/repo/configs.js +259 -0
- package/dist/commands/storage/repo/operations.d.ts +25 -0
- package/dist/commands/storage/repo/operations.js +337 -0
- package/dist/commands/storage/repo/remote.d.ts +32 -0
- package/dist/commands/storage/repo/remote.js +132 -0
- package/dist/commands/storage/repo/resources.d.ts +12 -0
- package/dist/commands/storage/repo/resources.js +134 -0
- package/dist/commands/storage/repo/shared.d.ts +181 -0
- package/dist/commands/storage/repo/shared.js +36 -0
- package/dist/commands/storage/repo/targets.d.ts +12 -0
- package/dist/commands/storage/repo/targets.js +178 -0
- package/dist/commands/storage/repo.d.ts +3 -166
- package/dist/commands/storage/repo.js +4 -1034
- package/dist/contract/snapshot.d.ts +30 -0
- package/dist/contract/snapshot.js +114 -0
- package/dist/daemon/client.js +2 -2
- package/dist/powersync/uploader.js +27 -2
- package/dist/skills/commandRefs.d.ts +21 -0
- package/dist/skills/commandRefs.js +132 -0
- package/dist/skills/generate.d.ts +13 -0
- package/dist/skills/generate.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +58 -0
- package/dist/utils/errorCodes.d.ts +311 -0
- package/dist/utils/errorCodes.js +92 -0
- package/dist/utils/errors.d.ts +13 -2
- package/dist/utils/errors.js +11 -0
- package/package.json +11 -4
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type NoteContext, type NoteDetail, type NoteExportItem, type NotePreview, type NoteRow } from './shared.js';
|
|
3
|
+
export declare function getNote(db: AbstractPowerSyncDatabase, id: string): Promise<NoteRow | null>;
|
|
4
|
+
export declare function getNoteContext(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContext | null>;
|
|
5
|
+
export declare function getNotePreview(db: AbstractPowerSyncDatabase, id: string, requestedLines: number): Promise<NotePreview | null>;
|
|
6
|
+
export declare function getNoteDetails(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteDetail[]>;
|
|
7
|
+
export declare function getNoteDetail(db: AbstractPowerSyncDatabase, id: string): Promise<NoteDetail | null>;
|
|
8
|
+
export declare function getNoteExportItems(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteExportItem[]>;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Note reads: single-note lookup, context, preview, detail, and export rows.
|
|
2
|
+
import { DinoxError } from '../../../utils/errors.js';
|
|
3
|
+
import { loadZettelBoxNameMap, normalizeStoredFlag, normalizeStoredStarred, parseStoredTags, parseZettelBoxIds, resolveSummary, } from './shared.js';
|
|
4
|
+
export async function getNote(db, id) {
|
|
5
|
+
return db.getOptional(`
|
|
6
|
+
SELECT
|
|
7
|
+
id,
|
|
8
|
+
title,
|
|
9
|
+
content_md,
|
|
10
|
+
content_json,
|
|
11
|
+
content_text,
|
|
12
|
+
created_at,
|
|
13
|
+
updated_at,
|
|
14
|
+
is_del,
|
|
15
|
+
starred_at,
|
|
16
|
+
CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
|
|
17
|
+
version
|
|
18
|
+
FROM c_note
|
|
19
|
+
WHERE id = ?
|
|
20
|
+
`, [id]);
|
|
21
|
+
}
|
|
22
|
+
async function getLinkCount(db, direction, noteId) {
|
|
23
|
+
const [column, boundColumn] = direction === 'in'
|
|
24
|
+
? ['src_note_id', 'dst_note_id']
|
|
25
|
+
: ['dst_note_id', 'src_note_id'];
|
|
26
|
+
const row = await db.getOptional(`
|
|
27
|
+
SELECT COUNT(1) AS count
|
|
28
|
+
FROM c_note_link l
|
|
29
|
+
JOIN c_note n ON n.id = l.${column}
|
|
30
|
+
WHERE l.${boundColumn} = ?
|
|
31
|
+
AND (l.is_del IS NULL OR l.is_del = 0)
|
|
32
|
+
AND (n.is_del IS NULL OR n.is_del = 0)
|
|
33
|
+
`, [noteId]);
|
|
34
|
+
return Number(row?.count ?? 0);
|
|
35
|
+
}
|
|
36
|
+
function buildPreviewText(contentMd, requestedLines) {
|
|
37
|
+
const normalized = (contentMd ?? '').replace(/\r\n/g, '\n');
|
|
38
|
+
if (!normalized) {
|
|
39
|
+
return {
|
|
40
|
+
preview: '',
|
|
41
|
+
totalLines: 0,
|
|
42
|
+
returnedLines: 0,
|
|
43
|
+
truncated: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const lines = normalized.split('\n');
|
|
47
|
+
const selected = lines.slice(0, requestedLines);
|
|
48
|
+
return {
|
|
49
|
+
preview: selected.join('\n'),
|
|
50
|
+
totalLines: lines.length,
|
|
51
|
+
returnedLines: selected.length,
|
|
52
|
+
truncated: selected.length < lines.length,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function getNoteContext(db, id) {
|
|
56
|
+
const row = await db.getOptional(`
|
|
57
|
+
SELECT
|
|
58
|
+
n.id,
|
|
59
|
+
n.title,
|
|
60
|
+
n.summary,
|
|
61
|
+
n.content_text,
|
|
62
|
+
n.tags,
|
|
63
|
+
n.created_at,
|
|
64
|
+
n.updated_at,
|
|
65
|
+
n.is_del,
|
|
66
|
+
n.starred_at,
|
|
67
|
+
CASE WHEN n.starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
|
|
68
|
+
n.zettel_boxes AS zettel_boxes_raw
|
|
69
|
+
FROM c_note n
|
|
70
|
+
WHERE n.id = ?
|
|
71
|
+
`, [id]);
|
|
72
|
+
if (!row) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const boxIds = parseZettelBoxIds(row.zettel_boxes_raw);
|
|
76
|
+
const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
|
|
77
|
+
const [backlinks, outlinks] = await Promise.all([
|
|
78
|
+
getLinkCount(db, 'in', id),
|
|
79
|
+
getLinkCount(db, 'out', id),
|
|
80
|
+
]);
|
|
81
|
+
return {
|
|
82
|
+
id: row.id,
|
|
83
|
+
title: row.title?.trim() ?? '',
|
|
84
|
+
summary: resolveSummary(row.summary, row.content_text),
|
|
85
|
+
tags: parseStoredTags(row.tags),
|
|
86
|
+
created_at: row.created_at,
|
|
87
|
+
updated_at: row.updated_at,
|
|
88
|
+
is_del: row.is_del,
|
|
89
|
+
starred_at: row.starred_at ?? null,
|
|
90
|
+
is_starred: normalizeStoredStarred(row.is_starred),
|
|
91
|
+
zettel_boxes: boxIds
|
|
92
|
+
.map((boxId) => boxNameMap.get(boxId))
|
|
93
|
+
.filter((name) => typeof name === 'string' && name.length > 0),
|
|
94
|
+
links: {
|
|
95
|
+
backlinks,
|
|
96
|
+
outlinks,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function getNotePreview(db, id, requestedLines) {
|
|
101
|
+
const row = await db.getOptional(`
|
|
102
|
+
SELECT
|
|
103
|
+
n.id,
|
|
104
|
+
n.title,
|
|
105
|
+
n.content_md,
|
|
106
|
+
n.updated_at
|
|
107
|
+
FROM c_note n
|
|
108
|
+
WHERE n.id = ?
|
|
109
|
+
`, [id]);
|
|
110
|
+
if (!row) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const preview = buildPreviewText(row.content_md, requestedLines);
|
|
114
|
+
return {
|
|
115
|
+
id: row.id,
|
|
116
|
+
title: row.title?.trim() ?? '',
|
|
117
|
+
requested_lines: requestedLines,
|
|
118
|
+
total_lines: preview.totalLines,
|
|
119
|
+
returned_lines: preview.returnedLines,
|
|
120
|
+
truncated: preview.truncated,
|
|
121
|
+
preview: preview.preview,
|
|
122
|
+
updated_at: row.updated_at ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function mapNoteDetailRow(note, boxNameMap) {
|
|
126
|
+
const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
|
|
127
|
+
const zettelBoxes = boxIds
|
|
128
|
+
.map((boxId) => boxNameMap.get(boxId))
|
|
129
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
130
|
+
return {
|
|
131
|
+
id: note.id,
|
|
132
|
+
created_at: note.created_at,
|
|
133
|
+
updated_at: note.updated_at,
|
|
134
|
+
title: note.title,
|
|
135
|
+
type: note.type,
|
|
136
|
+
source: note.source,
|
|
137
|
+
tags: parseStoredTags(note.tags),
|
|
138
|
+
is_del: note.is_del,
|
|
139
|
+
is_pub: normalizeStoredFlag(note.is_pub),
|
|
140
|
+
properties: parseStoredNoteProperties(note.properties),
|
|
141
|
+
starred_at: note.starred_at ?? null,
|
|
142
|
+
is_starred: normalizeStoredStarred(note.is_starred),
|
|
143
|
+
content_md: note.content_md,
|
|
144
|
+
content_text: note.content_text,
|
|
145
|
+
content_json: parseStoredJsonValue(note.content_json),
|
|
146
|
+
zettel_boxes: zettelBoxes,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function parseStoredJsonValue(raw) {
|
|
150
|
+
const value = raw?.trim() ?? '';
|
|
151
|
+
if (!value) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(value);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return raw;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseStoredNoteProperties(raw) {
|
|
162
|
+
const value = raw?.trim() ?? '';
|
|
163
|
+
if (!value) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(value);
|
|
169
|
+
}
|
|
170
|
+
catch (cause) {
|
|
171
|
+
throw new DinoxError('c_note.properties must be valid JSON', {
|
|
172
|
+
code: 'NOTE_INVALID_PROPERTIES',
|
|
173
|
+
details: { cause: cause instanceof Error ? cause.message : String(cause) },
|
|
174
|
+
recoverable: false,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
178
|
+
throw new DinoxError('c_note.properties must be a JSON object', {
|
|
179
|
+
code: 'NOTE_INVALID_PROPERTIES',
|
|
180
|
+
recoverable: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return Object.fromEntries(Object.entries(parsed));
|
|
184
|
+
}
|
|
185
|
+
function mapNoteExportRow(note, boxNameMap) {
|
|
186
|
+
const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
|
|
187
|
+
const boxes = boxIds
|
|
188
|
+
.map((boxId) => boxNameMap.get(boxId))
|
|
189
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
190
|
+
return {
|
|
191
|
+
id: note.id,
|
|
192
|
+
created_at: note.created_at,
|
|
193
|
+
updated_at: note.updated_at,
|
|
194
|
+
title: note.title,
|
|
195
|
+
type: note.type,
|
|
196
|
+
source: note.source,
|
|
197
|
+
summary: note.summary,
|
|
198
|
+
tags: parseStoredTags(note.tags),
|
|
199
|
+
is_del: note.is_del,
|
|
200
|
+
is_pub: normalizeStoredFlag(note.is_pub),
|
|
201
|
+
properties: parseStoredNoteProperties(note.properties),
|
|
202
|
+
starred_at: note.starred_at ?? null,
|
|
203
|
+
is_starred: normalizeStoredStarred(note.is_starred),
|
|
204
|
+
content_md: note.content_md ?? '',
|
|
205
|
+
content_text: note.content_text,
|
|
206
|
+
content_json: parseStoredJsonValue(note.content_json),
|
|
207
|
+
box_ids: boxIds,
|
|
208
|
+
boxes,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export async function getNoteDetails(db, ids) {
|
|
212
|
+
const targetIds = Array.from(new Set(ids
|
|
213
|
+
.map((id) => id.trim())
|
|
214
|
+
.filter((id) => id.length > 0)));
|
|
215
|
+
if (targetIds.length === 0) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const noteRowsById = new Map();
|
|
219
|
+
const chunkSize = 500;
|
|
220
|
+
for (let index = 0; index < targetIds.length; index += chunkSize) {
|
|
221
|
+
const batch = targetIds.slice(index, index + chunkSize);
|
|
222
|
+
const placeholders = batch.map(() => '?').join(', ');
|
|
223
|
+
const rows = await db.getAll(`
|
|
224
|
+
SELECT
|
|
225
|
+
id,
|
|
226
|
+
created_at,
|
|
227
|
+
updated_at,
|
|
228
|
+
title,
|
|
229
|
+
type,
|
|
230
|
+
source,
|
|
231
|
+
tags,
|
|
232
|
+
is_del,
|
|
233
|
+
is_pub,
|
|
234
|
+
properties,
|
|
235
|
+
starred_at,
|
|
236
|
+
CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
|
|
237
|
+
content_md,
|
|
238
|
+
content_text,
|
|
239
|
+
content_json,
|
|
240
|
+
zettel_boxes AS zettel_boxes_raw
|
|
241
|
+
FROM c_note
|
|
242
|
+
WHERE id IN (${placeholders})
|
|
243
|
+
`, batch);
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
const id = row.id?.trim() ?? '';
|
|
246
|
+
if (!id || noteRowsById.has(id)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
noteRowsById.set(id, row);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
|
|
253
|
+
const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
|
|
254
|
+
const details = [];
|
|
255
|
+
for (const id of targetIds) {
|
|
256
|
+
const row = noteRowsById.get(id);
|
|
257
|
+
if (!row) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
details.push(mapNoteDetailRow(row, boxNameMap));
|
|
261
|
+
}
|
|
262
|
+
return details;
|
|
263
|
+
}
|
|
264
|
+
export async function getNoteDetail(db, id) {
|
|
265
|
+
const details = await getNoteDetails(db, [id]);
|
|
266
|
+
return details[0] ?? null;
|
|
267
|
+
}
|
|
268
|
+
export async function getNoteExportItems(db, ids) {
|
|
269
|
+
const targetIds = Array.from(new Set(ids
|
|
270
|
+
.map((id) => id.trim())
|
|
271
|
+
.filter((id) => id.length > 0)));
|
|
272
|
+
if (targetIds.length === 0) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
const noteRowsById = new Map();
|
|
276
|
+
const chunkSize = 500;
|
|
277
|
+
for (let index = 0; index < targetIds.length; index += chunkSize) {
|
|
278
|
+
const batch = targetIds.slice(index, index + chunkSize);
|
|
279
|
+
const placeholders = batch.map(() => '?').join(', ');
|
|
280
|
+
const rows = await db.getAll(`
|
|
281
|
+
SELECT
|
|
282
|
+
id,
|
|
283
|
+
created_at,
|
|
284
|
+
updated_at,
|
|
285
|
+
title,
|
|
286
|
+
type,
|
|
287
|
+
source,
|
|
288
|
+
tags,
|
|
289
|
+
summary,
|
|
290
|
+
is_del,
|
|
291
|
+
is_pub,
|
|
292
|
+
properties,
|
|
293
|
+
starred_at,
|
|
294
|
+
CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
|
|
295
|
+
content_md,
|
|
296
|
+
content_text,
|
|
297
|
+
content_json,
|
|
298
|
+
zettel_boxes AS zettel_boxes_raw
|
|
299
|
+
FROM c_note
|
|
300
|
+
WHERE id IN (${placeholders})
|
|
301
|
+
`, batch);
|
|
302
|
+
for (const row of rows) {
|
|
303
|
+
const id = row.id?.trim() ?? '';
|
|
304
|
+
if (!id || noteRowsById.has(id)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
noteRowsById.set(id, row);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
|
|
311
|
+
const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
|
|
312
|
+
const items = [];
|
|
313
|
+
for (const id of targetIds) {
|
|
314
|
+
const row = noteRowsById.get(id);
|
|
315
|
+
if (!row) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
items.push(mapNoteExportRow(row, boxNameMap));
|
|
319
|
+
}
|
|
320
|
+
return items;
|
|
321
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
|
|
3
|
+
export declare function validateTagsForUpdate(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
|
|
4
|
+
export declare function validateTagsForBulk(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
|
|
5
|
+
export declare function resolveZettelBoxIdsForCreate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
6
|
+
export declare function resolveZettelBoxIdsForSearch(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
7
|
+
export declare function resolveZettelBoxIdsForUpdate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
8
|
+
export declare function resolveZettelBoxIdsForBulk(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Tag validation and zettel box name/path resolution for note writes and search.
|
|
2
|
+
import { DinoxError } from '../../../utils/errors.js';
|
|
3
|
+
import { canonicalizeBoxPath, listBoxes } from '../../boxes/repo.js';
|
|
4
|
+
import { listTags } from '../../tags/repo.js';
|
|
5
|
+
import { normalizeLookupKey } from './shared.js';
|
|
6
|
+
function normalizeZettelBoxLookupKey(value) {
|
|
7
|
+
return canonicalizeBoxPath(value).toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function buildZettelBoxRetryQuestion(intent, type) {
|
|
10
|
+
const command = intent === 'search'
|
|
11
|
+
? 'dino note search'
|
|
12
|
+
: intent === 'update'
|
|
13
|
+
? 'dino note update'
|
|
14
|
+
: intent === 'bulk'
|
|
15
|
+
? 'dino note bulk'
|
|
16
|
+
: 'dino note create';
|
|
17
|
+
if (type === 'ambiguous') {
|
|
18
|
+
return `Do you want to specify box paths and retry \`${command}\`?`;
|
|
19
|
+
}
|
|
20
|
+
return `Do you want to add these missing boxes and retry \`${command}\`?`;
|
|
21
|
+
}
|
|
22
|
+
export async function validateTagsForCreate(db, requestedTags, userId) {
|
|
23
|
+
return validateTagsForWrite(db, requestedTags, userId, 'dino note create');
|
|
24
|
+
}
|
|
25
|
+
export async function validateTagsForUpdate(db, requestedTags, userId) {
|
|
26
|
+
return validateTagsForWrite(db, requestedTags, userId, 'dino note update');
|
|
27
|
+
}
|
|
28
|
+
export async function validateTagsForBulk(db, requestedTags, userId) {
|
|
29
|
+
return validateTagsForWrite(db, requestedTags, userId, 'dino note bulk');
|
|
30
|
+
}
|
|
31
|
+
async function validateTagsForWrite(db, requestedTags, userId, retryCommand) {
|
|
32
|
+
const normalizedUserId = userId.trim();
|
|
33
|
+
if (!normalizedUserId) {
|
|
34
|
+
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.');
|
|
35
|
+
}
|
|
36
|
+
if (requestedTags.length === 0) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const tags = await listTags(db, { userId: normalizedUserId });
|
|
40
|
+
const existingTagMap = new Map();
|
|
41
|
+
for (const tag of tags) {
|
|
42
|
+
const key = normalizeLookupKey(tag);
|
|
43
|
+
if (!key || existingTagMap.has(key)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
existingTagMap.set(key, tag);
|
|
47
|
+
}
|
|
48
|
+
const resolved = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const missing = [];
|
|
51
|
+
for (const requested of requestedTags) {
|
|
52
|
+
const key = normalizeLookupKey(requested);
|
|
53
|
+
if (!key || seen.has(key)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
seen.add(key);
|
|
57
|
+
const matched = existingTagMap.get(key);
|
|
58
|
+
if (!matched) {
|
|
59
|
+
missing.push(requested);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
resolved.push(matched);
|
|
63
|
+
}
|
|
64
|
+
if (missing.length > 0) {
|
|
65
|
+
throw new DinoxError(`Unknown tags: ${missing.join(', ')}`, {
|
|
66
|
+
details: {
|
|
67
|
+
type: 'missing_tags',
|
|
68
|
+
missing,
|
|
69
|
+
knownCount: tags.length,
|
|
70
|
+
knownSample: tags.slice(0, 20),
|
|
71
|
+
question: `Do you want to add these missing tags and retry \`${retryCommand}\`?`,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
|
|
78
|
+
return resolveZettelBoxIdsByName(db, requestedNames, 'create');
|
|
79
|
+
}
|
|
80
|
+
export async function resolveZettelBoxIdsForSearch(db, requestedNames) {
|
|
81
|
+
return resolveZettelBoxIdsByName(db, requestedNames, 'search');
|
|
82
|
+
}
|
|
83
|
+
export async function resolveZettelBoxIdsForUpdate(db, requestedNames) {
|
|
84
|
+
return resolveZettelBoxIdsByName(db, requestedNames, 'update');
|
|
85
|
+
}
|
|
86
|
+
export async function resolveZettelBoxIdsForBulk(db, requestedNames) {
|
|
87
|
+
return resolveZettelBoxIdsByName(db, requestedNames, 'bulk');
|
|
88
|
+
}
|
|
89
|
+
async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
|
|
90
|
+
if (requestedNames.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const boxes = await listBoxes(db);
|
|
94
|
+
const availablePaths = Array.from(new Set(boxes.map((box) => box.path.trim()).filter(Boolean)));
|
|
95
|
+
const boxByPath = new Map();
|
|
96
|
+
const boxByName = new Map();
|
|
97
|
+
for (const box of boxes) {
|
|
98
|
+
const name = box.name.trim();
|
|
99
|
+
const path = canonicalizeBoxPath(box.path || name);
|
|
100
|
+
const pathKey = normalizeZettelBoxLookupKey(path);
|
|
101
|
+
if (pathKey && !boxByPath.has(pathKey)) {
|
|
102
|
+
boxByPath.set(pathKey, box.id);
|
|
103
|
+
}
|
|
104
|
+
const nameKey = normalizeLookupKey(name);
|
|
105
|
+
if (!nameKey) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const current = boxByName.get(nameKey);
|
|
109
|
+
if (!current) {
|
|
110
|
+
boxByName.set(nameKey, {
|
|
111
|
+
name,
|
|
112
|
+
ids: [box.id],
|
|
113
|
+
paths: path ? [path] : [],
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!current.ids.includes(box.id)) {
|
|
118
|
+
current.ids.push(box.id);
|
|
119
|
+
}
|
|
120
|
+
if (path && !current.paths.includes(path)) {
|
|
121
|
+
current.paths.push(path);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const resolvedIds = [];
|
|
125
|
+
const seenIds = new Set();
|
|
126
|
+
const missing = [];
|
|
127
|
+
const ambiguous = [];
|
|
128
|
+
const seenAmbiguous = new Set();
|
|
129
|
+
for (const requested of requestedNames) {
|
|
130
|
+
const pathKey = normalizeZettelBoxLookupKey(requested);
|
|
131
|
+
if (!pathKey) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const pathMatchId = boxByPath.get(pathKey);
|
|
135
|
+
if (pathMatchId) {
|
|
136
|
+
if (!seenIds.has(pathMatchId)) {
|
|
137
|
+
seenIds.add(pathMatchId);
|
|
138
|
+
resolvedIds.push(pathMatchId);
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const lookup = boxByName.get(normalizeLookupKey(requested));
|
|
143
|
+
if (!lookup) {
|
|
144
|
+
missing.push(requested);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (lookup.ids.length > 1) {
|
|
148
|
+
const marker = `${requested.toLowerCase()}::${lookup.ids.join(',')}`;
|
|
149
|
+
if (!seenAmbiguous.has(marker)) {
|
|
150
|
+
seenAmbiguous.add(marker);
|
|
151
|
+
ambiguous.push({
|
|
152
|
+
name: requested,
|
|
153
|
+
ids: lookup.ids,
|
|
154
|
+
paths: lookup.paths,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const id = lookup.ids[0];
|
|
160
|
+
if (!seenIds.has(id)) {
|
|
161
|
+
seenIds.add(id);
|
|
162
|
+
resolvedIds.push(id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (ambiguous.length > 0) {
|
|
166
|
+
throw new DinoxError(`Ambiguous zettel box names: ${ambiguous.map((item) => `${item.name} -> [${item.paths.join(', ')}]`).join('; ')}`, {
|
|
167
|
+
details: {
|
|
168
|
+
type: 'ambiguous_zettel_boxes',
|
|
169
|
+
ambiguous,
|
|
170
|
+
availablePathsSample: availablePaths.slice(0, 20),
|
|
171
|
+
question: buildZettelBoxRetryQuestion(intent, 'ambiguous'),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (missing.length > 0) {
|
|
176
|
+
throw new DinoxError(`Unknown zettel box paths or names: ${missing.join(', ')}`, {
|
|
177
|
+
details: {
|
|
178
|
+
type: 'missing_zettel_boxes',
|
|
179
|
+
missing,
|
|
180
|
+
availableCount: availablePaths.length,
|
|
181
|
+
availablePathsSample: availablePaths.slice(0, 20),
|
|
182
|
+
question: buildZettelBoxRetryQuestion(intent, 'missing'),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return resolvedIds;
|
|
187
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type SearchNotesOptions, type SearchedNote } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* JSON-safe note-search request. Box *names/paths* are carried (not resolved ids) so the
|
|
5
|
+
* same payload can be executed either in-process or sent to the daemon over the wire;
|
|
6
|
+
* id resolution happens inside {@link executeNoteSearch} against whichever db runs it.
|
|
7
|
+
*/
|
|
8
|
+
export type NoteSearchExecuteParams = {
|
|
9
|
+
keyword: string;
|
|
10
|
+
includeDeleted: boolean;
|
|
11
|
+
tagsExpression?: string;
|
|
12
|
+
createdAtFrom?: string;
|
|
13
|
+
createdAtTo?: string;
|
|
14
|
+
starred?: boolean;
|
|
15
|
+
boxValues: string[];
|
|
16
|
+
matchEmptyZettelBoxes: boolean;
|
|
17
|
+
sqlExpression?: string;
|
|
18
|
+
limit?: number;
|
|
19
|
+
offset?: number;
|
|
20
|
+
};
|
|
21
|
+
export type NoteSearchExecuteResult = {
|
|
22
|
+
rows: SearchedNote[];
|
|
23
|
+
totalCount: number;
|
|
24
|
+
/** Resolved zettel-box ids, surfaced so callers can echo them in response metadata. */
|
|
25
|
+
zettelBoxIds: string[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Resolve box names to ids and run the note search. Shared by the local command path and
|
|
29
|
+
* the daemon RPC handler so both produce identical results from one implementation.
|
|
30
|
+
*/
|
|
31
|
+
export declare function executeNoteSearch(db: AbstractPowerSyncDatabase, params: NoteSearchExecuteParams): Promise<NoteSearchExecuteResult>;
|
|
32
|
+
export declare function searchNotes(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesOptions): Promise<SearchedNote[]>;
|
|
33
|
+
type SearchNotesPageOptions = SearchNotesOptions & {
|
|
34
|
+
limit?: number;
|
|
35
|
+
offset?: number;
|
|
36
|
+
};
|
|
37
|
+
type SearchNotesPageResult = {
|
|
38
|
+
rows: SearchedNote[];
|
|
39
|
+
totalCount: number;
|
|
40
|
+
};
|
|
41
|
+
export declare function searchNotesPage(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesPageOptions): Promise<SearchNotesPageResult>;
|
|
42
|
+
export {};
|