@dinoxx/dinox-cli 1.1.1 → 2.1.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 (141) hide show
  1. package/README.md +44 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo/read.d.ts +8 -0
  19. package/dist/commands/notes/repo/read.js +321 -0
  20. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  21. package/dist/commands/notes/repo/resolve.js +187 -0
  22. package/dist/commands/notes/repo/search.d.ts +42 -0
  23. package/dist/commands/notes/repo/search.js +513 -0
  24. package/dist/commands/notes/repo/shared.d.ts +190 -0
  25. package/dist/commands/notes/repo/shared.js +102 -0
  26. package/dist/commands/notes/repo/write.d.ts +24 -0
  27. package/dist/commands/notes/repo/write.js +251 -0
  28. package/dist/commands/notes/repo.d.ts +7 -177
  29. package/dist/commands/notes/repo.js +7 -1310
  30. package/dist/commands/notes/searchOutput.d.ts +2 -1
  31. package/dist/commands/notes/searchOutput.js +2 -1
  32. package/dist/commands/notes/searchSql.js +9 -3
  33. package/dist/commands/prompt/index.d.ts +1 -0
  34. package/dist/commands/prompt/index.js +26 -11
  35. package/dist/commands/savedViews/index.d.ts +7 -0
  36. package/dist/commands/savedViews/index.js +458 -0
  37. package/dist/commands/savedViews/model.d.ts +87 -0
  38. package/dist/commands/savedViews/model.js +306 -0
  39. package/dist/commands/savedViews/repo.d.ts +123 -0
  40. package/dist/commands/savedViews/repo.js +512 -0
  41. package/dist/commands/savedViews/semantics.d.ts +46 -0
  42. package/dist/commands/savedViews/semantics.js +738 -0
  43. package/dist/commands/schema/commandMetadata.js +4 -4
  44. package/dist/commands/schema/index.d.ts +1 -0
  45. package/dist/commands/schema/index.js +23 -2
  46. package/dist/commands/schema/renderSkills.d.ts +6 -0
  47. package/dist/commands/schema/renderSkills.js +109 -11
  48. package/dist/commands/schema/spec.js +4 -0
  49. package/dist/commands/skills/index.d.ts +80 -0
  50. package/dist/commands/skills/index.js +572 -0
  51. package/dist/commands/storage/index.js +2 -2
  52. package/dist/commands/storage/repo/configs.d.ts +30 -0
  53. package/dist/commands/storage/repo/configs.js +259 -0
  54. package/dist/commands/storage/repo/operations.d.ts +25 -0
  55. package/dist/commands/storage/repo/operations.js +337 -0
  56. package/dist/commands/storage/repo/remote.d.ts +32 -0
  57. package/dist/commands/storage/repo/remote.js +132 -0
  58. package/dist/commands/storage/repo/resources.d.ts +12 -0
  59. package/dist/commands/storage/repo/resources.js +134 -0
  60. package/dist/commands/storage/repo/shared.d.ts +181 -0
  61. package/dist/commands/storage/repo/shared.js +36 -0
  62. package/dist/commands/storage/repo/targets.d.ts +12 -0
  63. package/dist/commands/storage/repo/targets.js +178 -0
  64. package/dist/commands/storage/repo.d.ts +3 -166
  65. package/dist/commands/storage/repo.js +4 -1034
  66. package/dist/commands/sync.js +2 -1
  67. package/dist/commands/tags/index.js +30 -11
  68. package/dist/commands/tags/operations.d.ts +3 -3
  69. package/dist/commands/tags/operations.js +45 -20
  70. package/dist/commands/templates/model.d.ts +50 -0
  71. package/dist/commands/templates/model.js +239 -0
  72. package/dist/commands/todo/index.js +3 -3
  73. package/dist/commands/update.d.ts +1 -0
  74. package/dist/commands/update.js +46 -9
  75. package/dist/commands/writeConfirmation.d.ts +13 -0
  76. package/dist/commands/writeConfirmation.js +54 -0
  77. package/dist/config/resolve.js +5 -10
  78. package/dist/config/types.d.ts +3 -6
  79. package/dist/constants/links.d.ts +18 -1
  80. package/dist/constants/links.js +26 -1
  81. package/dist/contract/snapshot.d.ts +30 -0
  82. package/dist/contract/snapshot.js +114 -0
  83. package/dist/daemon/client.d.ts +2 -2
  84. package/dist/daemon/client.js +108 -54
  85. package/dist/daemon/index.d.ts +5 -3
  86. package/dist/daemon/index.js +28 -5
  87. package/dist/daemon/lifecycle.d.ts +7 -2
  88. package/dist/daemon/lifecycle.js +53 -13
  89. package/dist/daemon/pid.d.ts +1 -1
  90. package/dist/daemon/pid.js +6 -3
  91. package/dist/dinox.js +9 -1
  92. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  93. package/dist/powersync/exclusiveRuntime.js +26 -8
  94. package/dist/powersync/runtime.d.ts +3 -2
  95. package/dist/powersync/runtime.js +19 -3
  96. package/dist/powersync/schema/content.d.ts +1 -78
  97. package/dist/powersync/schema/content.js +1 -78
  98. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  99. package/dist/powersync/schema/electronCanonical.js +1154 -0
  100. package/dist/powersync/schema/index.d.ts +573 -129
  101. package/dist/powersync/schema/index.js +3 -32
  102. package/dist/powersync/schema/local.d.ts +1 -56
  103. package/dist/powersync/schema/local.js +1 -60
  104. package/dist/powersync/schema/note.d.ts +1 -35
  105. package/dist/powersync/schema/note.js +1 -35
  106. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  107. package/dist/powersync/schema/notesExtras.js +1 -71
  108. package/dist/powersync/schema/projects.d.ts +1 -104
  109. package/dist/powersync/schema/projects.js +1 -104
  110. package/dist/powersync/schema/savedViews.d.ts +1 -0
  111. package/dist/powersync/schema/savedViews.js +1 -0
  112. package/dist/powersync/schema/storage.d.ts +1 -24
  113. package/dist/powersync/schema/storage.js +1 -24
  114. package/dist/powersync/schema/tags.d.ts +1 -37
  115. package/dist/powersync/schema/tags.js +1 -37
  116. package/dist/powersync/syncGate.d.ts +20 -2
  117. package/dist/powersync/uploader.d.ts +16 -2
  118. package/dist/powersync/uploader.js +152 -158
  119. package/dist/skills/check.d.ts +1 -0
  120. package/dist/skills/check.js +538 -70
  121. package/dist/skills/commandRefs.d.ts +21 -0
  122. package/dist/skills/commandRefs.js +132 -0
  123. package/dist/skills/generate.d.ts +13 -0
  124. package/dist/skills/generate.js +80 -0
  125. package/dist/skills/registry.d.ts +13 -0
  126. package/dist/skills/registry.js +58 -0
  127. package/dist/skills/retired.d.ts +60 -0
  128. package/dist/skills/retired.js +58 -0
  129. package/dist/utils/deadline.d.ts +8 -0
  130. package/dist/utils/deadline.js +31 -0
  131. package/dist/utils/dryRun.d.ts +20 -1
  132. package/dist/utils/dryRun.js +22 -3
  133. package/dist/utils/errorCodes.d.ts +311 -0
  134. package/dist/utils/errorCodes.js +92 -0
  135. package/dist/utils/errors.d.ts +15 -2
  136. package/dist/utils/errors.js +13 -0
  137. package/dist/utils/processExit.d.ts +1 -0
  138. package/dist/utils/processExit.js +33 -0
  139. package/dist/utils/updateNotice.d.ts +1 -0
  140. package/dist/utils/updateNotice.js +1 -1
  141. package/package.json +18 -8
@@ -1,5 +1,6 @@
1
1
  import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
2
  import { type NoteContentBlockDescriptor, type NoteContentOutlineItem } from './contentEngine.js';
3
+ export type NoteContentReadScope = 'structural-metadata' | 'full-content';
3
4
  export type NoteContentPatchOperation = {
4
5
  kind: 'appendSection';
5
6
  heading: string;
@@ -24,6 +25,7 @@ export type NoteContentReadResult = {
24
25
  title: string;
25
26
  readToken: string;
26
27
  readTokenExpiresAt: string;
28
+ readTokenScope: NoteContentReadScope;
27
29
  base: {
28
30
  contentHash: string;
29
31
  updatedAt: string | null;
@@ -72,7 +74,9 @@ export declare function planMarkdownPatch(input: {
72
74
  contentMd: string;
73
75
  operation: NoteContentPatchOperation;
74
76
  }): NoteContentPatchPlan;
75
- export declare function readNoteContentForPatch(db: AbstractPowerSyncDatabase, id: string, databaseScope: string, userId: string): Promise<NoteContentReadResult>;
77
+ export declare function readNoteContentForPatch(db: AbstractPowerSyncDatabase, id: string, databaseScope: string, userId: string, options?: {
78
+ includeContent?: boolean;
79
+ }): Promise<NoteContentReadResult>;
76
80
  export declare function planNoteContentPatch(db: AbstractPowerSyncDatabase, input: {
77
81
  id: string;
78
82
  userId: string;
@@ -154,6 +154,7 @@ async function rememberReadToken(input) {
154
154
  contentHash: input.contentHash,
155
155
  updatedAt: input.updatedAt,
156
156
  version: input.version,
157
+ reviewScope: input.reviewScope,
157
158
  createdAt: new Date(nowMs).toISOString(),
158
159
  expiresAt,
159
160
  };
@@ -296,7 +297,7 @@ export function planMarkdownPatch(input) {
296
297
  });
297
298
  return plan;
298
299
  }
299
- export async function readNoteContentForPatch(db, id, databaseScope, userId) {
300
+ export async function readNoteContentForPatch(db, id, databaseScope, userId, options = {}) {
300
301
  const row = await readNoteContentRow(db, id);
301
302
  const tagLookup = await loadTagReferenceLookup(db, userId);
302
303
  const document = parseStoredNoteDocument({
@@ -308,18 +309,23 @@ export async function readNoteContentForPatch(db, id, databaseScope, userId) {
308
309
  });
309
310
  const contentHash = getNoteContentHash(document);
310
311
  const version = normalizeVersion(row.version);
312
+ const readTokenScope = options.includeContent === true
313
+ ? 'full-content'
314
+ : 'structural-metadata';
311
315
  const token = await rememberReadToken({
312
316
  databaseScope,
313
317
  noteId: row.id,
314
318
  contentHash,
315
319
  updatedAt: row.updated_at ?? null,
316
320
  version,
321
+ reviewScope: readTokenScope,
317
322
  });
318
323
  return {
319
324
  noteId: row.id,
320
325
  title: row.title?.trim() ?? '',
321
326
  readToken: token.token,
322
327
  readTokenExpiresAt: token.expiresAt,
328
+ readTokenScope,
323
329
  base: {
324
330
  contentHash,
325
331
  updatedAt: row.updated_at ?? null,
@@ -353,6 +359,11 @@ export async function applyNoteContentPatch(db, input) {
353
359
  if (token.databaseScopeFingerprint !== fingerprintDatabaseScope(input.databaseScope)) {
354
360
  throw new DinoxError('read token was issued for a different local account database. Run `dino note content-read <id>` again.');
355
361
  }
362
+ if ('allowProtectedReplace' in input.operation
363
+ && input.operation.allowProtectedReplace === true
364
+ && token.reviewScope !== 'full-content') {
365
+ throw new DinoxError('Protected content replacement requires a full-content read token. Run `dino note content-read <id> --include-content --format json`, review the affected protected blocks, and retry with the new token.');
366
+ }
356
367
  const result = await withPowerSyncWriteTransaction(db, async (tx) => {
357
368
  const tagLookup = await loadTagReferenceLookup(tx, input.userId);
358
369
  const row = await readNoteContentRow(tx, input.id);
@@ -1,3 +1,6 @@
1
1
  import type { CommandExecutionContext } from '../descriptor/types.js';
2
+ import type { NoteContentBlockDescriptor } from './contentEngine.js';
3
+ export type PublicNoteContentBlockDescriptor = Pick<NoteContentBlockDescriptor, 'anchorId' | 'hasStableAnchor' | 'nodeIndex' | 'nodeRange' | 'type' | 'editMode' | 'level' | 'textHash' | 'nodeHash' | 'charCount'>;
4
+ export declare function toPublicNoteContentBlocks(blocks: NoteContentBlockDescriptor[]): PublicNoteContentBlockDescriptor[];
2
5
  export declare function runNoteContentRead({ command, options, args }: CommandExecutionContext): Promise<void>;
3
6
  export declare function runNotePatch({ command, options, args }: CommandExecutionContext): Promise<void>;
@@ -7,6 +7,20 @@ import { commandStale, printStaleCacheWarning, resolveWriteDurabilityForRuntime,
7
7
  import { readNoteWriteReceipt } from './writeReceipt.js';
8
8
  import { applyNoteContentPatch, planNoteContentPatch, readNoteContentForPatch, } from './contentPatch.js';
9
9
  import { parseStringInput, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
10
+ export function toPublicNoteContentBlocks(blocks) {
11
+ return blocks.map((block) => ({
12
+ anchorId: block.anchorId,
13
+ hasStableAnchor: block.hasStableAnchor,
14
+ nodeIndex: block.nodeIndex,
15
+ nodeRange: block.nodeRange,
16
+ type: block.type,
17
+ editMode: block.editMode,
18
+ level: block.level,
19
+ textHash: block.textHash,
20
+ nodeHash: block.nodeHash,
21
+ charCount: block.charCount,
22
+ }));
23
+ }
10
24
  function requireNoteId(id) {
11
25
  const normalized = id?.trim() ?? '';
12
26
  if (!normalized) {
@@ -60,11 +74,11 @@ export async function runNoteContentRead({ command, options, args }) {
60
74
  await withNoteDatabase(command, async (runtime) => {
61
75
  await syncBeforeNoteCommand(runtime);
62
76
  const identity = await resolveAuthIdentity(runtime.config);
63
- const result = await readNoteContentForPatch(runtime.db, id, getPowerSyncDbPath(identity.userId), identity.userId);
64
- const { content, ...rest } = result;
77
+ const result = await readNoteContentForPatch(runtime.db, id, getPowerSyncDbPath(identity.userId), identity.userId, { includeContent: readOptions.includeContent === true });
78
+ const { content, blocks, ...rest } = result;
65
79
  const payload = readOptions.includeContent
66
- ? { ...rest, content, stale: commandStale(runtime) }
67
- : { ...rest, stale: commandStale(runtime) };
80
+ ? { ...rest, blocks, content, stale: commandStale(runtime) }
81
+ : { ...rest, blocks: toPublicNoteContentBlocks(blocks), stale: commandStale(runtime) };
68
82
  printSuccess(payload);
69
83
  });
70
84
  }
@@ -88,6 +102,9 @@ export async function runNotePatch({ command, options, args }) {
88
102
  summary: `Preview ${plan.operation} for note "${plan.title}" (${plan.noteId}); content ${plan.changed ? 'would change' : 'is unchanged'}.`,
89
103
  warnings: [
90
104
  'Run `dino note content-read <id> --format json` before the real edit and pass the returned readToken as --read-token.',
105
+ ...('allowProtectedReplace' in operation && operation.allowProtectedReplace === true
106
+ ? ['Protected replacement requires `content-read --include-content`; review the affected protected blocks before using its full-content token.']
107
+ : []),
91
108
  ...(offline ? ['Execution would run in offline mode and only use the local cache.'] : []),
92
109
  ],
93
110
  inputs: {
@@ -102,12 +119,14 @@ export async function runNotePatch({ command, options, args }) {
102
119
  after_chars: plan.afterContent.length,
103
120
  changed: plan.changed,
104
121
  },
105
- });
122
+ }, runtime);
106
123
  return;
107
124
  }
108
- const readToken = patchOptions.readToken?.trim() ?? '';
125
+ const readToken = typeof patchOptions.readToken === 'string'
126
+ ? (await parseStringInput(patchOptions.readToken)).trim()
127
+ : '';
109
128
  if (!readToken) {
110
- throw new DinoxError('Real note content edits require --read-token from `dino note content-read <id>`.');
129
+ throw new DinoxError('Real note content edits require --read-token <token|@file> from `dino note content-read <id>`.');
111
130
  }
112
131
  const result = await applyNoteContentPatch(db, {
113
132
  id,
@@ -124,10 +143,12 @@ export async function runNotePatch({ command, options, args }) {
124
143
  upload,
125
144
  stale: commandStale(runtime),
126
145
  });
127
- const { changed: contentChanged, ...publicResult } = result;
146
+ const { changed: contentChanged, beforeBlocks, afterBlocks, ...publicResult } = result;
128
147
  const payload = {
129
148
  ok: true,
130
149
  ...publicResult,
150
+ beforeBlocks: toPublicNoteContentBlocks(beforeBlocks),
151
+ afterBlocks: toPublicNoteContentBlocks(afterBlocks),
131
152
  content_changed: contentChanged,
132
153
  ...receipt,
133
154
  };
@@ -42,7 +42,7 @@ export const noteCommandGroup = {
42
42
  { flags: '--sql <expr>', description: 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type/is_starred', type: 'string' },
43
43
  { flags: '--limit <n>', description: 'Maximum returned notes', type: 'number' },
44
44
  { flags: '--offset <n>', description: 'Result offset for pagination', type: 'number', defaultValue: 0 },
45
- { flags: '--fields <list>', description: 'Comma-separated fields: id,title,summary,tags,created_at,boxes,is_starred', type: 'string[]' },
45
+ { flags: '--fields <list>', description: 'Comma-separated fields: id,title,summary,tags,created_at,starred_at,boxes,is_starred', type: 'string[]' },
46
46
  { flags: '--include-deleted', description: 'Include soft-deleted notes', type: 'boolean' },
47
47
  ],
48
48
  schema: {
@@ -54,7 +54,7 @@ export const noteCommandGroup = {
54
54
  type: 'structured',
55
55
  summary: 'AI-friendly note search payload',
56
56
  entity: 'note_search_result',
57
- fields: ['id', 'title', 'summary', 'tags', 'created_at', 'boxes', 'is_starred'],
57
+ fields: ['id', 'title', 'summary', 'tags', 'created_at', 'starred_at', 'boxes', 'is_starred'],
58
58
  metaFields: ['query', 'returned_count', 'total_count', 'stale', 'pagination', 'fields', 'applied_filters'],
59
59
  },
60
60
  examples: [
@@ -66,9 +66,9 @@ export const noteCommandGroup = {
66
66
  },
67
67
  {
68
68
  name: 'get',
69
- description: 'Get one note by id, optionally in lightweight context mode',
69
+ description: 'Get lightweight note context by id without exposing full note content',
70
70
  arguments: [{ syntax: '<id>', description: 'Note id' }],
71
- options: [{ flags: '--context-only', description: 'Return lightweight note context (title/tags/summary/links)', type: 'boolean' }],
71
+ options: [{ flags: '--context-only', description: 'Compatibility flag; note get is context-only by default', type: 'boolean' }],
72
72
  schema: {
73
73
  path: 'note.get',
74
74
  risk: 'read',
@@ -76,11 +76,11 @@ export const noteCommandGroup = {
76
76
  globalOptions: NOTE_GLOBAL_OPTIONS,
77
77
  output: {
78
78
  type: 'structured',
79
- summary: 'Single note or note context payload',
79
+ summary: 'Lightweight note context payload; use note detail or content-read --include-content for full content',
80
80
  entity: 'note_context',
81
- fields: ['id', 'title', 'summary', 'tags', 'created_at', 'updated_at', 'is_del', 'boxes', 'links'],
81
+ fields: ['stale', 'note.id', 'note.title', 'note.summary', 'note.tags', 'note.created_at', 'note.updated_at', 'note.is_del', 'note.starred_at', 'note.is_starred', 'note.boxes', 'note.links'],
82
82
  },
83
- examples: ['dino note get <id> --context-only --format json', 'dino note get <id> --format json'],
83
+ examples: ['dino note get <id> --format json'],
84
84
  },
85
85
  action: runNoteGet,
86
86
  },
@@ -115,8 +115,8 @@ export const noteCommandGroup = {
115
115
  globalOptions: NOTE_GLOBAL_OPTIONS,
116
116
  output: {
117
117
  type: 'structured',
118
- summary: 'One full note object or an ordered array of note objects',
119
- fields: ['id', 'title', 'content_md', 'content_text', 'content_json', 'tags', 'boxes', 'created_at', 'updated_at', 'is_del'],
118
+ summary: 'Ordered full-note collection with freshness metadata',
119
+ fields: ['stale', 'count', 'notes.id', 'notes.title', 'notes.content_md', 'notes.content_text', 'notes.content_json', 'notes.properties', 'notes.is_pub', 'notes.starred_at', 'notes.is_starred', 'notes.tags', 'notes.boxes', 'notes.created_at', 'notes.updated_at', 'notes.is_del'],
120
120
  },
121
121
  examples: ['dino note detail <id> --format json', 'dino note detail --ids @/tmp/note-ids.txt --format json'],
122
122
  },
@@ -155,7 +155,7 @@ export const noteCommandGroup = {
155
155
  description: 'Read note content context and issue a short-lived token required before content patching',
156
156
  arguments: [{ syntax: '<id>', description: 'Note id' }],
157
157
  options: [
158
- { flags: '--include-content', description: 'Include full markdown content in the response', type: 'boolean' },
158
+ { flags: '--include-content', description: 'Include full markdown plus detailed block text and attributes', type: 'boolean' },
159
159
  ],
160
160
  schema: {
161
161
  path: 'note.content-read',
@@ -164,10 +164,13 @@ export const noteCommandGroup = {
164
164
  globalOptions: NOTE_GLOBAL_OPTIONS,
165
165
  output: {
166
166
  type: 'structured',
167
- summary: 'Note content read context with readToken, base hash, heading outline, block index, and derived metadata',
168
- fields: ['noteId', 'title', 'readToken', 'readTokenExpiresAt', 'base', 'outline', 'blocks', 'derived', 'content', 'stale'],
167
+ summary: 'Privacy-preserving note edit context; full content and detailed blocks require --include-content',
168
+ fields: ['noteId', 'title', 'readToken', 'readTokenExpiresAt', 'readTokenScope', 'base', 'outline', 'blocks.type', 'blocks.editMode', 'blocks.nodeRange', 'blocks.textHash', 'blocks.nodeHash', 'blocks.charCount', 'derived', 'content', 'stale'],
169
169
  },
170
- examples: ['dino note content-read <id> --include-content --format json'],
170
+ examples: [
171
+ 'dino note content-read <id> --format json',
172
+ 'dino note content-read <id> --include-content --format json',
173
+ ],
171
174
  },
172
175
  action: runNoteContentRead,
173
176
  },
@@ -295,8 +298,8 @@ export const noteCommandGroup = {
295
298
  { flags: '--replace-block', description: 'Replace one exact markdown block matched by --match', type: 'boolean' },
296
299
  { flags: '--match <string|@file>', description: 'Exact markdown to replace when using --replace-block', type: 'string' },
297
300
  { flags: '--content <string|@file>', description: 'Markdown content to insert', type: 'string' },
298
- { flags: '--read-token <token>', description: 'Required for real writes; returned by note content-read', type: 'string' },
299
- { flags: '--allow-protected-replace', description: 'Allow replace operations to affect media, table, container, or unknown blocks after reviewing content-read output', type: 'boolean' },
301
+ { flags: '--read-token <token|@file>', description: 'Single-use patch capability returned by note content-read; prefer @file to avoid exposing it in argv', type: 'string' },
302
+ { flags: '--allow-protected-replace', description: 'Allow replacement of media, table, container, or unknown blocks only with a reviewed full-content read token', type: 'boolean' },
300
303
  WRITE_DURABILITY_OPTION,
301
304
  { flags: '--dry-run', description: 'Preview the patch without writing', type: 'boolean' },
302
305
  ],
@@ -312,7 +315,7 @@ export const noteCommandGroup = {
312
315
  },
313
316
  examples: [
314
317
  'dino note content-read <id> --format json',
315
- 'dino note patch <id> --append-section "复盘" --content @/tmp/section.md --read-token <token> --format json',
318
+ 'dino note patch <id> --append-section "复盘" --content @/tmp/section.md --read-token @/tmp/dinox-read-token --format json',
316
319
  ],
317
320
  },
318
321
  action: runNotePatch,
@@ -59,6 +59,9 @@ function serializeMarkdownExport(note) {
59
59
  tags: note.tags,
60
60
  boxes: note.boxes,
61
61
  box_ids: note.box_ids,
62
+ properties: note.properties,
63
+ is_pub: note.is_pub,
64
+ starred_at: note.starred_at,
62
65
  is_starred: note.is_starred,
63
66
  is_del: note.is_del ?? 0,
64
67
  }, {
@@ -0,0 +1,8 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import { type NoteContext, type NoteDetail, type NoteExportItem, type NotePreview, type NoteRow } from './shared.js';
3
+ export declare function getNote(db: AbstractPowerSyncDatabase, id: string): Promise<NoteRow | null>;
4
+ export declare function getNoteContext(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContext | null>;
5
+ export declare function getNotePreview(db: AbstractPowerSyncDatabase, id: string, requestedLines: number): Promise<NotePreview | null>;
6
+ export declare function getNoteDetails(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteDetail[]>;
7
+ export declare function getNoteDetail(db: AbstractPowerSyncDatabase, id: string): Promise<NoteDetail | null>;
8
+ export declare function getNoteExportItems(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteExportItem[]>;
@@ -0,0 +1,321 @@
1
+ // Note reads: single-note lookup, context, preview, detail, and export rows.
2
+ import { DinoxError } from '../../../utils/errors.js';
3
+ import { loadZettelBoxNameMap, normalizeStoredFlag, normalizeStoredStarred, parseStoredTags, parseZettelBoxIds, resolveSummary, } from './shared.js';
4
+ export async function getNote(db, id) {
5
+ return db.getOptional(`
6
+ SELECT
7
+ id,
8
+ title,
9
+ content_md,
10
+ content_json,
11
+ content_text,
12
+ created_at,
13
+ updated_at,
14
+ is_del,
15
+ starred_at,
16
+ CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
17
+ version
18
+ FROM c_note
19
+ WHERE id = ?
20
+ `, [id]);
21
+ }
22
+ async function getLinkCount(db, direction, noteId) {
23
+ const [column, boundColumn] = direction === 'in'
24
+ ? ['src_note_id', 'dst_note_id']
25
+ : ['dst_note_id', 'src_note_id'];
26
+ const row = await db.getOptional(`
27
+ SELECT COUNT(1) AS count
28
+ FROM c_note_link l
29
+ JOIN c_note n ON n.id = l.${column}
30
+ WHERE l.${boundColumn} = ?
31
+ AND (l.is_del IS NULL OR l.is_del = 0)
32
+ AND (n.is_del IS NULL OR n.is_del = 0)
33
+ `, [noteId]);
34
+ return Number(row?.count ?? 0);
35
+ }
36
+ function buildPreviewText(contentMd, requestedLines) {
37
+ const normalized = (contentMd ?? '').replace(/\r\n/g, '\n');
38
+ if (!normalized) {
39
+ return {
40
+ preview: '',
41
+ totalLines: 0,
42
+ returnedLines: 0,
43
+ truncated: false,
44
+ };
45
+ }
46
+ const lines = normalized.split('\n');
47
+ const selected = lines.slice(0, requestedLines);
48
+ return {
49
+ preview: selected.join('\n'),
50
+ totalLines: lines.length,
51
+ returnedLines: selected.length,
52
+ truncated: selected.length < lines.length,
53
+ };
54
+ }
55
+ export async function getNoteContext(db, id) {
56
+ const row = await db.getOptional(`
57
+ SELECT
58
+ n.id,
59
+ n.title,
60
+ n.summary,
61
+ n.content_text,
62
+ n.tags,
63
+ n.created_at,
64
+ n.updated_at,
65
+ n.is_del,
66
+ n.starred_at,
67
+ CASE WHEN n.starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
68
+ n.zettel_boxes AS zettel_boxes_raw
69
+ FROM c_note n
70
+ WHERE n.id = ?
71
+ `, [id]);
72
+ if (!row) {
73
+ return null;
74
+ }
75
+ const boxIds = parseZettelBoxIds(row.zettel_boxes_raw);
76
+ const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
77
+ const [backlinks, outlinks] = await Promise.all([
78
+ getLinkCount(db, 'in', id),
79
+ getLinkCount(db, 'out', id),
80
+ ]);
81
+ return {
82
+ id: row.id,
83
+ title: row.title?.trim() ?? '',
84
+ summary: resolveSummary(row.summary, row.content_text),
85
+ tags: parseStoredTags(row.tags),
86
+ created_at: row.created_at,
87
+ updated_at: row.updated_at,
88
+ is_del: row.is_del,
89
+ starred_at: row.starred_at ?? null,
90
+ is_starred: normalizeStoredStarred(row.is_starred),
91
+ zettel_boxes: boxIds
92
+ .map((boxId) => boxNameMap.get(boxId))
93
+ .filter((name) => typeof name === 'string' && name.length > 0),
94
+ links: {
95
+ backlinks,
96
+ outlinks,
97
+ },
98
+ };
99
+ }
100
+ export async function getNotePreview(db, id, requestedLines) {
101
+ const row = await db.getOptional(`
102
+ SELECT
103
+ n.id,
104
+ n.title,
105
+ n.content_md,
106
+ n.updated_at
107
+ FROM c_note n
108
+ WHERE n.id = ?
109
+ `, [id]);
110
+ if (!row) {
111
+ return null;
112
+ }
113
+ const preview = buildPreviewText(row.content_md, requestedLines);
114
+ return {
115
+ id: row.id,
116
+ title: row.title?.trim() ?? '',
117
+ requested_lines: requestedLines,
118
+ total_lines: preview.totalLines,
119
+ returned_lines: preview.returnedLines,
120
+ truncated: preview.truncated,
121
+ preview: preview.preview,
122
+ updated_at: row.updated_at ?? null,
123
+ };
124
+ }
125
+ function mapNoteDetailRow(note, boxNameMap) {
126
+ const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
127
+ const zettelBoxes = boxIds
128
+ .map((boxId) => boxNameMap.get(boxId))
129
+ .filter((name) => typeof name === 'string' && name.length > 0);
130
+ return {
131
+ id: note.id,
132
+ created_at: note.created_at,
133
+ updated_at: note.updated_at,
134
+ title: note.title,
135
+ type: note.type,
136
+ source: note.source,
137
+ tags: parseStoredTags(note.tags),
138
+ is_del: note.is_del,
139
+ is_pub: normalizeStoredFlag(note.is_pub),
140
+ properties: parseStoredNoteProperties(note.properties),
141
+ starred_at: note.starred_at ?? null,
142
+ is_starred: normalizeStoredStarred(note.is_starred),
143
+ content_md: note.content_md,
144
+ content_text: note.content_text,
145
+ content_json: parseStoredJsonValue(note.content_json),
146
+ zettel_boxes: zettelBoxes,
147
+ };
148
+ }
149
+ function parseStoredJsonValue(raw) {
150
+ const value = raw?.trim() ?? '';
151
+ if (!value) {
152
+ return null;
153
+ }
154
+ try {
155
+ return JSON.parse(value);
156
+ }
157
+ catch {
158
+ return raw;
159
+ }
160
+ }
161
+ function parseStoredNoteProperties(raw) {
162
+ const value = raw?.trim() ?? '';
163
+ if (!value) {
164
+ return {};
165
+ }
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(value);
169
+ }
170
+ catch (cause) {
171
+ throw new DinoxError('c_note.properties must be valid JSON', {
172
+ code: 'NOTE_INVALID_PROPERTIES',
173
+ details: { cause: cause instanceof Error ? cause.message : String(cause) },
174
+ recoverable: false,
175
+ });
176
+ }
177
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
178
+ throw new DinoxError('c_note.properties must be a JSON object', {
179
+ code: 'NOTE_INVALID_PROPERTIES',
180
+ recoverable: false,
181
+ });
182
+ }
183
+ return Object.fromEntries(Object.entries(parsed));
184
+ }
185
+ function mapNoteExportRow(note, boxNameMap) {
186
+ const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
187
+ const boxes = boxIds
188
+ .map((boxId) => boxNameMap.get(boxId))
189
+ .filter((name) => typeof name === 'string' && name.length > 0);
190
+ return {
191
+ id: note.id,
192
+ created_at: note.created_at,
193
+ updated_at: note.updated_at,
194
+ title: note.title,
195
+ type: note.type,
196
+ source: note.source,
197
+ summary: note.summary,
198
+ tags: parseStoredTags(note.tags),
199
+ is_del: note.is_del,
200
+ is_pub: normalizeStoredFlag(note.is_pub),
201
+ properties: parseStoredNoteProperties(note.properties),
202
+ starred_at: note.starred_at ?? null,
203
+ is_starred: normalizeStoredStarred(note.is_starred),
204
+ content_md: note.content_md ?? '',
205
+ content_text: note.content_text,
206
+ content_json: parseStoredJsonValue(note.content_json),
207
+ box_ids: boxIds,
208
+ boxes,
209
+ };
210
+ }
211
+ export async function getNoteDetails(db, ids) {
212
+ const targetIds = Array.from(new Set(ids
213
+ .map((id) => id.trim())
214
+ .filter((id) => id.length > 0)));
215
+ if (targetIds.length === 0) {
216
+ return [];
217
+ }
218
+ const noteRowsById = new Map();
219
+ const chunkSize = 500;
220
+ for (let index = 0; index < targetIds.length; index += chunkSize) {
221
+ const batch = targetIds.slice(index, index + chunkSize);
222
+ const placeholders = batch.map(() => '?').join(', ');
223
+ const rows = await db.getAll(`
224
+ SELECT
225
+ id,
226
+ created_at,
227
+ updated_at,
228
+ title,
229
+ type,
230
+ source,
231
+ tags,
232
+ is_del,
233
+ is_pub,
234
+ properties,
235
+ starred_at,
236
+ CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
237
+ content_md,
238
+ content_text,
239
+ content_json,
240
+ zettel_boxes AS zettel_boxes_raw
241
+ FROM c_note
242
+ WHERE id IN (${placeholders})
243
+ `, batch);
244
+ for (const row of rows) {
245
+ const id = row.id?.trim() ?? '';
246
+ if (!id || noteRowsById.has(id)) {
247
+ continue;
248
+ }
249
+ noteRowsById.set(id, row);
250
+ }
251
+ }
252
+ const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
253
+ const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
254
+ const details = [];
255
+ for (const id of targetIds) {
256
+ const row = noteRowsById.get(id);
257
+ if (!row) {
258
+ continue;
259
+ }
260
+ details.push(mapNoteDetailRow(row, boxNameMap));
261
+ }
262
+ return details;
263
+ }
264
+ export async function getNoteDetail(db, id) {
265
+ const details = await getNoteDetails(db, [id]);
266
+ return details[0] ?? null;
267
+ }
268
+ export async function getNoteExportItems(db, ids) {
269
+ const targetIds = Array.from(new Set(ids
270
+ .map((id) => id.trim())
271
+ .filter((id) => id.length > 0)));
272
+ if (targetIds.length === 0) {
273
+ return [];
274
+ }
275
+ const noteRowsById = new Map();
276
+ const chunkSize = 500;
277
+ for (let index = 0; index < targetIds.length; index += chunkSize) {
278
+ const batch = targetIds.slice(index, index + chunkSize);
279
+ const placeholders = batch.map(() => '?').join(', ');
280
+ const rows = await db.getAll(`
281
+ SELECT
282
+ id,
283
+ created_at,
284
+ updated_at,
285
+ title,
286
+ type,
287
+ source,
288
+ tags,
289
+ summary,
290
+ is_del,
291
+ is_pub,
292
+ properties,
293
+ starred_at,
294
+ CASE WHEN starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
295
+ content_md,
296
+ content_text,
297
+ content_json,
298
+ zettel_boxes AS zettel_boxes_raw
299
+ FROM c_note
300
+ WHERE id IN (${placeholders})
301
+ `, batch);
302
+ for (const row of rows) {
303
+ const id = row.id?.trim() ?? '';
304
+ if (!id || noteRowsById.has(id)) {
305
+ continue;
306
+ }
307
+ noteRowsById.set(id, row);
308
+ }
309
+ }
310
+ const allBoxIds = Array.from(new Set(Array.from(noteRowsById.values()).flatMap((row) => parseZettelBoxIds(row.zettel_boxes_raw))));
311
+ const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
312
+ const items = [];
313
+ for (const id of targetIds) {
314
+ const row = noteRowsById.get(id);
315
+ if (!row) {
316
+ continue;
317
+ }
318
+ items.push(mapNoteExportRow(row, boxNameMap));
319
+ }
320
+ return items;
321
+ }
@@ -0,0 +1,8 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
3
+ export declare function validateTagsForUpdate(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
4
+ export declare function validateTagsForBulk(db: AbstractPowerSyncDatabase, requestedTags: string[], userId: string): Promise<string[]>;
5
+ export declare function resolveZettelBoxIdsForCreate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
6
+ export declare function resolveZettelBoxIdsForSearch(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
7
+ export declare function resolveZettelBoxIdsForUpdate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
8
+ export declare function resolveZettelBoxIdsForBulk(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;