@dinoxx/dinox-cli 1.0.11 → 1.0.13

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
@@ -13,10 +13,11 @@
13
13
  1. 登录账号:`dino auth login ...`
14
14
  2. 同步数据:`dino sync`
15
15
  3. 搜索笔记:`dino note search "关键词"`
16
- 4. 新建笔记:`dino note create ...`
17
- 5. 新建标签:`dino tag add ...`
18
- 6. 新建卡片盒:`dino box add ...`
19
- 7. 看当前版本:`dino info`
16
+ 4. 搜索待办:`dino todo search ...`
17
+ 5. 新建笔记:`dino note create ...`
18
+ 6. 新建标签:`dino tag add ...`
19
+ 7. 新建卡片盒:`dino box add ...`
20
+ 8. 看当前版本:`dino info`
20
21
 
21
22
  ---
22
23
 
@@ -119,6 +120,38 @@ dino note search "AI" --box "Inbox"
119
120
  dino note search --sql 'type = "crawl" AND zettel_boxes IN ("Inbox","Project")'
120
121
  ```
121
122
 
123
+ 搜索待办(基于 `image_detail` 的 taskList):
124
+
125
+ ```bash
126
+ dino todo search
127
+ dino todo search "付款"
128
+ dino todo search "付款" --status uncompleted --tags "工作,财务"
129
+ dino todo search --from 2026-03-01 --to 2026-03-07 --limit 100
130
+ ```
131
+
132
+ 追加待办到现有笔记:
133
+
134
+ ```bash
135
+ dino todo append "补交报销单"
136
+ dino todo append --note-id <note-id> --task "补交报销单" --task "整理发票"
137
+ dino todo append --tasks '["补交报销单","整理发票"]'
138
+ ```
139
+
140
+ 创建新的待办笔记:
141
+
142
+ ```bash
143
+ dino todo create "补交报销单"
144
+ dino todo create --task "补交报销单" --task "整理发票" --title "本周财务待办"
145
+ dino todo create --tasks @./todo.txt
146
+ ```
147
+
148
+ 更新待办完成状态:
149
+
150
+ ```bash
151
+ dino todo update <task-id> --status completed
152
+ dino todo update <task-id> --status uncompleted
153
+ ```
154
+
122
155
  创建笔记:
123
156
 
124
157
  ```bash
@@ -526,6 +559,101 @@ sync.timeoutMs (必须为正整数)
526
559
 
527
560
  支持全局参数:`--json`
528
561
 
562
+ ## 8.10 `todo` 命令
563
+
564
+ ### `dino todo search [query]`
565
+
566
+ 搜索待办任务(任务来源:`c_note.image_detail` 中的 taskItem/taskList)。
567
+
568
+ | 参数 | 必填 | 说明 |
569
+ | --- | --- | --- |
570
+ | `[query]` | 否 | 按任务文本模糊匹配(不区分大小写)。 |
571
+
572
+ | 选项 | 必填 | 说明 |
573
+ | --- | --- | --- |
574
+ | `--status <status>` | 否 | 任务状态:`all` / `completed` / `uncompleted`(默认 `all`)。 |
575
+ | `--tags <string\|@file>` | 否 | 任务标签筛选(JSON 数组或逗号/换行分隔),任务需包含全部标签。 |
576
+ | `--from <date>` | 否 | 任务时间范围起点(`YYYY-MM-DD` 或 ISO datetime)。 |
577
+ | `--to <date>` | 否 | 任务时间范围终点(`YYYY-MM-DD` 或 ISO datetime)。 |
578
+ | `--days <n>` | 否 | 最近 N 天(不能与 `--from/--to` 同时使用)。 |
579
+ | `--limit <n>` | 否 | 返回任务数上限(默认 `50`,最大 `500`)。 |
580
+ | `--scan-limit <n>` | 否 | 扫描 `c_note` 行数上限(默认 `200`,最大 `5000`)。 |
581
+ | `--include-deleted` | 否 | 包含软删除笔记中的任务。 |
582
+
583
+ 输出结构说明(AI 友好):
584
+
585
+ - 顶层包含 `meta`(查询条件回显、返回数量、是否截断)与 `tasks`(任务数组)。
586
+ - 每条任务包含 `task_key`、`note_id`、`note_title`、`status`、层级关系与时间字段。
587
+
588
+ 支持全局参数:`--offline`、`--sync-timeout`
589
+
590
+ ### `dino todo append [task]`
591
+
592
+ 向笔记末尾追加一个或多个待办任务。
593
+
594
+ 说明:
595
+
596
+ - `content_json` 是真源,`image_detail` 会从更新后的 `content_json` 派生回写。
597
+ - 若不传 `--note-id`,默认选择 `image_detail` 非空且不为 `[]` 的最新笔记(按 `created_at` 降序)。
598
+ - 若笔记末尾已有 `taskList`,会并入该 `taskList`;否则在末尾创建新的 `taskList`。
599
+
600
+ | 参数 | 必填 | 说明 |
601
+ | --- | --- | --- |
602
+ | `[task]` | 否 | 单个任务文本。 |
603
+
604
+ | 选项 | 必填 | 说明 |
605
+ | --- | --- | --- |
606
+ | `--task <text>` | 否 | 任务文本(可重复传入)。 |
607
+ | `--tasks <string\|@file>` | 否 | 任务列表(JSON 数组或逗号/换行分隔)。 |
608
+ | `--note-id <id>` | 否 | 指定目标笔记 ID。 |
609
+
610
+ 说明:`[task]`、`--task`、`--tasks` 至少提供一个。
611
+
612
+ 支持全局参数:`--offline`、`--sync-timeout`
613
+
614
+ ### `dino todo create [task]`
615
+
616
+ 创建一篇新笔记,并写入一个或多个待办任务。
617
+
618
+ 说明:
619
+
620
+ - `content_json` 是真源,`image_detail` 从 `content_json` 派生回写。
621
+ - 默认自动生成总结性标题(可用 `--title` 覆盖)。
622
+
623
+ | 参数 | 必填 | 说明 |
624
+ | --- | --- | --- |
625
+ | `[task]` | 否 | 单个任务文本。 |
626
+
627
+ | 选项 | 必填 | 说明 |
628
+ | --- | --- | --- |
629
+ | `--task <text>` | 否 | 任务文本(可重复传入)。 |
630
+ | `--tasks <string\|@file>` | 否 | 任务列表(JSON 数组或逗号/换行分隔)。 |
631
+ | `--title <string>` | 否 | 自定义笔记标题。 |
632
+
633
+ 说明:`[task]`、`--task`、`--tasks` 至少提供一个。
634
+
635
+ 支持全局参数:`--offline`、`--sync-timeout`
636
+
637
+ ### `dino todo update <taskId> --status <status>`
638
+
639
+ 更新任务完成状态(按 taskId 定位)。
640
+
641
+ 说明:
642
+
643
+ - 只接受合法状态值:`completed` / `uncompleted`(兼容 `done` / `undone` / `true` / `false` / `1` / `0`)。
644
+ - `content_json` 是真源,更新后会同步回写 `image_detail`、`content_md`、`content_text`。
645
+ - 若同一个 taskId 出现在多篇笔记,会报错并拒绝更新(避免误改)。
646
+
647
+ | 参数 | 必填 | 说明 |
648
+ | --- | --- | --- |
649
+ | `<taskId>` | 是 | 任务 ID。 |
650
+
651
+ | 选项 | 必填 | 说明 |
652
+ | --- | --- | --- |
653
+ | `--status <status>` | 是 | 目标状态。 |
654
+
655
+ 支持全局参数:`--offline`、`--sync-timeout`
656
+
529
657
  ---
530
658
 
531
659
  ## 9. 查看帮助
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { registerPromptCommands } from './commands/prompt/index.js';
8
8
  import { registerSyncCommand } from './commands/sync.js';
9
9
  import { registerUpdateCommand } from './commands/update.js';
10
10
  import { registerTagsCommand } from './commands/tags/index.js';
11
+ import { registerTodoCommands } from './commands/todo/index.js';
11
12
  import { getPackageVersion } from './utils/version.js';
12
13
  export function createCli() {
13
14
  const program = new Command();
@@ -29,6 +30,7 @@ export function createCli() {
29
30
  registerUpdateCommand(program);
30
31
  registerPromptCommands(program);
31
32
  registerTagsCommand(program);
33
+ registerTodoCommands(program);
32
34
  registerNoteCommands(program);
33
35
  return program;
34
36
  }
@@ -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, e.g. \'(work OR life) AND NOT archived\'')
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 comma/newline-separated)')
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 zettelBoxNames = boxInput ? await parseStringListInput(boxInput, '--box') : [];
118
- const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, zettelBoxNames);
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);
@@ -19,6 +19,7 @@ type SearchNotesOptions = {
19
19
  createdAtFrom?: string;
20
20
  createdAtTo?: string;
21
21
  zettelBoxIds?: string[];
22
+ matchEmptyZettelBoxes?: boolean;
22
23
  sqlExpression?: string;
23
24
  };
24
25
  export type SearchedNote = {
@@ -60,6 +61,7 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
60
61
  tags: string[];
61
62
  zettelBoxIds: string[];
62
63
  userId: string;
64
+ imageDetail?: unknown[];
63
65
  }): Promise<{
64
66
  id: string;
65
67
  }>;
@@ -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
- if (normalizedIds.length === 0) {
255
+ const shouldMatchEmpty = Boolean(matchEmptyZettelBoxes);
256
+ if (normalizedIds.length === 0 && !shouldMatchEmpty) {
250
257
  return null;
251
258
  }
252
- const placeholders = normalizedIds.map(() => '?').join(', ');
253
- return {
254
- sql: `
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: normalizedIds,
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
@@ -526,6 +571,7 @@ export async function createNote(db, input) {
526
571
  const contentJson = JSON.stringify(input.contentJson);
527
572
  const tags = JSON.stringify(input.tags);
528
573
  const zettelBoxes = JSON.stringify(input.zettelBoxIds);
574
+ const imageDetail = JSON.stringify(Array.isArray(input.imageDetail) ? input.imageDetail : []);
529
575
  const columns = await db.getAll('PRAGMA table_info(c_note)', []);
530
576
  const columnNames = new Set(columns
531
577
  .map((row) => row.name?.trim().toLowerCase() ?? '')
@@ -583,7 +629,7 @@ export async function createNote(db, input) {
583
629
  ['files', '[]'],
584
630
  ['audios', '[]'],
585
631
  ['audio_detail', '{}'],
586
- ['image_detail', '{}'],
632
+ ['image_detail', imageDetail],
587
633
  ['source', ''],
588
634
  ['resource_id', null],
589
635
  ['note_ids', '[]'],
@@ -0,0 +1,30 @@
1
+ type JsonRecord = Record<string, unknown>;
2
+ type TodoTaskItemNode = {
3
+ type: 'taskItem';
4
+ id: string;
5
+ checked: boolean;
6
+ attrs: {
7
+ id: string;
8
+ checked: boolean;
9
+ [key: string]: unknown;
10
+ };
11
+ content: unknown[];
12
+ };
13
+ export type ApplyTaskCheckedStatusResult = {
14
+ nextDoc: JsonRecord;
15
+ taskOrder: string[];
16
+ matchedIndices: number[];
17
+ changedCount: number;
18
+ };
19
+ export declare function parseTiptapDocFromDb(rawContentJson: string | null): JsonRecord;
20
+ export declare function createTodoDocFromTasks(taskTexts: string[]): JsonRecord;
21
+ export declare function appendTasksToDoc(doc: JsonRecord, taskTexts: string[]): JsonRecord;
22
+ export declare function deriveImageDetailFromContentJson(doc: JsonRecord): TodoTaskItemNode[];
23
+ export declare function buildAppendMarkdown(existingMarkdown: string | null, taskTexts: string[]): string;
24
+ export declare function buildCreateMarkdown(taskTexts: string[]): string;
25
+ export declare function buildContentTextPreview(contentMd: string): string;
26
+ export declare function buildSummaryTitle(taskTexts: string[]): string;
27
+ export declare function normalizeTaskTexts(values: string[]): string[];
28
+ export declare function applyTaskCheckedStatus(doc: JsonRecord, taskId: string, checked: boolean): ApplyTaskCheckedStatusResult;
29
+ export declare function patchTaskMarkdownByOrder(contentMd: string | null, taskOrder: string[], matchedIndices: number[], checked: boolean): string;
30
+ export {};