@dinoxx/dinox-cli 1.0.12 → 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,7 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
61
61
  tags: string[];
62
62
  zettelBoxIds: string[];
63
63
  userId: string;
64
+ imageDetail?: unknown[];
64
65
  }): Promise<{
65
66
  id: string;
66
67
  }>;
@@ -571,6 +571,7 @@ export async function createNote(db, input) {
571
571
  const contentJson = JSON.stringify(input.contentJson);
572
572
  const tags = JSON.stringify(input.tags);
573
573
  const zettelBoxes = JSON.stringify(input.zettelBoxIds);
574
+ const imageDetail = JSON.stringify(Array.isArray(input.imageDetail) ? input.imageDetail : []);
574
575
  const columns = await db.getAll('PRAGMA table_info(c_note)', []);
575
576
  const columnNames = new Set(columns
576
577
  .map((row) => row.name?.trim().toLowerCase() ?? '')
@@ -628,7 +629,7 @@ export async function createNote(db, input) {
628
629
  ['files', '[]'],
629
630
  ['audios', '[]'],
630
631
  ['audio_detail', '{}'],
631
- ['image_detail', '{}'],
632
+ ['image_detail', imageDetail],
632
633
  ['source', ''],
633
634
  ['resource_id', null],
634
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 {};
@@ -0,0 +1,325 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { generateId } from '../../utils/id.js';
3
+ import { stripMarkdown } from '../../utils/text.js';
4
+ const CONTENT_TEXT_PREVIEW_LIMIT = 300;
5
+ const DEFAULT_TITLE_PREFIX = '待办清单';
6
+ export function parseTiptapDocFromDb(rawContentJson) {
7
+ const raw = (rawContentJson ?? '').trim();
8
+ if (!raw) {
9
+ throw new DinoxError('Invalid note content_json: empty value');
10
+ }
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(raw);
14
+ }
15
+ catch (error) {
16
+ throw new DinoxError('Invalid note content_json: malformed JSON', { cause: error });
17
+ }
18
+ return assertTiptapDoc(parsed);
19
+ }
20
+ export function createTodoDocFromTasks(taskTexts) {
21
+ const tasks = buildTaskItemNodes(taskTexts);
22
+ return {
23
+ type: 'doc',
24
+ content: [
25
+ {
26
+ type: 'taskList',
27
+ content: tasks,
28
+ },
29
+ ],
30
+ };
31
+ }
32
+ export function appendTasksToDoc(doc, taskTexts) {
33
+ const nextDoc = cloneJson(doc);
34
+ const tasks = buildTaskItemNodes(taskTexts);
35
+ const content = ensureDocContentArray(nextDoc);
36
+ const lastIndex = content.length - 1;
37
+ const lastNode = lastIndex >= 0 ? content[lastIndex] : null;
38
+ if (isTaskListNode(lastNode)) {
39
+ const listContent = ensureNodeContentArray(lastNode);
40
+ listContent.push(...tasks);
41
+ return nextDoc;
42
+ }
43
+ content.push({
44
+ type: 'taskList',
45
+ content: tasks,
46
+ });
47
+ return nextDoc;
48
+ }
49
+ export function deriveImageDetailFromContentJson(doc) {
50
+ const root = assertTiptapDoc(doc);
51
+ const rootContent = ensureNodeContentArray(root);
52
+ const result = [];
53
+ const nodeStack = [];
54
+ const insideTaskStack = [];
55
+ for (let index = rootContent.length - 1; index >= 0; index -= 1) {
56
+ nodeStack.push(rootContent[index]);
57
+ insideTaskStack.push(false);
58
+ }
59
+ while (nodeStack.length > 0) {
60
+ const current = nodeStack.pop();
61
+ const insideTaskItem = insideTaskStack.pop() ?? false;
62
+ if (!current || typeof current !== 'object') {
63
+ continue;
64
+ }
65
+ const node = current;
66
+ const type = typeof node.type === 'string' ? node.type : '';
67
+ const content = Array.isArray(node.content) ? node.content : [];
68
+ if (type === 'taskItem' && !insideTaskItem) {
69
+ result.push(normalizeTaskItemNode(node));
70
+ }
71
+ const nextInsideTaskItem = insideTaskItem || type === 'taskItem';
72
+ for (let index = content.length - 1; index >= 0; index -= 1) {
73
+ nodeStack.push(content[index]);
74
+ insideTaskStack.push(nextInsideTaskItem);
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+ export function buildAppendMarkdown(existingMarkdown, taskTexts) {
80
+ const trimmed = (existingMarkdown ?? '').trimEnd();
81
+ const taskBlock = buildTaskMarkdown(taskTexts);
82
+ if (!trimmed) {
83
+ return taskBlock;
84
+ }
85
+ return `${trimmed}\n\n${taskBlock}`;
86
+ }
87
+ export function buildCreateMarkdown(taskTexts) {
88
+ return buildTaskMarkdown(taskTexts);
89
+ }
90
+ export function buildContentTextPreview(contentMd) {
91
+ const plain = stripMarkdown(contentMd);
92
+ const chars = Array.from(plain);
93
+ if (chars.length <= CONTENT_TEXT_PREVIEW_LIMIT) {
94
+ return plain;
95
+ }
96
+ return chars.slice(0, CONTENT_TEXT_PREVIEW_LIMIT).join('');
97
+ }
98
+ export function buildSummaryTitle(taskTexts) {
99
+ const count = taskTexts.length;
100
+ const keywords = taskTexts
101
+ .slice(0, 2)
102
+ .map((task) => task.trim())
103
+ .filter(Boolean)
104
+ .map((task) => truncateChars(task, 12))
105
+ .join('、');
106
+ if (!keywords) {
107
+ return `${DEFAULT_TITLE_PREFIX}(${count}项)`;
108
+ }
109
+ const base = `${DEFAULT_TITLE_PREFIX}(${count}项):${keywords}`;
110
+ return truncateChars(base, 60);
111
+ }
112
+ export function normalizeTaskTexts(values) {
113
+ const normalized = values
114
+ .map((value) => value.replace(/\s+/g, ' ').trim())
115
+ .filter((value) => value.length > 0);
116
+ if (normalized.length === 0) {
117
+ throw new DinoxError('At least one non-empty task is required');
118
+ }
119
+ return normalized;
120
+ }
121
+ export function applyTaskCheckedStatus(doc, taskId, checked) {
122
+ const normalizedTaskId = taskId.trim();
123
+ if (!normalizedTaskId) {
124
+ throw new DinoxError('taskId is required');
125
+ }
126
+ const nextDoc = cloneJson(assertTiptapDoc(doc));
127
+ const taskOrder = [];
128
+ const matchedIndices = [];
129
+ let changedCount = 0;
130
+ const visitNode = (value) => {
131
+ if (!value || typeof value !== 'object') {
132
+ return;
133
+ }
134
+ const node = value;
135
+ const type = typeof node.type === 'string' ? node.type : '';
136
+ if (type === 'taskItem') {
137
+ const currentTaskId = resolveTaskId(node);
138
+ if (currentTaskId) {
139
+ const currentIndex = taskOrder.length;
140
+ taskOrder.push(currentTaskId);
141
+ if (currentTaskId === normalizedTaskId) {
142
+ matchedIndices.push(currentIndex);
143
+ const attrs = ensureNodeAttrs(node);
144
+ const previousChecked = normalizeChecked(node.checked) ?? normalizeChecked(attrs.checked) ?? false;
145
+ if (previousChecked !== checked) {
146
+ changedCount += 1;
147
+ }
148
+ node.id = currentTaskId;
149
+ node.checked = checked;
150
+ attrs.id = currentTaskId;
151
+ attrs.checked = checked;
152
+ }
153
+ }
154
+ }
155
+ const content = Array.isArray(node.content) ? node.content : [];
156
+ for (const child of content) {
157
+ visitNode(child);
158
+ }
159
+ };
160
+ visitNode(nextDoc);
161
+ return {
162
+ nextDoc,
163
+ taskOrder,
164
+ matchedIndices,
165
+ changedCount,
166
+ };
167
+ }
168
+ export function patchTaskMarkdownByOrder(contentMd, taskOrder, matchedIndices, checked) {
169
+ if (taskOrder.length === 0 || matchedIndices.length === 0) {
170
+ return contentMd ?? '';
171
+ }
172
+ const markdown = contentMd ?? '';
173
+ if (!markdown.trim()) {
174
+ throw new DinoxError('Cannot update task markdown: note has task entries in content_json but empty content_md.');
175
+ }
176
+ const lines = markdown.split('\n');
177
+ const checklistLineIndexes = [];
178
+ for (let index = 0; index < lines.length; index += 1) {
179
+ if (TASK_MARKDOWN_LINE_RE.test(lines[index] ?? '')) {
180
+ checklistLineIndexes.push(index);
181
+ }
182
+ }
183
+ if (checklistLineIndexes.length !== taskOrder.length) {
184
+ throw new DinoxError(`Cannot update task markdown: checklist line count (${checklistLineIndexes.length}) does not match content_json task count (${taskOrder.length}).`);
185
+ }
186
+ for (const matchedIndex of matchedIndices) {
187
+ const lineIndex = checklistLineIndexes[matchedIndex];
188
+ if (lineIndex === undefined) {
189
+ throw new DinoxError('Cannot update task markdown: task index is out of range.');
190
+ }
191
+ lines[lineIndex] = patchChecklistLine(lines[lineIndex] ?? '', checked);
192
+ }
193
+ return lines.join('\n');
194
+ }
195
+ function buildTaskItemNodes(taskTexts) {
196
+ const normalizedTasks = normalizeTaskTexts(taskTexts);
197
+ return normalizedTasks.map((text) => {
198
+ const taskId = generateId();
199
+ return {
200
+ type: 'taskItem',
201
+ id: taskId,
202
+ checked: false,
203
+ attrs: {
204
+ id: taskId,
205
+ checked: false,
206
+ },
207
+ content: [
208
+ {
209
+ type: 'paragraph',
210
+ content: [
211
+ {
212
+ type: 'text',
213
+ text,
214
+ },
215
+ ],
216
+ },
217
+ ],
218
+ };
219
+ });
220
+ }
221
+ function buildTaskMarkdown(taskTexts) {
222
+ const normalizedTasks = normalizeTaskTexts(taskTexts);
223
+ return normalizedTasks.map((task) => `- [ ] ${task}`).join('\n');
224
+ }
225
+ function assertTiptapDoc(value) {
226
+ if (!value || typeof value !== 'object') {
227
+ throw new DinoxError('Invalid note content_json: expected object');
228
+ }
229
+ const doc = value;
230
+ if (doc.type !== 'doc') {
231
+ throw new DinoxError('Invalid note content_json: root.type must be "doc"');
232
+ }
233
+ if (!Array.isArray(doc.content)) {
234
+ throw new DinoxError('Invalid note content_json: root.content must be an array');
235
+ }
236
+ return doc;
237
+ }
238
+ function ensureDocContentArray(doc) {
239
+ const content = doc.content;
240
+ if (Array.isArray(content)) {
241
+ return content;
242
+ }
243
+ doc.content = [];
244
+ return doc.content;
245
+ }
246
+ function ensureNodeContentArray(node) {
247
+ const content = node.content;
248
+ if (Array.isArray(content)) {
249
+ return content;
250
+ }
251
+ node.content = [];
252
+ return node.content;
253
+ }
254
+ function isTaskListNode(node) {
255
+ return Boolean(node && typeof node === 'object' && node.type === 'taskList');
256
+ }
257
+ function ensureNodeAttrs(node) {
258
+ if (!isObject(node.attrs)) {
259
+ node.attrs = {};
260
+ }
261
+ return node.attrs;
262
+ }
263
+ function resolveTaskId(node) {
264
+ const nodeId = normalizeString(node.id);
265
+ if (nodeId) {
266
+ return nodeId;
267
+ }
268
+ const attrs = isObject(node.attrs) ? node.attrs : null;
269
+ const attrsId = normalizeString(attrs?.id);
270
+ return attrsId;
271
+ }
272
+ function normalizeTaskItemNode(node) {
273
+ const attrsSource = isObject(node.attrs) ? node.attrs : {};
274
+ const nodeId = normalizeString(node.id);
275
+ const attrsId = normalizeString(attrsSource.id);
276
+ const taskId = nodeId || attrsId || generateId();
277
+ const checked = normalizeChecked(node.checked) ?? normalizeChecked(attrsSource.checked) ?? false;
278
+ return {
279
+ type: 'taskItem',
280
+ id: taskId,
281
+ checked,
282
+ attrs: {
283
+ ...attrsSource,
284
+ id: taskId,
285
+ checked,
286
+ },
287
+ content: Array.isArray(node.content) ? cloneJson(node.content) : [],
288
+ };
289
+ }
290
+ function normalizeChecked(value) {
291
+ if (value === true)
292
+ return true;
293
+ if (value === false)
294
+ return false;
295
+ if (value === 'true')
296
+ return true;
297
+ if (value === 'false')
298
+ return false;
299
+ return undefined;
300
+ }
301
+ function normalizeString(value) {
302
+ if (typeof value !== 'string') {
303
+ return '';
304
+ }
305
+ return value.trim();
306
+ }
307
+ function truncateChars(value, limit) {
308
+ const chars = Array.from(value);
309
+ if (chars.length <= limit) {
310
+ return value;
311
+ }
312
+ return chars.slice(0, limit).join('');
313
+ }
314
+ function isObject(value) {
315
+ return Boolean(value && typeof value === 'object');
316
+ }
317
+ function cloneJson(value) {
318
+ return JSON.parse(JSON.stringify(value));
319
+ }
320
+ function patchChecklistLine(line, checked) {
321
+ return line.replace(TASK_MARKDOWN_LINE_RE, (_, prefix, suffix) => {
322
+ return `${prefix}${checked ? 'x' : ' '}${suffix}`;
323
+ });
324
+ }
325
+ const TASK_MARKDOWN_LINE_RE = /^(\s*[-*+]\s+\[)[ xX](\].*)$/;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerTodoCommands(program: Command): void;