@dinoxx/dinox-cli 1.0.16 → 1.0.18
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/README.md +102 -6
- package/dist/cli.js +2 -0
- package/dist/commands/graph/index.js +16 -1
- package/dist/commands/graph/repo.d.ts +7 -0
- package/dist/commands/graph/repo.js +50 -0
- package/dist/commands/mcp/index.js +3 -2
- package/dist/commands/mcp/server.d.ts +4 -3
- package/dist/commands/mcp/server.js +123 -55
- package/dist/commands/mcp/tools.js +14 -0
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/mcp/types.js +1 -1
- package/dist/commands/meta/index.d.ts +2 -0
- package/dist/commands/meta/index.js +45 -0
- package/dist/commands/meta/repo.d.ts +27 -0
- package/dist/commands/meta/repo.js +71 -0
- package/dist/commands/notes/index.d.ts +13 -3
- package/dist/commands/notes/index.js +135 -6
- package/dist/commands/notes/repo.d.ts +26 -0
- package/dist/commands/notes/repo.js +99 -0
- package/dist/commands/notes/searchOutput.d.ts +6 -0
- package/dist/commands/notes/searchOutput.js +41 -0
- package/dist/daemon/server.js +4 -1
- package/dist/dinox.js +6 -14
- package/dist/utils/errors.d.ts +22 -5
- package/dist/utils/errors.js +29 -0
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadConfig } from '../../config/store.js';
|
|
2
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
3
|
+
import { printYaml } from '../../utils/output.js';
|
|
4
|
+
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
5
|
+
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
6
|
+
import { getMetaSchema, getMetaStats } from './repo.js';
|
|
7
|
+
async function syncBeforeMetaCommand(db, offline, timeoutMs) {
|
|
8
|
+
if (offline) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
|
|
12
|
+
}
|
|
13
|
+
export function registerMetaCommands(program) {
|
|
14
|
+
const meta = program.command('meta').description('Query Dinox metadata');
|
|
15
|
+
meta
|
|
16
|
+
.command('stats')
|
|
17
|
+
.description('Get metadata stats (notes/tags/boxes)')
|
|
18
|
+
.action(async (_options, command) => {
|
|
19
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
20
|
+
const offline = Boolean(globals.offline);
|
|
21
|
+
const config = resolveConfig(await loadConfig());
|
|
22
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
23
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
24
|
+
try {
|
|
25
|
+
await syncBeforeMetaCommand(db, offline, timeoutMs);
|
|
26
|
+
const stats = await getMetaStats(db);
|
|
27
|
+
printYaml({
|
|
28
|
+
stale: offline ? true : stale,
|
|
29
|
+
stats,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await db.close().catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
meta
|
|
37
|
+
.command('schema')
|
|
38
|
+
.description('Get structured Dinox data schema for AI integrations')
|
|
39
|
+
.action(() => {
|
|
40
|
+
printYaml(getMetaSchema());
|
|
41
|
+
});
|
|
42
|
+
meta.action((_options, command) => {
|
|
43
|
+
command.outputHelp();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
export type MetaStats = {
|
|
3
|
+
notes_count: number;
|
|
4
|
+
tags_count: number;
|
|
5
|
+
boxes_count: number;
|
|
6
|
+
latest_note_updated_at: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type MetaSchema = {
|
|
9
|
+
version: number;
|
|
10
|
+
entities: {
|
|
11
|
+
note_search_result: {
|
|
12
|
+
fields: string[];
|
|
13
|
+
meta_fields: string[];
|
|
14
|
+
};
|
|
15
|
+
note_context: {
|
|
16
|
+
fields: string[];
|
|
17
|
+
};
|
|
18
|
+
graph_stats: {
|
|
19
|
+
fields: string[];
|
|
20
|
+
};
|
|
21
|
+
meta_stats: {
|
|
22
|
+
fields: string[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export declare function getMetaStats(db: AbstractPowerSyncDatabase): Promise<MetaStats>;
|
|
27
|
+
export declare function getMetaSchema(): MetaSchema;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export async function getMetaStats(db) {
|
|
2
|
+
const [notesRow, tagsRow, boxesRow, latestNoteRow] = await Promise.all([
|
|
3
|
+
db.getOptional(`
|
|
4
|
+
SELECT COUNT(1) AS count
|
|
5
|
+
FROM c_note n
|
|
6
|
+
WHERE n.is_del IS NULL OR n.is_del = 0
|
|
7
|
+
`, []),
|
|
8
|
+
db.getOptional(`
|
|
9
|
+
SELECT COUNT(1) AS count
|
|
10
|
+
FROM c_tag_node t
|
|
11
|
+
WHERE t.is_del IS NULL OR t.is_del = 0
|
|
12
|
+
`, []),
|
|
13
|
+
db.getOptional(`
|
|
14
|
+
SELECT COUNT(1) AS count
|
|
15
|
+
FROM c_zettel_box b
|
|
16
|
+
WHERE b.is_del IS NULL OR b.is_del = 0
|
|
17
|
+
`, []),
|
|
18
|
+
db.getOptional(`
|
|
19
|
+
SELECT MAX(COALESCE(n.updated_at, n.created_at)) AS latest
|
|
20
|
+
FROM c_note n
|
|
21
|
+
WHERE n.is_del IS NULL OR n.is_del = 0
|
|
22
|
+
`, []),
|
|
23
|
+
]);
|
|
24
|
+
return {
|
|
25
|
+
notes_count: Number(notesRow?.count ?? 0),
|
|
26
|
+
tags_count: Number(tagsRow?.count ?? 0),
|
|
27
|
+
boxes_count: Number(boxesRow?.count ?? 0),
|
|
28
|
+
latest_note_updated_at: latestNoteRow?.latest ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function getMetaSchema() {
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
entities: {
|
|
35
|
+
note_search_result: {
|
|
36
|
+
fields: ['id', 'title', 'summary', 'tags', 'created_at', 'zettel_boxes'],
|
|
37
|
+
meta_fields: [
|
|
38
|
+
'query',
|
|
39
|
+
'returned_count',
|
|
40
|
+
'total_count',
|
|
41
|
+
'stale',
|
|
42
|
+
'pagination.limit',
|
|
43
|
+
'pagination.offset',
|
|
44
|
+
'pagination.has_more',
|
|
45
|
+
'fields',
|
|
46
|
+
'applied_filters',
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
note_context: {
|
|
50
|
+
fields: [
|
|
51
|
+
'id',
|
|
52
|
+
'title',
|
|
53
|
+
'summary',
|
|
54
|
+
'tags',
|
|
55
|
+
'created_at',
|
|
56
|
+
'updated_at',
|
|
57
|
+
'is_del',
|
|
58
|
+
'zettel_boxes',
|
|
59
|
+
'links.backlinks',
|
|
60
|
+
'links.outlinks',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
graph_stats: {
|
|
64
|
+
fields: ['notes_count', 'links_count', 'connected_notes_count', 'isolated_notes_count'],
|
|
65
|
+
},
|
|
66
|
+
meta_stats: {
|
|
67
|
+
fields: ['notes_count', 'tags_count', 'boxes_count', 'latest_note_updated_at'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { type
|
|
2
|
+
import { type NoteSearchField, type NoteSearchResultItem } from './searchOutput.js';
|
|
3
3
|
type NoteSearchResponse = {
|
|
4
4
|
meta: {
|
|
5
5
|
query: string;
|
|
6
6
|
returned_count: number;
|
|
7
7
|
total_count: number;
|
|
8
8
|
stale: boolean;
|
|
9
|
+
pagination: {
|
|
10
|
+
limit: number | null;
|
|
11
|
+
offset: number;
|
|
12
|
+
has_more: boolean;
|
|
13
|
+
};
|
|
14
|
+
fields: NoteSearchField[] | null;
|
|
9
15
|
applied_filters: {
|
|
10
16
|
include_deleted: boolean;
|
|
11
17
|
tags_expression: string | null;
|
|
@@ -20,13 +26,17 @@ type NoteSearchResponse = {
|
|
|
20
26
|
sql_expression: string | null;
|
|
21
27
|
};
|
|
22
28
|
};
|
|
23
|
-
notes:
|
|
29
|
+
notes: NoteSearchResultItem[];
|
|
24
30
|
};
|
|
25
31
|
type BuildNoteSearchResponseInput = {
|
|
26
32
|
query: string;
|
|
27
|
-
notes:
|
|
33
|
+
notes: NoteSearchResultItem[];
|
|
34
|
+
totalCount: number;
|
|
28
35
|
stale: boolean;
|
|
29
36
|
includeDeleted: boolean;
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
fields?: NoteSearchField[];
|
|
30
40
|
tagsExpression?: string;
|
|
31
41
|
createdAtFrom?: string;
|
|
32
42
|
createdAtTo?: string;
|
|
@@ -9,8 +9,11 @@ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
|
9
9
|
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
10
10
|
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
11
11
|
import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
|
|
12
|
-
import { createNote, getNote, getNoteDetails, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
|
|
12
|
+
import { createNote, getNote, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
|
|
13
|
+
import { parseNoteSearchFields, projectNoteSearchFields, } from './searchOutput.js';
|
|
13
14
|
import { parseCreatedAtRange } from './searchTime.js';
|
|
15
|
+
const DEFAULT_NOTE_SEARCH_LIMIT = 50;
|
|
16
|
+
const MAX_NOTE_SEARCH_LIMIT = 500;
|
|
14
17
|
function parseNoteType(value) {
|
|
15
18
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
16
19
|
if (normalized === 'note' || normalized === 'crawl') {
|
|
@@ -18,6 +21,63 @@ function parseNoteType(value) {
|
|
|
18
21
|
}
|
|
19
22
|
throw new DinoxError('--type must be one of: note, crawl');
|
|
20
23
|
}
|
|
24
|
+
function parseSearchLimit(value) {
|
|
25
|
+
if (value === undefined || value === null || value === '') {
|
|
26
|
+
return DEFAULT_NOTE_SEARCH_LIMIT;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value !== 'string') {
|
|
29
|
+
throw new DinoxError('--limit must be a positive integer');
|
|
30
|
+
}
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
33
|
+
throw new DinoxError('--limit must be a positive integer');
|
|
34
|
+
}
|
|
35
|
+
const parsed = Number(trimmed);
|
|
36
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
37
|
+
throw new DinoxError('--limit must be a positive integer');
|
|
38
|
+
}
|
|
39
|
+
if (parsed > MAX_NOTE_SEARCH_LIMIT) {
|
|
40
|
+
throw new DinoxError(`--limit cannot be greater than ${MAX_NOTE_SEARCH_LIMIT}`);
|
|
41
|
+
}
|
|
42
|
+
return Math.trunc(parsed);
|
|
43
|
+
}
|
|
44
|
+
function parseSearchOffset(value) {
|
|
45
|
+
if (value === undefined || value === null || value === '') {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value !== 'string') {
|
|
49
|
+
throw new DinoxError('--offset must be a non-negative integer');
|
|
50
|
+
}
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
53
|
+
throw new DinoxError('--offset must be a non-negative integer');
|
|
54
|
+
}
|
|
55
|
+
const parsed = Number(trimmed);
|
|
56
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
57
|
+
throw new DinoxError('--offset must be a non-negative integer');
|
|
58
|
+
}
|
|
59
|
+
return Math.trunc(parsed);
|
|
60
|
+
}
|
|
61
|
+
function parsePreviewLineCount(value) {
|
|
62
|
+
if (value === undefined || value === null || value === '') {
|
|
63
|
+
return 10;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value !== 'string') {
|
|
66
|
+
throw new DinoxError('--lines must be a positive integer');
|
|
67
|
+
}
|
|
68
|
+
const trimmed = value.trim();
|
|
69
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
70
|
+
throw new DinoxError('--lines must be a positive integer');
|
|
71
|
+
}
|
|
72
|
+
const parsed = Number(trimmed);
|
|
73
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
74
|
+
throw new DinoxError('--lines must be a positive integer');
|
|
75
|
+
}
|
|
76
|
+
if (parsed > 500) {
|
|
77
|
+
throw new DinoxError('--lines cannot be greater than 500');
|
|
78
|
+
}
|
|
79
|
+
return Math.trunc(parsed);
|
|
80
|
+
}
|
|
21
81
|
async function parseStringInput(value) {
|
|
22
82
|
return readStringOrFile(value);
|
|
23
83
|
}
|
|
@@ -115,12 +175,23 @@ export function buildNoteSearchResponse(input) {
|
|
|
115
175
|
const tagsExpression = input.tagsExpression?.trim() ?? '';
|
|
116
176
|
const sqlExpression = input.sqlExpression?.trim() ?? '';
|
|
117
177
|
const hasCreatedAtRange = Boolean(input.createdAtFrom || input.createdAtTo);
|
|
178
|
+
const offset = input.offset ?? 0;
|
|
179
|
+
const limit = input.limit ?? null;
|
|
180
|
+
const returnedCount = input.notes.length;
|
|
181
|
+
const totalCount = input.totalCount;
|
|
182
|
+
const hasMore = offset + returnedCount < totalCount;
|
|
118
183
|
return {
|
|
119
184
|
meta: {
|
|
120
185
|
query,
|
|
121
|
-
returned_count:
|
|
122
|
-
total_count:
|
|
186
|
+
returned_count: returnedCount,
|
|
187
|
+
total_count: totalCount,
|
|
123
188
|
stale: input.stale,
|
|
189
|
+
pagination: {
|
|
190
|
+
limit,
|
|
191
|
+
offset,
|
|
192
|
+
has_more: hasMore,
|
|
193
|
+
},
|
|
194
|
+
fields: input.fields && input.fields.length > 0 ? [...input.fields] : null,
|
|
124
195
|
applied_filters: {
|
|
125
196
|
include_deleted: input.includeDeleted,
|
|
126
197
|
tags_expression: tagsExpression || null,
|
|
@@ -152,6 +223,9 @@ export function registerNoteCommands(program) {
|
|
|
152
223
|
.option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
|
|
153
224
|
.option('--box <string|@file>', 'zettel box names (JSON array/comma list), or [] for empty boxes')
|
|
154
225
|
.option('--sql <expr>', 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type')
|
|
226
|
+
.option('--limit <n>', `maximum returned notes (default: ${DEFAULT_NOTE_SEARCH_LIMIT}, max: ${MAX_NOTE_SEARCH_LIMIT})`)
|
|
227
|
+
.option('--offset <n>', 'result offset for pagination (default: 0)')
|
|
228
|
+
.option('--fields <list>', 'comma-separated fields: id,title,summary,tags,created_at,zettel_boxes')
|
|
155
229
|
.option('--include-deleted', 'include soft-deleted notes')
|
|
156
230
|
.action(async (query, options, command) => {
|
|
157
231
|
const globals = command.optsWithGlobals?.() ?? {};
|
|
@@ -174,6 +248,9 @@ export function registerNoteCommands(program) {
|
|
|
174
248
|
: { values: [], isExplicitEmptyArray: false };
|
|
175
249
|
const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, parsedBoxInput.values);
|
|
176
250
|
const sqlExpression = typeof options.sql === 'string' ? options.sql.trim() : '';
|
|
251
|
+
const limit = parseSearchLimit(options.limit);
|
|
252
|
+
const offset = parseSearchOffset(options.offset);
|
|
253
|
+
const fields = parseNoteSearchFields(options.fields, '--fields');
|
|
177
254
|
const rows = await searchNotes(db, keyword, {
|
|
178
255
|
includeDeleted: Boolean(options.includeDeleted),
|
|
179
256
|
tagsExpression: tagsExpression || undefined,
|
|
@@ -183,11 +260,18 @@ export function registerNoteCommands(program) {
|
|
|
183
260
|
matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
|
|
184
261
|
sqlExpression: sqlExpression || undefined,
|
|
185
262
|
});
|
|
263
|
+
const totalCount = rows.length;
|
|
264
|
+
const pagedRows = rows.slice(offset, offset + limit);
|
|
265
|
+
const projectedRows = projectNoteSearchFields(pagedRows, fields);
|
|
186
266
|
const response = buildNoteSearchResponse({
|
|
187
267
|
query: keyword,
|
|
188
|
-
notes:
|
|
268
|
+
notes: projectedRows,
|
|
269
|
+
totalCount,
|
|
189
270
|
stale: offline ? true : stale,
|
|
190
271
|
includeDeleted: Boolean(options.includeDeleted),
|
|
272
|
+
limit,
|
|
273
|
+
offset,
|
|
274
|
+
fields,
|
|
191
275
|
tagsExpression: tagsExpression || undefined,
|
|
192
276
|
createdAtFrom,
|
|
193
277
|
createdAtTo,
|
|
@@ -207,7 +291,8 @@ export function registerNoteCommands(program) {
|
|
|
207
291
|
.command('get')
|
|
208
292
|
.description('Get note by id')
|
|
209
293
|
.argument('<id>', 'note id')
|
|
210
|
-
.
|
|
294
|
+
.option('--context-only', 'return lightweight note context (title/tags/summary/links)')
|
|
295
|
+
.action(async (id, options, command) => {
|
|
211
296
|
const globals = command.optsWithGlobals?.() ?? {};
|
|
212
297
|
const offline = Boolean(globals.offline);
|
|
213
298
|
const jsonOutput = Boolean(globals.json);
|
|
@@ -216,9 +301,25 @@ export function registerNoteCommands(program) {
|
|
|
216
301
|
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
217
302
|
try {
|
|
218
303
|
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
304
|
+
if (options.contextOnly) {
|
|
305
|
+
const context = await getNoteContext(db, id);
|
|
306
|
+
if (!context) {
|
|
307
|
+
throw new DinoxError(`Note not found: ${id}`, {
|
|
308
|
+
code: 'NOTE_NOT_FOUND',
|
|
309
|
+
suggestion: "Run 'dino note search <keyword>' to locate the note id.",
|
|
310
|
+
recoverable: true,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
printYaml({ stale: offline ? true : stale, note: context });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
219
316
|
const note = await getNote(db, id);
|
|
220
317
|
if (!note) {
|
|
221
|
-
throw new DinoxError(`Note not found: ${id}
|
|
318
|
+
throw new DinoxError(`Note not found: ${id}`, {
|
|
319
|
+
code: 'NOTE_NOT_FOUND',
|
|
320
|
+
suggestion: "Run 'dino note search <keyword>' to locate the note id.",
|
|
321
|
+
recoverable: true,
|
|
322
|
+
});
|
|
222
323
|
}
|
|
223
324
|
if (jsonOutput) {
|
|
224
325
|
printYaml({ stale, note });
|
|
@@ -240,6 +341,34 @@ export function registerNoteCommands(program) {
|
|
|
240
341
|
await db.close().catch(() => undefined);
|
|
241
342
|
}
|
|
242
343
|
});
|
|
344
|
+
note
|
|
345
|
+
.command('preview')
|
|
346
|
+
.description('Preview the first N lines of note markdown')
|
|
347
|
+
.argument('<id>', 'note id')
|
|
348
|
+
.option('--lines <n>', 'number of lines to return (default: 10)')
|
|
349
|
+
.action(async (id, options, command) => {
|
|
350
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
351
|
+
const offline = Boolean(globals.offline);
|
|
352
|
+
const config = resolveConfig(await loadConfig());
|
|
353
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
354
|
+
const lines = parsePreviewLineCount(options.lines);
|
|
355
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
356
|
+
try {
|
|
357
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
358
|
+
const preview = await getNotePreview(db, id, lines);
|
|
359
|
+
if (!preview) {
|
|
360
|
+
throw new DinoxError(`Note not found: ${id}`, {
|
|
361
|
+
code: 'NOTE_NOT_FOUND',
|
|
362
|
+
suggestion: "Run 'dino note search <keyword>' to locate the note id.",
|
|
363
|
+
recoverable: true,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
printYaml({ stale: offline ? true : stale, note: preview });
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
await db.close().catch(() => undefined);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
243
372
|
note
|
|
244
373
|
.command('detail')
|
|
245
374
|
.description('Get full note details by id')
|
|
@@ -41,6 +41,30 @@ export type NoteDetail = {
|
|
|
41
41
|
content_md: string | null;
|
|
42
42
|
zettel_boxes: string[];
|
|
43
43
|
};
|
|
44
|
+
export type NoteContext = {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
summary: string;
|
|
48
|
+
tags: string[];
|
|
49
|
+
created_at: string | null;
|
|
50
|
+
updated_at: string | null;
|
|
51
|
+
is_del: number | null;
|
|
52
|
+
zettel_boxes: string[];
|
|
53
|
+
links: {
|
|
54
|
+
backlinks: number;
|
|
55
|
+
outlinks: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export type NotePreview = {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
requested_lines: number;
|
|
62
|
+
total_lines: number;
|
|
63
|
+
returned_lines: number;
|
|
64
|
+
truncated: boolean;
|
|
65
|
+
preview: string;
|
|
66
|
+
updated_at: string | null;
|
|
67
|
+
};
|
|
44
68
|
export type UpdateNoteResult = {
|
|
45
69
|
id: string;
|
|
46
70
|
updated: boolean;
|
|
@@ -49,6 +73,8 @@ export type UpdateNoteResult = {
|
|
|
49
73
|
};
|
|
50
74
|
export declare function searchNotes(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesOptions): Promise<SearchedNote[]>;
|
|
51
75
|
export declare function getNote(db: AbstractPowerSyncDatabase, id: string): Promise<NoteRow | null>;
|
|
76
|
+
export declare function getNoteContext(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContext | null>;
|
|
77
|
+
export declare function getNotePreview(db: AbstractPowerSyncDatabase, id: string, requestedLines: number): Promise<NotePreview | null>;
|
|
52
78
|
export declare function getNoteDetails(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteDetail[]>;
|
|
53
79
|
export declare function getNoteDetail(db: AbstractPowerSyncDatabase, id: string): Promise<NoteDetail | null>;
|
|
54
80
|
export declare function createNote(db: AbstractPowerSyncDatabase, input: {
|
|
@@ -495,6 +495,105 @@ export async function getNote(db, id) {
|
|
|
495
495
|
WHERE id = ?
|
|
496
496
|
`, [id]);
|
|
497
497
|
}
|
|
498
|
+
async function getLinkCount(db, direction, noteId) {
|
|
499
|
+
const [column, boundColumn] = direction === 'in'
|
|
500
|
+
? ['src_note_id', 'dst_note_id']
|
|
501
|
+
: ['dst_note_id', 'src_note_id'];
|
|
502
|
+
const row = await db.getOptional(`
|
|
503
|
+
SELECT COUNT(1) AS count
|
|
504
|
+
FROM c_note_link l
|
|
505
|
+
JOIN c_note n ON n.id = l.${column}
|
|
506
|
+
WHERE l.${boundColumn} = ?
|
|
507
|
+
AND l.is_del = 0
|
|
508
|
+
AND (n.is_del IS NULL OR n.is_del = 0)
|
|
509
|
+
`, [noteId]);
|
|
510
|
+
return Number(row?.count ?? 0);
|
|
511
|
+
}
|
|
512
|
+
function buildPreviewText(contentMd, requestedLines) {
|
|
513
|
+
const normalized = (contentMd ?? '').replace(/\r\n/g, '\n');
|
|
514
|
+
if (!normalized) {
|
|
515
|
+
return {
|
|
516
|
+
preview: '',
|
|
517
|
+
totalLines: 0,
|
|
518
|
+
returnedLines: 0,
|
|
519
|
+
truncated: false,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const lines = normalized.split('\n');
|
|
523
|
+
const selected = lines.slice(0, requestedLines);
|
|
524
|
+
return {
|
|
525
|
+
preview: selected.join('\n'),
|
|
526
|
+
totalLines: lines.length,
|
|
527
|
+
returnedLines: selected.length,
|
|
528
|
+
truncated: selected.length < lines.length,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
export async function getNoteContext(db, id) {
|
|
532
|
+
const row = await db.getOptional(`
|
|
533
|
+
SELECT
|
|
534
|
+
n.id,
|
|
535
|
+
n.title,
|
|
536
|
+
n.summary,
|
|
537
|
+
n.content_text,
|
|
538
|
+
n.tags,
|
|
539
|
+
n.created_at,
|
|
540
|
+
n.updated_at,
|
|
541
|
+
n.is_del,
|
|
542
|
+
n.zettel_boxes AS zettel_boxes_raw
|
|
543
|
+
FROM c_note n
|
|
544
|
+
WHERE n.id = ?
|
|
545
|
+
`, [id]);
|
|
546
|
+
if (!row) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const boxIds = parseZettelBoxIds(row.zettel_boxes_raw);
|
|
550
|
+
const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
|
|
551
|
+
const [backlinks, outlinks] = await Promise.all([
|
|
552
|
+
getLinkCount(db, 'in', id),
|
|
553
|
+
getLinkCount(db, 'out', id),
|
|
554
|
+
]);
|
|
555
|
+
return {
|
|
556
|
+
id: row.id,
|
|
557
|
+
title: row.title?.trim() ?? '',
|
|
558
|
+
summary: resolveSummary(row.summary, row.content_text),
|
|
559
|
+
tags: parseStoredTags(row.tags),
|
|
560
|
+
created_at: row.created_at,
|
|
561
|
+
updated_at: row.updated_at,
|
|
562
|
+
is_del: row.is_del,
|
|
563
|
+
zettel_boxes: boxIds
|
|
564
|
+
.map((boxId) => boxNameMap.get(boxId))
|
|
565
|
+
.filter((name) => typeof name === 'string' && name.length > 0),
|
|
566
|
+
links: {
|
|
567
|
+
backlinks,
|
|
568
|
+
outlinks,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
export async function getNotePreview(db, id, requestedLines) {
|
|
573
|
+
const row = await db.getOptional(`
|
|
574
|
+
SELECT
|
|
575
|
+
n.id,
|
|
576
|
+
n.title,
|
|
577
|
+
n.content_md,
|
|
578
|
+
n.updated_at
|
|
579
|
+
FROM c_note n
|
|
580
|
+
WHERE n.id = ?
|
|
581
|
+
`, [id]);
|
|
582
|
+
if (!row) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const preview = buildPreviewText(row.content_md, requestedLines);
|
|
586
|
+
return {
|
|
587
|
+
id: row.id,
|
|
588
|
+
title: row.title?.trim() ?? '',
|
|
589
|
+
requested_lines: requestedLines,
|
|
590
|
+
total_lines: preview.totalLines,
|
|
591
|
+
returned_lines: preview.returnedLines,
|
|
592
|
+
truncated: preview.truncated,
|
|
593
|
+
preview: preview.preview,
|
|
594
|
+
updated_at: row.updated_at ?? null,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
498
597
|
function mapNoteDetailRow(note, boxNameMap) {
|
|
499
598
|
const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
|
|
500
599
|
const zettelBoxes = boxIds
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SearchedNote } from './repo.js';
|
|
2
|
+
export declare const NOTE_SEARCH_FIELDS: readonly ["id", "title", "summary", "tags", "created_at", "zettel_boxes"];
|
|
3
|
+
export type NoteSearchField = (typeof NOTE_SEARCH_FIELDS)[number];
|
|
4
|
+
export type NoteSearchResultItem = Partial<SearchedNote>;
|
|
5
|
+
export declare function parseNoteSearchFields(value: unknown, optionName: string): NoteSearchField[] | undefined;
|
|
6
|
+
export declare function projectNoteSearchFields(notes: SearchedNote[], fields: NoteSearchField[] | undefined): NoteSearchResultItem[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
2
|
+
export const NOTE_SEARCH_FIELDS = ['id', 'title', 'summary', 'tags', 'created_at', 'zettel_boxes'];
|
|
3
|
+
const NOTE_SEARCH_FIELD_SET = new Set(NOTE_SEARCH_FIELDS);
|
|
4
|
+
export function parseNoteSearchFields(value, optionName) {
|
|
5
|
+
if (value === undefined || value === null || value === '') {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
let rawFields = [];
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
rawFields = value.map((item) => String(item).trim());
|
|
11
|
+
}
|
|
12
|
+
else if (typeof value === 'string') {
|
|
13
|
+
rawFields = value
|
|
14
|
+
.split(/[\n,,]/g)
|
|
15
|
+
.map((item) => item.trim());
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
throw new DinoxError(`${optionName} must be a comma-separated string or string array`);
|
|
19
|
+
}
|
|
20
|
+
const normalized = Array.from(new Set(rawFields.filter(Boolean)));
|
|
21
|
+
if (normalized.length === 0) {
|
|
22
|
+
throw new DinoxError(`${optionName} must include at least one field`);
|
|
23
|
+
}
|
|
24
|
+
const invalidField = normalized.find((field) => !NOTE_SEARCH_FIELD_SET.has(field));
|
|
25
|
+
if (invalidField) {
|
|
26
|
+
throw new DinoxError(`${optionName} has unsupported field: ${invalidField}. Allowed: ${NOTE_SEARCH_FIELDS.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
export function projectNoteSearchFields(notes, fields) {
|
|
31
|
+
if (!fields || fields.length === 0) {
|
|
32
|
+
return notes.map((note) => ({ ...note }));
|
|
33
|
+
}
|
|
34
|
+
return notes.map((note) => {
|
|
35
|
+
const item = {};
|
|
36
|
+
for (const field of fields) {
|
|
37
|
+
item[field] = note[field];
|
|
38
|
+
}
|
|
39
|
+
return item;
|
|
40
|
+
});
|
|
41
|
+
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { loadConfig } from '../config/store.js';
|
|
|
3
3
|
import { resolveConfig } from '../config/resolve.js';
|
|
4
4
|
import { connectPowerSync, waitForIdleDataFlow } from '../powersync/runtime.js';
|
|
5
5
|
import { searchNotes, getNoteDetails } from '../commands/notes/repo.js';
|
|
6
|
-
import { getBacklinks, getOutlinks, getRelatedNotes } from '../commands/graph/repo.js';
|
|
6
|
+
import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from '../commands/graph/repo.js';
|
|
7
7
|
import { searchTodoTasks } from '../commands/todo/repo.js';
|
|
8
8
|
import { savePid, removePid } from './pid.js';
|
|
9
9
|
import { getPackageVersion } from '../utils/version.js';
|
|
@@ -75,6 +75,9 @@ export class DaemonServer {
|
|
|
75
75
|
case 'graph.related':
|
|
76
76
|
const related = await getRelatedNotes(this.db, params.id, params.depth || 2);
|
|
77
77
|
return { id: req.id, ok: true, result: related };
|
|
78
|
+
case 'graph.stats':
|
|
79
|
+
const graphStats = await getGraphStats(this.db);
|
|
80
|
+
return { id: req.id, ok: true, result: graphStats };
|
|
78
81
|
case 'todo.search':
|
|
79
82
|
const todos = await searchTodoTasks(this.db, params.query || '', {
|
|
80
83
|
includeDeleted: false,
|
package/dist/dinox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createCli } from './cli.js';
|
|
3
|
-
import { isDinoxError } from './utils/errors.js';
|
|
3
|
+
import { buildStructuredErrorPayload, isDinoxError } from './utils/errors.js';
|
|
4
4
|
import { printYaml } from './utils/output.js';
|
|
5
5
|
function normalizeArgv(argv) {
|
|
6
6
|
// pnpm/npm script forwarding can prepend a standalone "--".
|
|
@@ -25,26 +25,18 @@ main().catch((error) => {
|
|
|
25
25
|
const argv = process.argv;
|
|
26
26
|
const json = shouldOutputJson(argv);
|
|
27
27
|
const verbose = isVerbose(argv);
|
|
28
|
-
const
|
|
29
|
-
const name = error instanceof Error ? error.name : 'Error';
|
|
30
|
-
const stack = error instanceof Error ? error.stack : undefined;
|
|
31
|
-
const details = isDinoxError(error) ? error.details : undefined;
|
|
28
|
+
const payload = buildStructuredErrorPayload(error, verbose);
|
|
32
29
|
const exitCode = isDinoxError(error) ? error.exitCode : 1;
|
|
33
30
|
process.exitCode = exitCode;
|
|
34
31
|
if (json) {
|
|
35
32
|
printYaml({
|
|
36
33
|
ok: false,
|
|
37
|
-
error:
|
|
38
|
-
name,
|
|
39
|
-
message,
|
|
40
|
-
...(details !== undefined ? { details } : {}),
|
|
41
|
-
...(verbose && stack ? { stack } : {}),
|
|
42
|
-
},
|
|
34
|
+
error: payload,
|
|
43
35
|
});
|
|
44
36
|
return;
|
|
45
37
|
}
|
|
46
|
-
console.error(message);
|
|
47
|
-
if (verbose && stack) {
|
|
48
|
-
console.error(stack);
|
|
38
|
+
console.error(payload.message);
|
|
39
|
+
if (verbose && payload.stack) {
|
|
40
|
+
console.error(payload.stack);
|
|
49
41
|
}
|
|
50
42
|
});
|