@dinoxx/dinox-cli 1.0.1 → 1.0.3

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 CHANGED
@@ -115,6 +115,8 @@ dino auth status
115
115
  dino note search "AI"
116
116
  dino note search "AI" --days 7
117
117
  dino note search "AI" --from 2026-02-01 --to 2026-02-28
118
+ dino note search "AI" --box "Inbox"
119
+ dino note search --sql 'type = "crawl" AND zettel_boxes IN ("Inbox","Project")'
118
120
  ```
119
121
 
120
122
  创建笔记:
@@ -174,6 +176,7 @@ dino box add --name "Inbox" --description "用于存放待整理的想法和资
174
176
 
175
177
  ```bash
176
178
  dino prompt list
179
+ dino prompt add --name "周报助手" --cmd "请基于本周笔记输出一份简洁周报"
177
180
  ```
178
181
 
179
182
  ## 5.5 配置
@@ -207,11 +210,6 @@ dino config set sync.timeoutMs 20000
207
210
  1. macOS: `~/Library/Application Support/dinox/config.json`
208
211
  2. Windows: `%APPDATA%\dinox\config.json`
209
212
 
210
- 本地数据库:
211
-
212
- 1. macOS: `~/Library/Application Support/dinox/powersync.sqlite`
213
- 2. Windows: `%LOCALAPPDATA%\dinox\powersync.sqlite`
214
-
215
213
  ## 6.2 多行命令写法不同
216
214
 
217
215
  macOS / Linux(bash/zsh)用 `\` 续行:
@@ -269,7 +267,244 @@ dino box add "你的卡片盒"
269
267
 
270
268
  ---
271
269
 
272
- ## 8. 查看帮助
270
+ ## 8. 完整命令参数参考
271
+
272
+ ## 8.1 全局参数(适用于 CLI)
273
+
274
+ 以下参数可放在命令前后使用,例如:`dino --json auth status`。
275
+
276
+ | 参数 | 类型 | 说明 |
277
+ | --- | --- | --- |
278
+ | `--json` | flag | 输出 machine-readable YAML(仅被支持该参数的命令读取)。 |
279
+ | `--offline` | flag | 跳过 connect/sync,仅用本地缓存(仅被支持该参数的命令读取)。 |
280
+ | `--sync-timeout <ms>` | integer | 覆盖连接/同步超时毫秒,必须是正整数。 |
281
+ | `--verbose` | flag | 预留 verbose 开关。 |
282
+
283
+ ## 8.2 `auth` 命令
284
+
285
+ ### `dino auth login <authorization>`
286
+
287
+ 保存授权信息并验证连接。
288
+
289
+ | 参数 | 必填 | 说明 |
290
+ | --- | --- | --- |
291
+ | `<authorization>` | 是 | 完整 Authorization 头值,例如:`"Bearer <token>"` |
292
+
293
+ 支持全局参数:`--json`、`--sync-timeout`
294
+
295
+ ### `dino auth logout`
296
+
297
+ 清理已保存登录信息。
298
+
299
+ | 选项 | 必填 | 说明 |
300
+ | --- | --- | --- |
301
+ | `--clear-local-db` | 否 | 同时删除本地 SQLite 数据库。 |
302
+
303
+ ### `dino auth status`
304
+
305
+ 查看当前登录与同步状态。
306
+
307
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
308
+
309
+ ## 8.3 `sync` 命令
310
+
311
+ ### `dino sync`
312
+
313
+ 连接并执行同步。
314
+
315
+ 支持全局参数:`--json`、`--sync-timeout`
316
+
317
+ ## 8.4 `note` 命令
318
+
319
+ ### `dino note search [query]`
320
+
321
+ 按关键词/标签/创建时间搜索笔记。
322
+
323
+ | 参数 | 必填 | 说明 |
324
+ | --- | --- | --- |
325
+ | `[query]` | 否 | 搜索关键词。可为空。 |
326
+
327
+ | 选项 | 必填 | 说明 |
328
+ | --- | --- | --- |
329
+ | `--tags <expr>` | 否 | 标签表达式,支持 `AND` / `OR` / `NOT` 与括号。 |
330
+ | `--from <date>` | 否 | 创建时间起点,支持 `YYYY-MM-DD` 或 ISO datetime。 |
331
+ | `--to <date>` | 否 | 创建时间终点,支持 `YYYY-MM-DD` 或 ISO datetime。 |
332
+ | `--days <n>` | 否 | 最近 N 天(按 `created_at`),不能与 `--from/--to` 同时使用。 |
333
+ | `--box <string\|@file>` | 否 | 按卡片盒名称筛选(支持 JSON 数组或逗号/换行分隔);会自动解析为 box id 查询。 |
334
+ | `--sql <expr>` | 否 | SQL 风格条件表达式;仅支持字段 `id/content_md/summary/tags/zettel_boxes/created_at/type`,其中 `zettel_boxes` 按名称自动转 id。只支持只读 WHERE 条件(禁止 `INSERT/UPDATE/DELETE`、注释和多语句)。 |
335
+ | `--include-deleted` | 否 | 包含软删除笔记。 |
336
+
337
+ 支持全局参数:`--offline`、`--sync-timeout`
338
+
339
+ ### `dino note get <id>`
340
+
341
+ 按 ID 获取笔记基础信息。
342
+
343
+ | 参数 | 必填 | 说明 |
344
+ | --- | --- | --- |
345
+ | `<id>` | 是 | 笔记 ID。 |
346
+
347
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
348
+
349
+ ### `dino note detail <id>`
350
+
351
+ 按 ID 获取笔记完整详情。
352
+
353
+ | 参数 | 必填 | 说明 |
354
+ | --- | --- | --- |
355
+ | `<id>` | 是 | 笔记 ID。 |
356
+
357
+ 支持全局参数:`--offline`、`--sync-timeout`
358
+
359
+ ### `dino note create --title <string> --content <string|@file> [options]`
360
+
361
+ 创建笔记。
362
+
363
+ | 选项 | 必填 | 说明 |
364
+ | --- | --- | --- |
365
+ | `--title <string>` | 是 | 笔记标题。 |
366
+ | `--content <string|@file>` | 是 | Markdown 内容,可传 `@文件路径`。 |
367
+ | `--type <note\|crawl>` | 否 | 笔记类型,默认 `crawl`。 |
368
+ | `--tags <string\|@file>` | 否 | 标签列表(JSON 数组或逗号/换行分隔)。 |
369
+ | `--zettel_boxes <string\|@file>` | 否 | 卡片盒名称列表(JSON 数组或逗号/换行分隔)。 |
370
+
371
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
372
+
373
+ ### `dino note delete <id>`
374
+
375
+ 软删除笔记(`is_del=1`)。
376
+
377
+ | 参数 | 必填 | 说明 |
378
+ | --- | --- | --- |
379
+ | `<id>` | 是 | 笔记 ID。 |
380
+
381
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
382
+
383
+ ## 8.5 `tag` 命令
384
+
385
+ ### `dino tag list`
386
+
387
+ 列出标签。
388
+
389
+ | 选项 | 必填 | 说明 |
390
+ | --- | --- | --- |
391
+ | `--json` | 否 | 输出 machine-readable YAML。 |
392
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
393
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
394
+
395
+ ### `dino tag add [name] [options]`
396
+
397
+ 新增标签。
398
+
399
+ | 参数 | 必填 | 说明 |
400
+ | --- | --- | --- |
401
+ | `[name]` | 否 | 标签名/路径(支持斜杠层级)。 |
402
+
403
+ | 选项 | 必填 | 说明 |
404
+ | --- | --- | --- |
405
+ | `--name <string>` | 否 | 标签名/路径,可替代位置参数 `[name]`。 |
406
+ | `--emoji <string>` | 否 | 标签 emoji。 |
407
+ | `--json` | 否 | 输出 machine-readable YAML。 |
408
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
409
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
410
+
411
+ 说明:`[name]` 与 `--name` 至少提供一个;若两者都提供且值不同会报错。
412
+
413
+ ## 8.6 `box` 命令
414
+
415
+ ### `dino box list`
416
+
417
+ 列出卡片盒。
418
+
419
+ | 选项 | 必填 | 说明 |
420
+ | --- | --- | --- |
421
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
422
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
423
+
424
+ ### `dino box add [name] [options]`
425
+
426
+ 新增卡片盒。
427
+
428
+ | 参数 | 必填 | 说明 |
429
+ | --- | --- | --- |
430
+ | `[name]` | 否 | 卡片盒名称。 |
431
+
432
+ | 选项 | 必填 | 说明 |
433
+ | --- | --- | --- |
434
+ | `--name <string>` | 否 | 卡片盒名称,可替代位置参数 `[name]`。 |
435
+ | `--description <string>` | 否 | 用途说明(可帮助 AI 路由笔记)。 |
436
+ | `--color <string>` | 否 | 卡片盒颜色。 |
437
+ | `--json` | 否 | 输出 machine-readable YAML。 |
438
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
439
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
440
+
441
+ 说明:`[name]` 与 `--name` 至少提供一个;若两者都提供且值不同会报错。
442
+
443
+ ## 8.7 `prompt` 命令
444
+
445
+ ### `dino prompt list`
446
+
447
+ 列出 Prompt(`name` 与 `cmd`)。
448
+
449
+ | 选项 | 必填 | 说明 |
450
+ | --- | --- | --- |
451
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
452
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
453
+
454
+ ### `dino prompt add [options]`
455
+
456
+ 新增 Prompt(写入 `c_cmd`)。
457
+
458
+ | 选项 | 必填 | 说明 |
459
+ | --- | --- | --- |
460
+ | `--name <string>` | 是 | Prompt 名称。 |
461
+ | `--cmd <string>` | 是 | Prompt 指令内容。 |
462
+ | `--json` | 否 | 输出 machine-readable YAML。 |
463
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
464
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
465
+
466
+ ## 8.8 `config` 命令
467
+
468
+ ### `dino config get [key]`
469
+
470
+ 读取配置(输出 YAML)。
471
+
472
+ | 参数 | 必填 | 说明 |
473
+ | --- | --- | --- |
474
+ | `[key]` | 否 | 配置键(点路径);不传则返回全部配置。 |
475
+
476
+ 可读 key:
477
+
478
+ ```text
479
+ sync.timeoutMs
480
+ ```
481
+
482
+ ### `dino config set <key> <value>`
483
+
484
+ 写入配置。
485
+
486
+ | 参数 | 必填 | 说明 |
487
+ | --- | --- | --- |
488
+ | `<key>` | 是 | 配置键(点路径)。 |
489
+ | `<value>` | 是 | 配置值。 |
490
+
491
+ 可写 key:
492
+
493
+ ```text
494
+ sync.timeoutMs (必须为正整数)
495
+ ```
496
+
497
+ ## 8.9 `info` 命令
498
+
499
+ ### `dino info`
500
+
501
+ 显示 CLI 版本信息。
502
+
503
+ 支持全局参数:`--json`
504
+
505
+ ---
506
+
507
+ ## 9. 查看帮助
273
508
 
274
509
  随时可以看帮助:
275
510
 
@@ -8,6 +8,7 @@ import { printYaml } from '../../utils/output.js';
8
8
  import { refreshAndPersistAuthIdentity } from '../../auth/userInfo.js';
9
9
  import { connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js';
10
10
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
11
+ import { runSingleSyncPass } from '../../powersync/syncPass.js';
11
12
  function parseSyncTimeoutMs(value) {
12
13
  if (typeof value !== 'string') {
13
14
  return undefined;
@@ -18,6 +19,12 @@ function parseSyncTimeoutMs(value) {
18
19
  }
19
20
  return Math.trunc(parsed);
20
21
  }
22
+ async function removeSqliteArtifacts(dbPath) {
23
+ const artifacts = ['', '-wal', '-shm'];
24
+ for (const suffix of artifacts) {
25
+ await fs.rm(`${dbPath}${suffix}`, { force: true }).catch(() => undefined);
26
+ }
27
+ }
21
28
  export function registerAuthCommands(program) {
22
29
  const auth = program
23
30
  .command('auth')
@@ -26,8 +33,7 @@ export function registerAuthCommands(program) {
26
33
  .command('login')
27
34
  .description('Save authorization token and verify PowerSync connectivity')
28
35
  .argument('<authorization>', 'full Authorization header value (e.g. "Bearer <token>")')
29
- .option('--no-verify', 'skip credential exchange and initial sync')
30
- .action(async (authorization, options, command) => {
36
+ .action(async (authorization, _options, command) => {
31
37
  const auth = authorization?.trim();
32
38
  if (!auth) {
33
39
  throw new DinoxError('Authorization token is required');
@@ -39,20 +45,6 @@ export function registerAuthCommands(program) {
39
45
  const globals = command.optsWithGlobals?.() ?? {};
40
46
  const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? resolved.sync.timeoutMs;
41
47
  const jsonOutput = Boolean(globals.json);
42
- if (!options.verify) {
43
- const result = {
44
- ok: true,
45
- verified: false,
46
- authorization: redactAuthorization(auth),
47
- };
48
- if (jsonOutput) {
49
- printYaml(result);
50
- }
51
- else {
52
- console.log('Login saved (verification skipped).');
53
- }
54
- return;
55
- }
56
48
  // 1) Token exchange sanity check.
57
49
  await fetchCredentialsWithAuthorization({
58
50
  authorization: auth,
@@ -68,6 +60,7 @@ export function registerAuthCommands(program) {
68
60
  timeoutMs,
69
61
  });
70
62
  try {
63
+ await runSingleSyncPass(db, timeoutMs);
71
64
  const status = getStatusSnapshot(db);
72
65
  const result = {
73
66
  ok: true,
@@ -110,7 +103,7 @@ export function registerAuthCommands(program) {
110
103
  await saveConfig(config);
111
104
  if (options.clearLocalDb) {
112
105
  const dbPath = getPowerSyncDbPath();
113
- await fs.rm(dbPath, { force: true }).catch(() => undefined);
106
+ await removeSqliteArtifacts(dbPath);
114
107
  }
115
108
  });
116
109
  auth
@@ -1,2 +1,3 @@
1
1
  import { Command } from 'commander';
2
+ export declare function sanitizeConfigForDisplay(config: unknown): unknown;
2
3
  export declare function registerConfigCommands(program: Command): void;
@@ -1,10 +1,9 @@
1
1
  import { isConfigKey, isFixedConfigKey, isReadableConfigKey } from '../../config/keys.js';
2
- import { getConfigValue, loadConfig, setConfigValue } from '../../config/store.js';
3
- import { resolveConfig } from '../../config/resolve.js';
2
+ import { getConfigValue, setConfigValue } from '../../config/store.js';
4
3
  import { redactAuthorization } from '../../utils/redact.js';
5
4
  import { DinoxError } from '../../utils/errors.js';
6
5
  import { printYaml } from '../../utils/output.js';
7
- function redactConfigForDisplay(config) {
6
+ export function sanitizeConfigForDisplay(config) {
8
7
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
9
8
  return config;
10
9
  }
@@ -13,16 +12,9 @@ function redactConfigForDisplay(config) {
13
12
  if (auth && typeof auth === 'object') {
14
13
  auth.authorization = redactAuthorization(auth.authorization);
15
14
  }
15
+ delete clone.powersync;
16
16
  return clone;
17
17
  }
18
- function getByPath(value, keyPath) {
19
- return keyPath.split('.').reduce((current, segment) => {
20
- if (!current || typeof current !== 'object') {
21
- return undefined;
22
- }
23
- return current[segment];
24
- }, value);
25
- }
26
18
  export function registerConfigCommands(program) {
27
19
  const configCmd = program.command('config').description('Read and write Dinox configuration');
28
20
  configCmd
@@ -33,13 +25,8 @@ export function registerConfigCommands(program) {
33
25
  if (key && !isReadableConfigKey(key)) {
34
26
  throw new DinoxError(`Unknown config key: ${key}`);
35
27
  }
36
- if (key && isFixedConfigKey(key)) {
37
- const resolved = resolveConfig(await loadConfig());
38
- printYaml(getByPath(resolved, key));
39
- return;
40
- }
41
28
  const value = await getConfigValue(key);
42
- const redacted = key ? value : redactConfigForDisplay(value);
29
+ const redacted = key ? value : sanitizeConfigForDisplay(value);
43
30
  printYaml(redacted);
44
31
  });
45
32
  configCmd
@@ -8,7 +8,7 @@ import { printYaml } from '../../utils/output.js';
8
8
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
9
9
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
10
10
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
11
- import { createNote, getNote, getNoteDetail, resolveZettelBoxIdsForCreate, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
11
+ import { createNote, getNote, getNoteDetail, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
12
12
  import { parseCreatedAtRange } from './searchTime.js';
13
13
  function parseSyncTimeoutMs(value) {
14
14
  if (typeof value !== 'string') {
@@ -78,6 +78,8 @@ export function registerNoteCommands(program) {
78
78
  .option('--from <date>', 'created_at start (YYYY-MM-DD or ISO datetime)')
79
79
  .option('--to <date>', 'created_at end (YYYY-MM-DD or ISO datetime)')
80
80
  .option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
81
+ .option('--box <string|@file>', 'zettel box names (JSON array or comma/newline-separated)')
82
+ .option('--sql <expr>', 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type')
81
83
  .option('--include-deleted', 'include soft-deleted notes')
82
84
  .action(async (query, options, command) => {
83
85
  const globals = command.optsWithGlobals?.() ?? {};
@@ -94,11 +96,17 @@ export function registerNoteCommands(program) {
94
96
  to: options.to,
95
97
  days: options.days,
96
98
  });
99
+ const boxInput = typeof options.box === 'string' ? options.box : '';
100
+ const zettelBoxNames = boxInput ? await parseStringListInput(boxInput, '--box') : [];
101
+ const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, zettelBoxNames);
102
+ const sqlExpression = typeof options.sql === 'string' ? options.sql.trim() : '';
97
103
  const rows = await searchNotes(db, keyword, {
98
104
  includeDeleted: Boolean(options.includeDeleted),
99
105
  tagsExpression: tagsExpression || undefined,
100
106
  createdAtFrom,
101
107
  createdAtTo,
108
+ zettelBoxIds,
109
+ sqlExpression: sqlExpression || undefined,
102
110
  });
103
111
  printYaml(rows);
104
112
  }
@@ -18,6 +18,8 @@ type SearchNotesOptions = {
18
18
  tagsExpression?: string;
19
19
  createdAtFrom?: string;
20
20
  createdAtTo?: string;
21
+ zettelBoxIds?: string[];
22
+ sqlExpression?: string;
21
23
  };
22
24
  export type SearchedNote = {
23
25
  id: string;
@@ -66,5 +68,6 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
66
68
  }>;
67
69
  export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
68
70
  export declare function resolveZettelBoxIdsForCreate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
71
+ export declare function resolveZettelBoxIdsForSearch(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
69
72
  export declare function softDeleteNote(db: AbstractPowerSyncDatabase, id: string): Promise<void>;
70
73
  export {};
@@ -3,16 +3,23 @@ import { nowIsoUtc } from '../../utils/time.js';
3
3
  import { tokenizeWithJieba } from '../../utils/tokenize.js';
4
4
  import { listBoxes } from '../boxes/repo.js';
5
5
  import { listTags } from '../tags/repo.js';
6
+ import { buildNoteSearchSqlFilter } from './searchSql.js';
6
7
  export async function searchNotes(db, query, options) {
7
8
  const normalizedQuery = query.trim();
8
9
  const tagFilter = buildTagFilter(options.tagsExpression, 'n');
9
10
  const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
11
+ const zettelBoxFilter = buildZettelBoxFilter(options.zettelBoxIds, 'n');
12
+ const sqlFilter = await buildNoteSearchSqlFilter({
13
+ expression: options.sqlExpression,
14
+ noteAlias: 'n',
15
+ resolveZettelBoxNames: (names) => resolveZettelBoxIdsForSearch(db, names),
16
+ });
10
17
  let rows = [];
11
18
  if (!options.includeDeleted) {
12
19
  const ftsMatch = buildFtsMatchQuery(normalizedQuery);
13
20
  if (ftsMatch) {
14
21
  try {
15
- rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter);
22
+ rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter, zettelBoxFilter, sqlFilter);
16
23
  return enrichSearchRows(db, rows);
17
24
  }
18
25
  catch (error) {
@@ -53,6 +60,14 @@ export async function searchNotes(db, query, options) {
53
60
  whereClauses.push(createdAtFilter.sql);
54
61
  params.push(...createdAtFilter.params);
55
62
  }
63
+ if (zettelBoxFilter) {
64
+ whereClauses.push(zettelBoxFilter.sql);
65
+ params.push(...zettelBoxFilter.params);
66
+ }
67
+ if (sqlFilter) {
68
+ whereClauses.push(sqlFilter.sql);
69
+ params.push(...sqlFilter.params);
70
+ }
56
71
  rows = await db.getAll(`
57
72
  SELECT
58
73
  n.id,
@@ -69,7 +84,7 @@ export async function searchNotes(db, query, options) {
69
84
  `, params);
70
85
  return enrichSearchRows(db, rows);
71
86
  }
72
- async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter) {
87
+ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, zettelBoxFilter, sqlFilter) {
73
88
  const whereClauses = ['(n.is_del IS NULL OR n.is_del = 0)', 'note_local_fts MATCH ?'];
74
89
  const params = [matchQuery];
75
90
  if (tagFilter) {
@@ -80,6 +95,14 @@ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter) {
80
95
  whereClauses.push(createdAtFilter.sql);
81
96
  params.push(...createdAtFilter.params);
82
97
  }
98
+ if (zettelBoxFilter) {
99
+ whereClauses.push(zettelBoxFilter.sql);
100
+ params.push(...zettelBoxFilter.params);
101
+ }
102
+ if (sqlFilter) {
103
+ whereClauses.push(sqlFilter.sql);
104
+ params.push(...sqlFilter.params);
105
+ }
83
106
  return db.getAll(`
84
107
  SELECT
85
108
  n.id,
@@ -214,6 +237,30 @@ function buildCreatedAtFilter(createdAtFrom, createdAtTo, noteAlias) {
214
237
  params,
215
238
  };
216
239
  }
240
+ function buildZettelBoxFilter(zettelBoxIds, noteAlias) {
241
+ const normalizedIds = Array.from(new Set((zettelBoxIds ?? [])
242
+ .map((id) => id.trim())
243
+ .filter((id) => id.length > 0)));
244
+ if (normalizedIds.length === 0) {
245
+ return null;
246
+ }
247
+ const placeholders = normalizedIds.map(() => '?').join(', ');
248
+ return {
249
+ sql: `
250
+ EXISTS (
251
+ SELECT 1
252
+ FROM json_each(
253
+ CASE
254
+ WHEN json_valid(COALESCE(${noteAlias}.zettel_boxes, '')) THEN ${noteAlias}.zettel_boxes
255
+ ELSE '[]'
256
+ END
257
+ ) box_item
258
+ WHERE TRIM(CAST(box_item.value AS TEXT)) IN (${placeholders})
259
+ )
260
+ `,
261
+ params: normalizedIds,
262
+ };
263
+ }
217
264
  function parseTagExpression(expression) {
218
265
  const tokens = tokenizeTagExpression(expression);
219
266
  if (tokens.length === 0) {
@@ -537,6 +584,13 @@ export async function createNote(db, input) {
537
584
  function normalizeLookupKey(value) {
538
585
  return value.trim().toLowerCase();
539
586
  }
587
+ function buildZettelBoxRetryQuestion(intent, type) {
588
+ const command = intent === 'search' ? 'dino note search' : 'dino note create';
589
+ if (type === 'ambiguous') {
590
+ return `Do you want to specify unique box names (or IDs) and retry \`${command}\`?`;
591
+ }
592
+ return `Do you want to add these missing boxes and retry \`${command}\`?`;
593
+ }
540
594
  export async function validateTagsForCreate(db, requestedTags) {
541
595
  if (requestedTags.length === 0) {
542
596
  return [];
@@ -580,6 +634,12 @@ export async function validateTagsForCreate(db, requestedTags) {
580
634
  return resolved;
581
635
  }
582
636
  export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
637
+ return resolveZettelBoxIdsByName(db, requestedNames, 'create');
638
+ }
639
+ export async function resolveZettelBoxIdsForSearch(db, requestedNames) {
640
+ return resolveZettelBoxIdsByName(db, requestedNames, 'search');
641
+ }
642
+ async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
583
643
  if (requestedNames.length === 0) {
584
644
  return [];
585
645
  }
@@ -642,7 +702,7 @@ export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
642
702
  type: 'ambiguous_zettel_boxes',
643
703
  ambiguous,
644
704
  availableNamesSample: availableNames.slice(0, 20),
645
- question: 'Do you want to specify unique box names (or IDs) and retry `dino note create`?',
705
+ question: buildZettelBoxRetryQuestion(intent, 'ambiguous'),
646
706
  },
647
707
  });
648
708
  }
@@ -653,7 +713,7 @@ export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
653
713
  missing,
654
714
  availableCount: availableNames.length,
655
715
  availableNamesSample: availableNames.slice(0, 20),
656
- question: 'Do you want to add these missing boxes and retry `dino note create`?',
716
+ question: buildZettelBoxRetryQuestion(intent, 'missing'),
657
717
  },
658
718
  });
659
719
  }
@@ -0,0 +1,11 @@
1
+ export type SqlFilter = {
2
+ sql: string;
3
+ params: string[];
4
+ };
5
+ type BuildNoteSearchSqlFilterOptions = {
6
+ expression?: string;
7
+ noteAlias: string;
8
+ resolveZettelBoxNames: (names: string[]) => Promise<string[]>;
9
+ };
10
+ export declare function buildNoteSearchSqlFilter(options: BuildNoteSearchSqlFilterOptions): Promise<SqlFilter | null>;
11
+ export {};