@dinoxx/dinox-cli 1.0.0

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.
Files changed (90) hide show
  1. package/README.md +294 -0
  2. package/dist/auth/userInfo.d.ts +14 -0
  3. package/dist/auth/userInfo.js +115 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +32 -0
  6. package/dist/cliTypes.d.ts +6 -0
  7. package/dist/cliTypes.js +1 -0
  8. package/dist/commands/auth/index.d.ts +2 -0
  9. package/dist/commands/auth/index.js +193 -0
  10. package/dist/commands/boxes/index.d.ts +2 -0
  11. package/dist/commands/boxes/index.js +107 -0
  12. package/dist/commands/boxes/repo.d.ts +21 -0
  13. package/dist/commands/boxes/repo.js +154 -0
  14. package/dist/commands/config/index.d.ts +2 -0
  15. package/dist/commands/config/index.js +67 -0
  16. package/dist/commands/info/index.d.ts +2 -0
  17. package/dist/commands/info/index.js +20 -0
  18. package/dist/commands/notes/index.d.ts +2 -0
  19. package/dist/commands/notes/index.js +271 -0
  20. package/dist/commands/notes/repo.d.ts +70 -0
  21. package/dist/commands/notes/repo.js +674 -0
  22. package/dist/commands/notes/searchTime.d.ts +9 -0
  23. package/dist/commands/notes/searchTime.js +85 -0
  24. package/dist/commands/prompt/index.d.ts +2 -0
  25. package/dist/commands/prompt/index.js +51 -0
  26. package/dist/commands/prompt/repo.d.ts +6 -0
  27. package/dist/commands/prompt/repo.js +18 -0
  28. package/dist/commands/sync.d.ts +2 -0
  29. package/dist/commands/sync.js +68 -0
  30. package/dist/commands/tags/index.d.ts +2 -0
  31. package/dist/commands/tags/index.js +120 -0
  32. package/dist/commands/tags/repo.d.ts +14 -0
  33. package/dist/commands/tags/repo.js +247 -0
  34. package/dist/config/keys.d.ts +9 -0
  35. package/dist/config/keys.js +17 -0
  36. package/dist/config/paths.d.ts +4 -0
  37. package/dist/config/paths.js +39 -0
  38. package/dist/config/resolve.d.ts +2 -0
  39. package/dist/config/resolve.js +56 -0
  40. package/dist/config/serviceEndpoints.d.ts +3 -0
  41. package/dist/config/serviceEndpoints.js +3 -0
  42. package/dist/config/store.d.ts +5 -0
  43. package/dist/config/store.js +87 -0
  44. package/dist/config/types.d.ts +51 -0
  45. package/dist/config/types.js +1 -0
  46. package/dist/dinox.d.ts +2 -0
  47. package/dist/dinox.js +50 -0
  48. package/dist/powersync/connector.d.ts +21 -0
  49. package/dist/powersync/connector.js +58 -0
  50. package/dist/powersync/runtime.d.ts +37 -0
  51. package/dist/powersync/runtime.js +107 -0
  52. package/dist/powersync/schema/content.d.ts +76 -0
  53. package/dist/powersync/schema/content.js +76 -0
  54. package/dist/powersync/schema/index.d.ts +371 -0
  55. package/dist/powersync/schema/index.js +35 -0
  56. package/dist/powersync/schema/local.d.ts +68 -0
  57. package/dist/powersync/schema/local.js +83 -0
  58. package/dist/powersync/schema/note.d.ts +34 -0
  59. package/dist/powersync/schema/note.js +34 -0
  60. package/dist/powersync/schema/notesExtras.d.ts +62 -0
  61. package/dist/powersync/schema/notesExtras.js +71 -0
  62. package/dist/powersync/schema/projects.d.ts +101 -0
  63. package/dist/powersync/schema/projects.js +101 -0
  64. package/dist/powersync/schema/tags.d.ts +37 -0
  65. package/dist/powersync/schema/tags.js +37 -0
  66. package/dist/powersync/tokenIndex.d.ts +17 -0
  67. package/dist/powersync/tokenIndex.js +202 -0
  68. package/dist/powersync/uploader.d.ts +7 -0
  69. package/dist/powersync/uploader.js +134 -0
  70. package/dist/utils/argValue.d.ts +1 -0
  71. package/dist/utils/argValue.js +17 -0
  72. package/dist/utils/errors.d.ts +10 -0
  73. package/dist/utils/errors.js +17 -0
  74. package/dist/utils/id.d.ts +1 -0
  75. package/dist/utils/id.js +4 -0
  76. package/dist/utils/output.d.ts +2 -0
  77. package/dist/utils/output.js +10 -0
  78. package/dist/utils/redact.d.ts +1 -0
  79. package/dist/utils/redact.js +10 -0
  80. package/dist/utils/text.d.ts +1 -0
  81. package/dist/utils/text.js +35 -0
  82. package/dist/utils/time.d.ts +1 -0
  83. package/dist/utils/time.js +3 -0
  84. package/dist/utils/tiptapMarkdown.d.ts +6 -0
  85. package/dist/utils/tiptapMarkdown.js +149 -0
  86. package/dist/utils/tokenize.d.ts +1 -0
  87. package/dist/utils/tokenize.js +56 -0
  88. package/dist/utils/version.d.ts +1 -0
  89. package/dist/utils/version.js +21 -0
  90. package/package.json +63 -0
@@ -0,0 +1,107 @@
1
+ import { loadConfig } from '../../config/store.js';
2
+ import { resolveConfig } from '../../config/resolve.js';
3
+ import { DinoxError } from '../../utils/errors.js';
4
+ import { printYaml } from '../../utils/output.js';
5
+ import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
6
+ import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
7
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
8
+ import { addBox, listBoxes } from './repo.js';
9
+ function parseSyncTimeoutMs(value) {
10
+ if (typeof value !== 'string') {
11
+ return undefined;
12
+ }
13
+ const parsed = Number(value);
14
+ if (!Number.isFinite(parsed) || parsed <= 0) {
15
+ throw new DinoxError('--sync-timeout must be a positive integer');
16
+ }
17
+ return Math.trunc(parsed);
18
+ }
19
+ async function syncBeforeBoxCommand(db, offline, timeoutMs) {
20
+ if (offline) {
21
+ return;
22
+ }
23
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
24
+ await syncNoteTokenIndex(db);
25
+ }
26
+ async function runBoxListCommand(options, command) {
27
+ const globals = command.optsWithGlobals?.() ?? {};
28
+ const offline = Boolean(options.offline ?? globals.offline);
29
+ const config = resolveConfig(await loadConfig());
30
+ const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
31
+ const { db } = await connectPowerSync({ config, offline, timeoutMs });
32
+ try {
33
+ await syncBeforeBoxCommand(db, offline, timeoutMs);
34
+ const boxes = await listBoxes(db);
35
+ printYaml(boxes);
36
+ }
37
+ finally {
38
+ await db.close().catch(() => undefined);
39
+ }
40
+ }
41
+ export function registerBoxesCommand(program) {
42
+ const box = program.command('box').description('Manage zettel boxes');
43
+ box
44
+ .command('list')
45
+ .description('List zettel boxes (id and name) from c_zettel_box')
46
+ .option('--offline', 'skip connect/sync and only use local cache')
47
+ .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
48
+ .action(runBoxListCommand);
49
+ box
50
+ .command('add')
51
+ .description('Create a new zettel box in c_zettel_box')
52
+ .argument('[name]', 'box name')
53
+ .option('--name <string>', 'box name (alternative to positional name)')
54
+ .option('--description <string>', 'box purpose/usage description (helps AI route notes to this box)')
55
+ .option('--color <string>', 'box color')
56
+ .option('--json', 'output machine-readable YAML')
57
+ .option('--offline', 'skip connect/sync and only use local cache')
58
+ .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
59
+ .action(async (nameArg, options, command) => {
60
+ const globals = command.optsWithGlobals?.() ?? {};
61
+ const offline = Boolean(options.offline ?? globals.offline);
62
+ const jsonOutput = Boolean(options.json ?? globals.json);
63
+ const optionName = typeof options.name === 'string' ? options.name : undefined;
64
+ const name = (nameArg ?? optionName ?? '').trim();
65
+ if (!name) {
66
+ throw new DinoxError('Box name is required. Use `dino box add <name>` or `dino box add --name <name>`.');
67
+ }
68
+ if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
69
+ throw new DinoxError('Conflicting box name values from positional argument and --name option.');
70
+ }
71
+ const rawConfig = await loadConfig();
72
+ const config = resolveConfig(rawConfig);
73
+ const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
74
+ const identity = await resolveAuthIdentity(rawConfig, config);
75
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
76
+ try {
77
+ await syncBeforeBoxCommand(db, offline, timeoutMs);
78
+ const result = await addBox(db, {
79
+ name,
80
+ userId: identity.userId,
81
+ description: typeof options.description === 'string' ? options.description : undefined,
82
+ color: typeof options.color === 'string' ? options.color : undefined,
83
+ });
84
+ if (!offline && !config.powersync.uploadBaseUrl) {
85
+ console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
86
+ }
87
+ if (!offline) {
88
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
89
+ }
90
+ const payload = { ok: true, stale: offline ? true : stale, ...result };
91
+ if (jsonOutput) {
92
+ printYaml(payload);
93
+ return;
94
+ }
95
+ console.log(result.id);
96
+ if (stale && !offline) {
97
+ console.log('warning: sync timed out; results may be stale');
98
+ }
99
+ }
100
+ finally {
101
+ await db.close().catch(() => undefined);
102
+ }
103
+ });
104
+ box.action((_options, command) => {
105
+ command.outputHelp();
106
+ });
107
+ }
@@ -0,0 +1,21 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ export type BoxListItem = {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ };
7
+ export type AddBoxInput = {
8
+ name: string;
9
+ userId: string;
10
+ description?: string;
11
+ color?: string;
12
+ };
13
+ export type AddBoxResult = {
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ color: string;
18
+ restored: boolean;
19
+ };
20
+ export declare function listBoxes(db: AbstractPowerSyncDatabase): Promise<BoxListItem[]>;
21
+ export declare function addBox(db: AbstractPowerSyncDatabase, input: AddBoxInput): Promise<AddBoxResult>;
@@ -0,0 +1,154 @@
1
+ import { generateId } from '../../utils/id.js';
2
+ import { DinoxError } from '../../utils/errors.js';
3
+ function normalizeName(value) {
4
+ return value.trim();
5
+ }
6
+ function normalizeLookup(value) {
7
+ return normalizeName(value).toLowerCase();
8
+ }
9
+ async function getBoxColumns(db) {
10
+ const rows = await db.getAll('PRAGMA table_info(c_zettel_box)', []);
11
+ return new Set(rows.map((row) => row.name).filter(Boolean));
12
+ }
13
+ async function listBoxRowsForWrite(db) {
14
+ return db.getAll(`
15
+ SELECT
16
+ TRIM(COALESCE(id, '')) AS id,
17
+ TRIM(COALESCE(name, '')) AS name,
18
+ TRIM(COALESCE(description, '')) AS description,
19
+ TRIM(COALESCE(color, '')) AS color,
20
+ is_del,
21
+ TRIM(COALESCE(user_no, '')) AS user_no
22
+ FROM c_zettel_box
23
+ WHERE TRIM(COALESCE(id, '')) <> ''
24
+ ORDER BY
25
+ CASE WHEN is_del IS NULL OR is_del = 0 THEN 0 ELSE 1 END ASC,
26
+ updated_at DESC,
27
+ created_at DESC,
28
+ id DESC
29
+ `, []);
30
+ }
31
+ export async function listBoxes(db) {
32
+ const rows = await db.getAll(`
33
+ SELECT DISTINCT
34
+ TRIM(COALESCE(id, '')) AS id,
35
+ TRIM(COALESCE(name, '')) AS name,
36
+ TRIM(COALESCE(description, '')) AS description
37
+ FROM c_zettel_box
38
+ WHERE (is_del IS NULL OR is_del = 0)
39
+ AND TRIM(COALESCE(id, '')) <> ''
40
+ ORDER BY name COLLATE NOCASE ASC, id ASC
41
+ `, []);
42
+ return rows
43
+ .map((row) => ({
44
+ id: row.id?.trim() ?? '',
45
+ name: row.name?.trim() ?? '',
46
+ description: row.description?.trim() ?? '',
47
+ }))
48
+ .filter((row) => row.id.length > 0);
49
+ }
50
+ export async function addBox(db, input) {
51
+ const userId = input.userId.trim();
52
+ const name = normalizeName(input.name);
53
+ if (!name) {
54
+ throw new DinoxError('Box name is required');
55
+ }
56
+ const description = normalizeName(input.description ?? '');
57
+ const color = normalizeName(input.color ?? '');
58
+ const lookup = normalizeLookup(name);
59
+ const columnNames = await getBoxColumns(db);
60
+ const requiredColumns = ['id', 'name', 'user_no'];
61
+ const missingRequired = requiredColumns.filter((column) => !columnNames.has(column));
62
+ if (missingRequired.length > 0) {
63
+ throw new DinoxError(`c_zettel_box table is missing required columns: ${missingRequired.join(', ')}`);
64
+ }
65
+ if (!userId) {
66
+ throw new DinoxError('c_zettel_box insert requires user_no. Missing persisted userId. Run `dino auth login` first.');
67
+ }
68
+ const rows = await listBoxRowsForWrite(db);
69
+ const sameNameRows = rows.filter((row) => normalizeLookup(row.name ?? '') === lookup);
70
+ const activeRows = sameNameRows.filter((row) => row.is_del !== 1);
71
+ if (activeRows.length > 0) {
72
+ const ids = activeRows.map((row) => row.id?.trim() ?? '').filter(Boolean);
73
+ throw new DinoxError(`Zettel box already exists: ${name}`, {
74
+ details: {
75
+ type: 'box_exists',
76
+ name,
77
+ ids,
78
+ question: 'This box already exists. Do you want to keep using the existing box?',
79
+ },
80
+ });
81
+ }
82
+ const timestamp = new Date().toISOString();
83
+ const deletedRow = sameNameRows.find((row) => (row.id?.trim() ?? '').length > 0);
84
+ if (deletedRow?.id) {
85
+ const setParts = [];
86
+ const params = [];
87
+ const setValue = (column, value) => {
88
+ if (!columnNames.has(column)) {
89
+ return;
90
+ }
91
+ setParts.push(`${column} = ?`);
92
+ params.push(value);
93
+ };
94
+ setValue('name', name);
95
+ setValue('is_del', 0);
96
+ setValue('updated_at', timestamp);
97
+ if (description) {
98
+ setValue('description', description);
99
+ }
100
+ if (color) {
101
+ setValue('color', color);
102
+ }
103
+ setValue('user_no', userId);
104
+ if (setParts.length > 0) {
105
+ params.push(deletedRow.id);
106
+ await db.execute(`
107
+ UPDATE c_zettel_box
108
+ SET ${setParts.join(', ')}
109
+ WHERE id = ?
110
+ `, params);
111
+ }
112
+ return {
113
+ id: deletedRow.id,
114
+ name,
115
+ description: description || (deletedRow.description?.trim() ?? ''),
116
+ color: color || (deletedRow.color?.trim() ?? ''),
117
+ restored: true,
118
+ };
119
+ }
120
+ const id = generateId();
121
+ const insertEntries = [];
122
+ const pushInsert = (column, value) => {
123
+ if (!columnNames.has(column)) {
124
+ return;
125
+ }
126
+ insertEntries.push([column, value]);
127
+ };
128
+ pushInsert('id', id);
129
+ pushInsert('name', name);
130
+ pushInsert('description', description || null);
131
+ pushInsert('color', color || null);
132
+ pushInsert('priority', 0);
133
+ pushInsert('is_show', 1);
134
+ pushInsert('cover_url', '');
135
+ pushInsert('created_at', timestamp);
136
+ pushInsert('updated_at', timestamp);
137
+ pushInsert('extra_data', '{}');
138
+ pushInsert('user_no', userId);
139
+ pushInsert('is_del', 0);
140
+ const insertColumns = insertEntries.map(([column]) => column);
141
+ const placeholders = insertColumns.map(() => '?').join(', ');
142
+ const insertParams = insertEntries.map(([, value]) => value);
143
+ await db.execute(`
144
+ INSERT INTO c_zettel_box (${insertColumns.join(', ')})
145
+ VALUES (${placeholders})
146
+ `, insertParams);
147
+ return {
148
+ id,
149
+ name,
150
+ description,
151
+ color,
152
+ restored: false,
153
+ };
154
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerConfigCommands(program: Command): void;
@@ -0,0 +1,67 @@
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';
4
+ import { redactAuthorization } from '../../utils/redact.js';
5
+ import { DinoxError } from '../../utils/errors.js';
6
+ import { printYaml } from '../../utils/output.js';
7
+ function redactConfigForDisplay(config) {
8
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
9
+ return config;
10
+ }
11
+ const clone = JSON.parse(JSON.stringify(config));
12
+ const auth = clone?.auth;
13
+ if (auth && typeof auth === 'object') {
14
+ auth.authorization = redactAuthorization(auth.authorization);
15
+ }
16
+ return clone;
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
+ export function registerConfigCommands(program) {
27
+ const configCmd = program.command('config').description('Read and write Dinox configuration');
28
+ configCmd
29
+ .command('get')
30
+ .description('Get configuration value')
31
+ .argument('[key]', 'config key (dot path)')
32
+ .action(async (key) => {
33
+ if (key && !isReadableConfigKey(key)) {
34
+ throw new DinoxError(`Unknown config key: ${key}`);
35
+ }
36
+ if (key && isFixedConfigKey(key)) {
37
+ const resolved = resolveConfig(await loadConfig());
38
+ printYaml(getByPath(resolved, key));
39
+ return;
40
+ }
41
+ const value = await getConfigValue(key);
42
+ const redacted = key ? value : redactConfigForDisplay(value);
43
+ printYaml(redacted);
44
+ });
45
+ configCmd
46
+ .command('set')
47
+ .description('Set configuration value')
48
+ .argument('<key>', 'config key (dot path)')
49
+ .argument('<value>', 'value')
50
+ .action(async (key, value) => {
51
+ if (isFixedConfigKey(key)) {
52
+ throw new DinoxError(`${key} is fixed and cannot be changed`);
53
+ }
54
+ if (!isConfigKey(key)) {
55
+ throw new DinoxError(`Unknown config key: ${key}`);
56
+ }
57
+ if (key === 'sync.timeoutMs') {
58
+ const parsed = Number(value);
59
+ if (!Number.isFinite(parsed) || parsed <= 0) {
60
+ throw new DinoxError('sync.timeoutMs must be a positive integer');
61
+ }
62
+ await setConfigValue(key, Math.trunc(parsed));
63
+ return;
64
+ }
65
+ await setConfigValue(key, value);
66
+ });
67
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInfoCommand(program: Command): void;
@@ -0,0 +1,20 @@
1
+ import { printYaml } from '../../utils/output.js';
2
+ import { getPackageVersion } from '../../utils/version.js';
3
+ export function registerInfoCommand(program) {
4
+ program
5
+ .command('info')
6
+ .description('Show CLI version info')
7
+ .action((_options, command) => {
8
+ const globals = command.optsWithGlobals?.() ?? {};
9
+ const jsonOutput = Boolean(globals.json);
10
+ const payload = {
11
+ name: 'dino',
12
+ version: getPackageVersion(),
13
+ };
14
+ if (jsonOutput) {
15
+ printYaml(payload);
16
+ return;
17
+ }
18
+ console.log(payload.version);
19
+ });
20
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerNoteCommands(program: Command): void;
@@ -0,0 +1,271 @@
1
+ import { loadConfig } from '../../config/store.js';
2
+ import { resolveConfig } from '../../config/resolve.js';
3
+ import { DinoxError } from '../../utils/errors.js';
4
+ import { readStringOrFile } from '../../utils/argValue.js';
5
+ import { generateId } from '../../utils/id.js';
6
+ import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
7
+ import { printYaml } from '../../utils/output.js';
8
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
9
+ import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
10
+ import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
11
+ import { createNote, getNote, getNoteDetail, resolveZettelBoxIdsForCreate, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
12
+ import { parseCreatedAtRange } from './searchTime.js';
13
+ function parseSyncTimeoutMs(value) {
14
+ if (typeof value !== 'string') {
15
+ return undefined;
16
+ }
17
+ const parsed = Number(value);
18
+ if (!Number.isFinite(parsed) || parsed <= 0) {
19
+ throw new DinoxError('--sync-timeout must be a positive integer');
20
+ }
21
+ return Math.trunc(parsed);
22
+ }
23
+ function parseNoteType(value) {
24
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
25
+ if (normalized === 'note' || normalized === 'crawl') {
26
+ return normalized;
27
+ }
28
+ throw new DinoxError('--type must be one of: note, crawl');
29
+ }
30
+ async function parseStringInput(value) {
31
+ return readStringOrFile(value);
32
+ }
33
+ function parseListInput(raw, optionName) {
34
+ const value = raw.trim();
35
+ if (!value) {
36
+ return [];
37
+ }
38
+ if (value.startsWith('[')) {
39
+ try {
40
+ const parsed = JSON.parse(value);
41
+ if (!Array.isArray(parsed)) {
42
+ throw new DinoxError(`${optionName} must be a JSON array`);
43
+ }
44
+ return Array.from(new Set(parsed
45
+ .map((entry) => String(entry).trim())
46
+ .filter((entry) => entry.length > 0)));
47
+ }
48
+ catch (error) {
49
+ if (error instanceof DinoxError) {
50
+ throw error;
51
+ }
52
+ throw new DinoxError(`Invalid list for ${optionName}`, { cause: error });
53
+ }
54
+ }
55
+ return Array.from(new Set(value
56
+ .split(/[\n,,]/g)
57
+ .map((entry) => entry.trim())
58
+ .filter((entry) => entry.length > 0)));
59
+ }
60
+ async function parseStringListInput(value, optionName) {
61
+ const raw = await readStringOrFile(value);
62
+ return parseListInput(raw, optionName);
63
+ }
64
+ async function syncBeforeNoteCommand(db, offline, timeoutMs) {
65
+ if (offline) {
66
+ return;
67
+ }
68
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
69
+ await syncNoteTokenIndex(db);
70
+ }
71
+ export function registerNoteCommands(program) {
72
+ const note = program.command('note').description('Manage notes');
73
+ note
74
+ .command('search')
75
+ .description('Search notes and return note list for AI agents')
76
+ .argument('[query]', 'search query')
77
+ .option('--tags <expr>', 'tag expression with AND/OR/NOT, e.g. \'(work OR life) AND NOT archived\'')
78
+ .option('--from <date>', 'created_at start (YYYY-MM-DD or ISO datetime)')
79
+ .option('--to <date>', 'created_at end (YYYY-MM-DD or ISO datetime)')
80
+ .option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
81
+ .option('--include-deleted', 'include soft-deleted notes')
82
+ .action(async (query, options, command) => {
83
+ const globals = command.optsWithGlobals?.() ?? {};
84
+ const offline = Boolean(globals.offline);
85
+ const config = resolveConfig(await loadConfig());
86
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
87
+ const { db } = await connectPowerSync({ config, offline, timeoutMs });
88
+ try {
89
+ await syncBeforeNoteCommand(db, offline, timeoutMs);
90
+ const keyword = query?.trim() ?? '';
91
+ const tagsExpression = typeof options.tags === 'string' ? options.tags.trim() : '';
92
+ const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
93
+ from: options.from,
94
+ to: options.to,
95
+ days: options.days,
96
+ });
97
+ const rows = await searchNotes(db, keyword, {
98
+ includeDeleted: Boolean(options.includeDeleted),
99
+ tagsExpression: tagsExpression || undefined,
100
+ createdAtFrom,
101
+ createdAtTo,
102
+ });
103
+ printYaml(rows);
104
+ }
105
+ finally {
106
+ await db.close().catch(() => undefined);
107
+ }
108
+ });
109
+ note
110
+ .command('get')
111
+ .description('Get note by id')
112
+ .argument('<id>', 'note id')
113
+ .action(async (id, _options, command) => {
114
+ const globals = command.optsWithGlobals?.() ?? {};
115
+ const offline = Boolean(globals.offline);
116
+ const jsonOutput = Boolean(globals.json);
117
+ const config = resolveConfig(await loadConfig());
118
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
119
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
120
+ try {
121
+ await syncBeforeNoteCommand(db, offline, timeoutMs);
122
+ const note = await getNote(db, id);
123
+ if (!note) {
124
+ throw new DinoxError(`Note not found: ${id}`);
125
+ }
126
+ if (jsonOutput) {
127
+ printYaml({ stale, note });
128
+ return;
129
+ }
130
+ if (stale && !offline) {
131
+ console.log('warning: sync timed out; result may be stale');
132
+ }
133
+ console.log(`${note.id}`);
134
+ console.log(`title: ${note.title ?? ''}`);
135
+ if (note.updated_at) {
136
+ console.log(`updated_at: ${note.updated_at}`);
137
+ }
138
+ if (note.is_del) {
139
+ console.log('is_del: 1');
140
+ }
141
+ }
142
+ finally {
143
+ await db.close().catch(() => undefined);
144
+ }
145
+ });
146
+ note
147
+ .command('detail')
148
+ .description('Get full note details by id')
149
+ .argument('<id>', 'note id')
150
+ .action(async (id, _options, command) => {
151
+ const globals = command.optsWithGlobals?.() ?? {};
152
+ const offline = Boolean(globals.offline);
153
+ const config = resolveConfig(await loadConfig());
154
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
155
+ const { db } = await connectPowerSync({ config, offline, timeoutMs });
156
+ try {
157
+ await syncBeforeNoteCommand(db, offline, timeoutMs);
158
+ const note = await getNoteDetail(db, id);
159
+ if (!note) {
160
+ throw new DinoxError(`Note not found: ${id}`);
161
+ }
162
+ printYaml(note);
163
+ }
164
+ finally {
165
+ await db.close().catch(() => undefined);
166
+ }
167
+ });
168
+ note
169
+ .command('create')
170
+ .description('Create a new note')
171
+ .requiredOption('--title <string>', 'note title')
172
+ .requiredOption('--content <string|@file>', 'markdown content')
173
+ .option('--type <note|crawl>', 'note type: note or crawl', 'crawl')
174
+ .option('--tags <string|@file>', 'tag list (JSON array or comma/newline-separated)')
175
+ .option('--zettel_boxes <string|@file>', 'zettel box names (JSON array or comma/newline-separated)')
176
+ .action(async (options, command) => {
177
+ const globals = command.optsWithGlobals?.() ?? {};
178
+ const offline = Boolean(globals.offline);
179
+ const jsonOutput = Boolean(globals.json);
180
+ const rawConfig = await loadConfig();
181
+ const config = resolveConfig(rawConfig);
182
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
183
+ const identity = await resolveAuthIdentity(rawConfig, config);
184
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
185
+ try {
186
+ await syncBeforeNoteCommand(db, offline, timeoutMs);
187
+ const contentMd = await parseStringInput(options.content);
188
+ const { contentJson, contentText } = convertMarkdownToNoteContent(contentMd);
189
+ const tagsInput = typeof options.tags === 'string'
190
+ ? await parseStringListInput(options.tags, '--tags')
191
+ : [];
192
+ const tags = await validateTagsForCreate(db, tagsInput);
193
+ const zettelBoxesOption = typeof options.zettel_boxes === 'string'
194
+ ? options.zettel_boxes
195
+ : typeof options.zettelBoxes === 'string'
196
+ ? options.zettelBoxes
197
+ : undefined;
198
+ const zettelBoxNames = zettelBoxesOption
199
+ ? await parseStringListInput(zettelBoxesOption, '--zettel_boxes')
200
+ : [];
201
+ const zettelBoxIds = await resolveZettelBoxIdsForCreate(db, zettelBoxNames);
202
+ const noteType = parseNoteType(options.type);
203
+ const id = generateId();
204
+ await createNote(db, {
205
+ id,
206
+ title: String(options.title),
207
+ noteType,
208
+ contentMd,
209
+ contentJson,
210
+ contentText,
211
+ tags,
212
+ zettelBoxIds,
213
+ userId: identity.userId,
214
+ });
215
+ if (!offline && !config.powersync.uploadBaseUrl) {
216
+ console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
217
+ }
218
+ if (!offline) {
219
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
220
+ }
221
+ const payload = { ok: true, id, stale: offline ? true : stale };
222
+ if (jsonOutput) {
223
+ printYaml(payload);
224
+ }
225
+ else {
226
+ console.log(id);
227
+ if (stale && !offline) {
228
+ console.log('warning: sync timed out; local cache may be stale');
229
+ }
230
+ }
231
+ }
232
+ finally {
233
+ await db.close().catch(() => undefined);
234
+ }
235
+ });
236
+ note
237
+ .command('delete')
238
+ .description('Soft-delete a note (is_del=1)')
239
+ .argument('<id>', 'note id')
240
+ .action(async (id, _options, command) => {
241
+ const globals = command.optsWithGlobals?.() ?? {};
242
+ const offline = Boolean(globals.offline);
243
+ const jsonOutput = Boolean(globals.json);
244
+ const config = resolveConfig(await loadConfig());
245
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
246
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
247
+ try {
248
+ await syncBeforeNoteCommand(db, offline, timeoutMs);
249
+ await softDeleteNote(db, id);
250
+ if (!offline && !config.powersync.uploadBaseUrl) {
251
+ console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
252
+ }
253
+ if (!offline) {
254
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
255
+ }
256
+ const payload = { ok: true, id, stale: offline ? true : stale };
257
+ if (jsonOutput) {
258
+ printYaml(payload);
259
+ }
260
+ else {
261
+ console.log('OK');
262
+ if (stale && !offline) {
263
+ console.log('warning: sync timed out; local cache may be stale');
264
+ }
265
+ }
266
+ }
267
+ finally {
268
+ await db.close().catch(() => undefined);
269
+ }
270
+ });
271
+ }