@dinoxx/dinox-cli 1.0.11 → 1.0.12
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.
|
@@ -61,6 +61,26 @@ async function parseStringListInput(value, optionName) {
|
|
|
61
61
|
const raw = await readStringOrFile(value);
|
|
62
62
|
return parseListInput(raw, optionName);
|
|
63
63
|
}
|
|
64
|
+
async function parseSearchListInput(value, optionName) {
|
|
65
|
+
const raw = await readStringOrFile(value);
|
|
66
|
+
return {
|
|
67
|
+
values: parseListInput(raw, optionName),
|
|
68
|
+
isExplicitEmptyArray: isExplicitEmptyArrayLiteral(raw),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function isExplicitEmptyArrayLiteral(value) {
|
|
72
|
+
const normalized = value.trim();
|
|
73
|
+
if (!normalized.startsWith('[')) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(normalized);
|
|
78
|
+
return Array.isArray(parsed) && parsed.length === 0;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
64
84
|
async function parseTargetNoteIds(idArg, idsOption, commandName) {
|
|
65
85
|
const merged = [];
|
|
66
86
|
const idFromArg = typeof idArg === 'string' ? idArg.trim() : '';
|
|
@@ -91,11 +111,11 @@ export function registerNoteCommands(program) {
|
|
|
91
111
|
.command('search')
|
|
92
112
|
.description('Search notes and return note list for AI agents')
|
|
93
113
|
.argument('[query]', 'search query')
|
|
94
|
-
.option('--tags <expr>', 'tag expression with AND/OR/NOT,
|
|
114
|
+
.option('--tags <expr>', 'tag expression with AND/OR/NOT, or [] for empty tags')
|
|
95
115
|
.option('--from <date>', 'created_at start (YYYY-MM-DD or ISO datetime)')
|
|
96
116
|
.option('--to <date>', 'created_at end (YYYY-MM-DD or ISO datetime)')
|
|
97
117
|
.option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
|
|
98
|
-
.option('--box <string|@file>', 'zettel box names (JSON array or
|
|
118
|
+
.option('--box <string|@file>', 'zettel box names (JSON array/comma list), or [] for empty boxes')
|
|
99
119
|
.option('--sql <expr>', 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type')
|
|
100
120
|
.option('--include-deleted', 'include soft-deleted notes')
|
|
101
121
|
.action(async (query, options, command) => {
|
|
@@ -114,8 +134,10 @@ export function registerNoteCommands(program) {
|
|
|
114
134
|
days: options.days,
|
|
115
135
|
});
|
|
116
136
|
const boxInput = typeof options.box === 'string' ? options.box : '';
|
|
117
|
-
const
|
|
118
|
-
|
|
137
|
+
const parsedBoxInput = boxInput
|
|
138
|
+
? await parseSearchListInput(boxInput, '--box')
|
|
139
|
+
: { values: [], isExplicitEmptyArray: false };
|
|
140
|
+
const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, parsedBoxInput.values);
|
|
119
141
|
const sqlExpression = typeof options.sql === 'string' ? options.sql.trim() : '';
|
|
120
142
|
const rows = await searchNotes(db, keyword, {
|
|
121
143
|
includeDeleted: Boolean(options.includeDeleted),
|
|
@@ -123,6 +145,7 @@ export function registerNoteCommands(program) {
|
|
|
123
145
|
createdAtFrom,
|
|
124
146
|
createdAtTo,
|
|
125
147
|
zettelBoxIds,
|
|
148
|
+
matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
|
|
126
149
|
sqlExpression: sqlExpression || undefined,
|
|
127
150
|
});
|
|
128
151
|
printYaml(rows);
|
|
@@ -9,7 +9,7 @@ export async function searchNotes(db, query, options) {
|
|
|
9
9
|
const normalizedQuery = query.trim();
|
|
10
10
|
const tagFilter = buildTagFilter(options.tagsExpression, 'n');
|
|
11
11
|
const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
|
|
12
|
-
const zettelBoxFilter = buildZettelBoxFilter(options.zettelBoxIds, 'n');
|
|
12
|
+
const zettelBoxFilter = buildZettelBoxFilter(options.zettelBoxIds, options.matchEmptyZettelBoxes, 'n');
|
|
13
13
|
const sqlFilter = await buildNoteSearchSqlFilter({
|
|
14
14
|
expression: options.sqlExpression,
|
|
15
15
|
noteAlias: 'n',
|
|
@@ -211,6 +211,12 @@ function buildTagFilter(tagsExpression, noteAlias) {
|
|
|
211
211
|
if (!expression) {
|
|
212
212
|
return null;
|
|
213
213
|
}
|
|
214
|
+
if (isExplicitEmptyArrayLiteral(expression)) {
|
|
215
|
+
return {
|
|
216
|
+
sql: `NOT (${buildJsonArrayHasValuesSql(noteAlias, 'tags', 'tag_item')})`,
|
|
217
|
+
params: [],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
214
220
|
let ast;
|
|
215
221
|
try {
|
|
216
222
|
ast = parseTagExpression(expression);
|
|
@@ -242,16 +248,19 @@ function buildCreatedAtFilter(createdAtFrom, createdAtTo, noteAlias) {
|
|
|
242
248
|
params,
|
|
243
249
|
};
|
|
244
250
|
}
|
|
245
|
-
function buildZettelBoxFilter(zettelBoxIds, noteAlias) {
|
|
251
|
+
function buildZettelBoxFilter(zettelBoxIds, matchEmptyZettelBoxes, noteAlias) {
|
|
246
252
|
const normalizedIds = Array.from(new Set((zettelBoxIds ?? [])
|
|
247
253
|
.map((id) => id.trim())
|
|
248
254
|
.filter((id) => id.length > 0)));
|
|
249
|
-
|
|
255
|
+
const shouldMatchEmpty = Boolean(matchEmptyZettelBoxes);
|
|
256
|
+
if (normalizedIds.length === 0 && !shouldMatchEmpty) {
|
|
250
257
|
return null;
|
|
251
258
|
}
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
const clauses = [];
|
|
260
|
+
const params = [];
|
|
261
|
+
if (normalizedIds.length > 0) {
|
|
262
|
+
const placeholders = normalizedIds.map(() => '?').join(', ');
|
|
263
|
+
clauses.push(`
|
|
255
264
|
EXISTS (
|
|
256
265
|
SELECT 1
|
|
257
266
|
FROM json_each(
|
|
@@ -262,8 +271,18 @@ function buildZettelBoxFilter(zettelBoxIds, noteAlias) {
|
|
|
262
271
|
) box_item
|
|
263
272
|
WHERE TRIM(CAST(box_item.value AS TEXT)) IN (${placeholders})
|
|
264
273
|
)
|
|
265
|
-
|
|
266
|
-
params
|
|
274
|
+
`);
|
|
275
|
+
params.push(...normalizedIds);
|
|
276
|
+
}
|
|
277
|
+
if (shouldMatchEmpty) {
|
|
278
|
+
clauses.push(`NOT (${buildJsonArrayHasValuesSql(noteAlias, 'zettel_boxes', 'box_item')})`);
|
|
279
|
+
}
|
|
280
|
+
if (clauses.length === 1) {
|
|
281
|
+
return { sql: clauses[0], params };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
sql: `(${clauses.join(' OR ')})`,
|
|
285
|
+
params,
|
|
267
286
|
};
|
|
268
287
|
}
|
|
269
288
|
function parseTagExpression(expression) {
|
|
@@ -434,6 +453,32 @@ function compileTagExpression(node, noteAlias, params) {
|
|
|
434
453
|
function normalizeTagValue(value) {
|
|
435
454
|
return value.trim().toLowerCase();
|
|
436
455
|
}
|
|
456
|
+
function isExplicitEmptyArrayLiteral(value) {
|
|
457
|
+
if (!value.startsWith('[')) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const parsed = JSON.parse(value);
|
|
462
|
+
return Array.isArray(parsed) && parsed.length === 0;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function buildJsonArrayHasValuesSql(noteAlias, field, itemAlias) {
|
|
469
|
+
return `
|
|
470
|
+
EXISTS (
|
|
471
|
+
SELECT 1
|
|
472
|
+
FROM json_each(
|
|
473
|
+
CASE
|
|
474
|
+
WHEN json_valid(COALESCE(${noteAlias}.${field}, '')) THEN ${noteAlias}.${field}
|
|
475
|
+
ELSE '[]'
|
|
476
|
+
END
|
|
477
|
+
) ${itemAlias}
|
|
478
|
+
WHERE LENGTH(TRIM(CAST(${itemAlias}.value AS TEXT))) > 0
|
|
479
|
+
)
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
437
482
|
export async function getNote(db, id) {
|
|
438
483
|
return db.getOptional(`
|
|
439
484
|
SELECT
|