@dinoxx/dinox-cli 2.14.0 → 2.16.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 (97) hide show
  1. package/README.md +2 -0
  2. package/dist/ai/ragxClient.d.ts +15 -0
  3. package/dist/ai/ragxClient.js +71 -0
  4. package/dist/cli.js +27 -3
  5. package/dist/commands/batch.d.ts +35 -0
  6. package/dist/commands/batch.js +202 -0
  7. package/dist/commands/boxes/index.js +21 -21
  8. package/dist/commands/mcp.d.ts +6 -0
  9. package/dist/commands/mcp.js +47 -0
  10. package/dist/commands/notes/actions.js +18 -2
  11. package/dist/commands/notes/descriptors.js +111 -5
  12. package/dist/commands/notes/detailContent.d.ts +19 -0
  13. package/dist/commands/notes/detailContent.js +62 -0
  14. package/dist/commands/notes/export.js +4 -0
  15. package/dist/commands/notes/repo/search.d.ts +2 -1
  16. package/dist/commands/notes/repo/search.js +1 -1
  17. package/dist/commands/notes/repo/semantic.d.ts +14 -0
  18. package/dist/commands/notes/repo/semantic.js +31 -0
  19. package/dist/commands/notes/searchResponse.d.ts +3 -0
  20. package/dist/commands/notes/searchResponse.js +1 -0
  21. package/dist/commands/notes/semantic.d.ts +116 -0
  22. package/dist/commands/notes/semantic.js +332 -0
  23. package/dist/commands/prompt/index.js +7 -5
  24. package/dist/commands/savedViews/index.js +12 -3
  25. package/dist/commands/schema/globalOptions.d.ts +6 -0
  26. package/dist/commands/schema/globalOptions.js +6 -0
  27. package/dist/commands/schema/renderSkills.js +5 -2
  28. package/dist/commands/schema/spec.js +18 -7
  29. package/dist/commands/storage/index.js +7 -5
  30. package/dist/commands/sync.js +3 -3
  31. package/dist/commands/tags/index.js +21 -21
  32. package/dist/commands/todo/index.js +22 -2
  33. package/dist/commands/todo/repo.d.ts +5 -0
  34. package/dist/commands/todo/repo.js +7 -2
  35. package/dist/commands/writeReceipt.d.ts +13 -0
  36. package/dist/commands/writeReceipt.js +12 -0
  37. package/dist/config/serviceEndpoints.d.ts +2 -0
  38. package/dist/config/serviceEndpoints.js +2 -0
  39. package/dist/daemon/client.d.ts +2 -0
  40. package/dist/daemon/client.js +22 -2
  41. package/dist/daemon/forward.d.ts +4 -1
  42. package/dist/daemon/forward.js +13 -4
  43. package/dist/daemon/server.d.ts +1 -1
  44. package/dist/daemon/server.js +17 -1
  45. package/dist/daemon/types.d.ts +6 -0
  46. package/dist/dinox.js +12 -10
  47. package/dist/gateway/adapters/acp.d.ts +5 -1
  48. package/dist/gateway/adapters/acp.js +21 -9
  49. package/dist/gateway/adapters/echo.js +7 -4
  50. package/dist/gateway/adapters/index.d.ts +1 -0
  51. package/dist/gateway/adapters/index.js +5 -3
  52. package/dist/gateway/deviceHost.d.ts +3 -1
  53. package/dist/gateway/deviceHost.js +75 -5
  54. package/dist/gateway/mcpCommandTools.d.ts +15 -5
  55. package/dist/gateway/mcpCommandTools.js +30 -19
  56. package/dist/gateway/mcpEnv.d.ts +4 -0
  57. package/dist/gateway/mcpEnv.js +4 -0
  58. package/dist/gateway/mcpGroups.d.ts +3 -0
  59. package/dist/gateway/mcpGroups.js +4 -0
  60. package/dist/gateway/mcpServer.d.ts +10 -1
  61. package/dist/gateway/mcpServer.js +26 -5
  62. package/dist/gateway/protocol.d.ts +31 -2
  63. package/dist/gateway/protocol.js +15 -3
  64. package/dist/gateway/runExecutor.js +23 -12
  65. package/dist/gateway/service.d.ts +2 -0
  66. package/dist/gateway/service.js +29 -4
  67. package/dist/gateway/threadStore.d.ts +2 -0
  68. package/dist/gateway/threadStore.js +13 -0
  69. package/dist/gateway/types.d.ts +5 -4
  70. package/dist/gateway/workspace.d.ts +5 -0
  71. package/dist/gateway/workspace.js +30 -0
  72. package/dist/gateway/workspaceFolders.d.ts +5 -3
  73. package/dist/gateway/workspaceFolders.js +20 -7
  74. package/dist/gateway/workspaceListing.d.ts +5 -0
  75. package/dist/gateway/workspaceListing.js +78 -0
  76. package/dist/powersync/syncPass.js +13 -1
  77. package/dist/powersync/tokenIndex.d.ts +2 -0
  78. package/dist/powersync/tokenIndex.js +1 -0
  79. package/dist/requests/execute.d.ts +11 -0
  80. package/dist/requests/execute.js +26 -0
  81. package/dist/skills/commandRefs.js +2 -10
  82. package/dist/utils/cursor.d.ts +9 -0
  83. package/dist/utils/cursor.js +61 -0
  84. package/dist/utils/defaultFormat.d.ts +8 -0
  85. package/dist/utils/defaultFormat.js +38 -0
  86. package/dist/utils/errorCodes.d.ts +4 -0
  87. package/dist/utils/errorCodes.js +1 -0
  88. package/dist/utils/output.d.ts +2 -0
  89. package/dist/utils/output.js +41 -3
  90. package/dist/utils/progress.d.ts +25 -0
  91. package/dist/utils/progress.js +59 -0
  92. package/dist/utils/requestContext.d.ts +8 -0
  93. package/dist/utils/select.d.ts +20 -0
  94. package/dist/utils/select.js +92 -0
  95. package/dist/utils/usageError.d.ts +37 -0
  96. package/dist/utils/usageError.js +127 -0
  97. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
+ import { encodeCursor, resolveCursorOffset } from '../../utils/cursor.js';
2
3
  import { canonicalizeBidlinksInMarkdown } from '../../utils/bidlinkCanonicalization.js';
3
4
  import { printDryRunPreview } from '../../utils/dryRun.js';
4
5
  import { generateId } from '../../utils/id.js';
@@ -17,6 +18,7 @@ import { buildNoteSearchResponse, normalizeDaysForMeta } from './searchResponse.
17
18
  import { parseMutationListInput, parseNoteType, parsePreviewLineCount, parseSearchLimit, parseSearchListInput, parseSearchOffset, parseStarredOption, parseStringInput, parseStringListInput, parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
18
19
  import { readNoteProperties } from '../properties/repo.js';
19
20
  import { createNoteWithSeed, previewNoteCreateProperties, resolveNoteCreateSeed } from './createFromTemplate.js';
21
+ import { parseNoteContentShape, shapeNoteContent } from './detailContent.js';
20
22
  function toPublicNoteContext(note) {
21
23
  if (!note) {
22
24
  return null;
@@ -249,8 +251,20 @@ export async function runNoteSearch({ command, options, args, globals }) {
249
251
  : { values: [], isExplicitEmptyArray: false };
250
252
  const sqlExpression = typeof searchOptions.sql === 'string' ? searchOptions.sql.trim() : '';
251
253
  const limit = parseSearchLimit(searchOptions.limit);
252
- const offset = parseSearchOffset(searchOptions.offset);
253
254
  const fields = parseNoteSearchFields(searchOptions.fields, '--fields');
255
+ // Raw option values, not the resolved range: `--days` must not change the hash between pages.
256
+ const cursorQuery = {
257
+ keyword,
258
+ includeDeleted: Boolean(searchOptions.includeDeleted),
259
+ tags: tagsExpression,
260
+ from: searchOptions.from,
261
+ to: searchOptions.to,
262
+ days: searchOptions.days,
263
+ starred,
264
+ boxes: boxInput,
265
+ sql: sqlExpression,
266
+ };
267
+ const offset = resolveCursorOffset(searchOptions.cursor, parseSearchOffset(searchOptions.offset), 'note.search', cursorQuery);
254
268
  const execParams = {
255
269
  keyword,
256
270
  includeDeleted: Boolean(searchOptions.includeDeleted),
@@ -278,6 +292,7 @@ export async function runNoteSearch({ command, options, args, globals }) {
278
292
  includeDeleted: Boolean(searchOptions.includeDeleted),
279
293
  limit,
280
294
  offset,
295
+ nextCursor: (nextOffset) => encodeCursor('note.search', nextOffset, cursorQuery),
281
296
  fields,
282
297
  tagsExpression: tagsExpression || undefined,
283
298
  createdAtFrom,
@@ -349,6 +364,7 @@ export async function runNotePreview({ command, options, args }) {
349
364
  }
350
365
  export async function runNoteDetail({ command, options, args }) {
351
366
  const detailOptions = options;
367
+ const contentShape = parseNoteContentShape(detailOptions.content, detailOptions.maxChars);
352
368
  await withNoteDatabase(command, async (runtime) => {
353
369
  const { db } = runtime;
354
370
  await syncBeforeNoteCommand(runtime);
@@ -359,7 +375,7 @@ export async function runNoteDetail({ command, options, args }) {
359
375
  if (missing.length > 0) {
360
376
  throw new DinoxError(ids.length === 1 ? `Note not found: ${missing[0]}` : `Notes not found: ${missing.join(', ')}`);
361
377
  }
362
- const publicNotes = notes.map((note) => toPublicNoteDetail(note));
378
+ const publicNotes = notes.map((note) => shapeNoteContent(toPublicNoteDetail(note), contentShape));
363
379
  printSuccess({
364
380
  stale: commandStale(runtime),
365
381
  count: publicNotes.length,
@@ -1,11 +1,28 @@
1
1
  import { registerCommandGroup } from '../descriptor/register.js';
2
- import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
2
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, PROGRESS_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
3
3
  import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
4
4
  import { runNoteCreate, runNoteDelete, runNoteDetail, runNoteGet, runNoteMove, runNotePreview, runNoteSearch, runNoteStar, runNoteTag, runNoteUnstar, runNoteUpdate, } from './actions.js';
5
5
  import { runNoteBulk as runNoteBulkAction } from './bulk.js';
6
6
  import { runNoteContentRead, runNotePatch } from './contentPatchCommand.js';
7
7
  import { runNoteExport } from './export.js';
8
+ import { runNoteContext, runNoteSemantic, runNoteSimilar, SEMANTIC_LIMITS } from './semantic.js';
8
9
  const NOTE_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
10
+ // Semantic commands query the online index; they refuse --offline.
11
+ const NOTE_SEMANTIC_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
12
+ const SEMANTIC_FILTER_OPTIONS = [
13
+ { flags: '--tags <string|@file>', description: 'Only notes with all these tags (JSON array or comma list; child tags included)', type: 'string' },
14
+ { flags: '--boxes <string|@file>', description: 'Only notes in any of these box paths or unique names (JSON array or comma list)', type: 'string' },
15
+ { flags: '--from <date>', description: 'created_at start (YYYY-MM-DD or ISO datetime)', type: 'string' },
16
+ { flags: '--to <date>', description: 'created_at end (YYYY-MM-DD or ISO datetime)', type: 'string' },
17
+ { flags: '--days <n>', description: 'Recent N days by created_at; mutually exclusive with --from/--to', type: 'number' },
18
+ ];
19
+ const SEMANTIC_MODE_OPTION = {
20
+ flags: '--mode <mode>',
21
+ description: 'hybrid (meaning + keywords, default), semantic (meaning only), or keyword',
22
+ type: 'string',
23
+ enum: ['hybrid', 'semantic', 'keyword'],
24
+ };
25
+ const SEMANTIC_NOTE_FIELDS = ['id', 'title', 'tags', 'boxes', 'created_at', 'is_starred', 'score', 'section', 'heading_path', 'snippet', 'matched_source', 'matches', 'in_local_cache'];
9
26
  const NOTE_WRITE_RECEIPT_FIELDS = [
10
27
  'durability',
11
28
  'upload_queue_remaining',
@@ -42,6 +59,7 @@ export const noteCommandGroup = {
42
59
  { flags: '--sql <expr>', description: 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type/is_starred', type: 'string' },
43
60
  { flags: '--limit <n>', description: 'Maximum returned notes', type: 'number' },
44
61
  { flags: '--offset <n>', description: 'Result offset for pagination', type: 'number', defaultValue: 0 },
62
+ { flags: '--cursor <token>', description: 'Continue from meta.pagination.next_cursor of the previous page (same query)' },
45
63
  { flags: '--fields <list>', description: 'Comma-separated fields: id,title,summary,tags,created_at,starred_at,boxes,is_starred', type: 'string[]' },
46
64
  { flags: '--include-deleted', description: 'Include soft-deleted notes', type: 'boolean' },
47
65
  ],
@@ -60,10 +78,90 @@ export const noteCommandGroup = {
60
78
  examples: [
61
79
  'dino note search "AI" --days 7 --format json',
62
80
  'dino note search --tags "(work OR life) AND NOT archived" --format json',
81
+ 'dino note search "AI" --cursor <meta.pagination.next_cursor> --format json',
63
82
  ],
64
83
  },
65
84
  action: runNoteSearch,
66
85
  },
86
+ {
87
+ name: 'semantic',
88
+ description: 'Find notes by meaning through the Dinox semantic index (online)',
89
+ arguments: [{ syntax: '<query>', description: 'Natural-language query' }],
90
+ options: [
91
+ SEMANTIC_MODE_OPTION,
92
+ { flags: '--limit <n>', description: `Maximum returned notes (1-${SEMANTIC_LIMITS.search.max}, default ${SEMANTIC_LIMITS.search.default})`, type: 'number' },
93
+ ...SEMANTIC_FILTER_OPTIONS,
94
+ { flags: '--include-resources', description: 'Also match attachments (PDFs, images, web clips); hits report their parent note', type: 'boolean' },
95
+ ],
96
+ schema: {
97
+ path: 'note.semantic',
98
+ risk: 'read',
99
+ auth: 'required',
100
+ globalOptions: NOTE_SEMANTIC_GLOBAL_OPTIONS,
101
+ output: {
102
+ type: 'structured',
103
+ summary: 'Notes ranked by semantic relevance, one entry per note with its best-matching snippet',
104
+ entity: 'semantic_note',
105
+ fields: SEMANTIC_NOTE_FIELDS,
106
+ metaFields: ['query', 'mode', 'returned_count', 'limit', 'include_resources', 'dropped_deleted_count', 'applied_filters'],
107
+ },
108
+ examples: [
109
+ 'dino note semantic "how I planned the Q3 launch" --format json',
110
+ 'dino note semantic "sleep and productivity" --tags health --days 90 --limit 20 --format json',
111
+ ],
112
+ },
113
+ action: runNoteSemantic,
114
+ },
115
+ {
116
+ name: 'similar',
117
+ description: 'Find notes semantically similar to a note (online)',
118
+ arguments: [{ syntax: '<id>', description: 'Note id' }],
119
+ options: [
120
+ { flags: '--limit <n>', description: `Maximum returned notes (1-${SEMANTIC_LIMITS.similar.max}, default ${SEMANTIC_LIMITS.similar.default})`, type: 'number' },
121
+ ],
122
+ schema: {
123
+ path: 'note.similar',
124
+ risk: 'read',
125
+ auth: 'required',
126
+ globalOptions: NOTE_SEMANTIC_GLOBAL_OPTIONS,
127
+ output: {
128
+ type: 'structured',
129
+ summary: 'Semantically similar notes, excluding the note itself',
130
+ entity: 'semantic_note',
131
+ fields: SEMANTIC_NOTE_FIELDS,
132
+ metaFields: ['note_id', 'returned_count', 'limit', 'dropped_deleted_count'],
133
+ },
134
+ examples: ['dino note similar <id> --limit 10 --format json'],
135
+ },
136
+ action: runNoteSimilar,
137
+ },
138
+ {
139
+ name: 'context',
140
+ description: 'Assemble cited, token-budgeted context from notes to answer a question (online)',
141
+ arguments: [{ syntax: '<query>', description: 'Question to gather context for' }],
142
+ options: [
143
+ SEMANTIC_MODE_OPTION,
144
+ { flags: '--max-tokens <n>', description: `Context token budget (1-${SEMANTIC_LIMITS.contextTokens.max}, default ${SEMANTIC_LIMITS.contextTokens.default})`, type: 'number' },
145
+ { flags: '--max-notes <n>', description: `Maximum notes contributing blocks (1-${SEMANTIC_LIMITS.contextGroups.max}, default ${SEMANTIC_LIMITS.contextGroups.default})`, type: 'number' },
146
+ { flags: '--limit <n>', description: `Retrieved chunks before assembly (1-${SEMANTIC_LIMITS.contextTopK.max}, default ${SEMANTIC_LIMITS.contextTopK.default})`, type: 'number' },
147
+ ...SEMANTIC_FILTER_OPTIONS,
148
+ { flags: '--notes-only', description: 'Leave attachments out of the context', type: 'boolean' },
149
+ ],
150
+ schema: {
151
+ path: 'note.context',
152
+ risk: 'read',
153
+ auth: 'required',
154
+ globalOptions: NOTE_SEMANTIC_GLOBAL_OPTIONS,
155
+ output: {
156
+ type: 'structured',
157
+ summary: 'Context blocks (content with note id and citations) ready to ground an answer',
158
+ fields: ['blocks.ref', 'blocks.note_id', 'blocks.title', 'blocks.role', 'blocks.score', 'blocks.token_count', 'blocks.content', 'blocks.citations'],
159
+ metaFields: ['query', 'mode', 'returned_count', 'context_tokens', 'max_tokens', 'dropped_deleted_count'],
160
+ },
161
+ examples: ['dino note context "what did I decide about pricing?" --max-tokens 4000 --format json'],
162
+ },
163
+ action: runNoteContext,
164
+ },
67
165
  {
68
166
  name: 'get',
69
167
  description: 'Get lightweight note context by id without exposing full note content',
@@ -107,7 +205,11 @@ export const noteCommandGroup = {
107
205
  name: 'detail',
108
206
  description: 'Get full note details for one or more ids',
109
207
  arguments: [{ syntax: '[id]', description: 'Optional note id when reading a single note' }],
110
- options: [{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' }],
208
+ options: [
209
+ { flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
210
+ { flags: '--content <formats>', description: 'Content fields to return: comma-separated md,text,json, or none (default: all three)', type: 'string' },
211
+ { flags: '--max-chars <n>', description: 'Shorten content_md and content_text to n characters; sets content_truncated', type: 'number' },
212
+ ],
111
213
  schema: {
112
214
  path: 'note.detail',
113
215
  risk: 'read',
@@ -116,9 +218,13 @@ export const noteCommandGroup = {
116
218
  output: {
117
219
  type: 'structured',
118
220
  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'],
221
+ fields: ['stale', 'count', 'notes.id', 'notes.title', 'notes.content_md', 'notes.content_text', 'notes.content_json', 'notes.content_truncated', '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
222
  },
121
- examples: ['dino note detail <id> --format json', 'dino note detail --ids @/tmp/note-ids.txt --format json'],
223
+ examples: [
224
+ 'dino note detail <id> --format json',
225
+ 'dino note detail <id> --content md --max-chars 4000 --format json',
226
+ 'dino note detail --ids @/tmp/note-ids.txt --format json',
227
+ ],
122
228
  },
123
229
  action: runNoteDetail,
124
230
  },
@@ -137,7 +243,7 @@ export const noteCommandGroup = {
137
243
  path: 'note.export',
138
244
  risk: 'read',
139
245
  auth: 'required',
140
- globalOptions: NOTE_GLOBAL_OPTIONS,
246
+ globalOptions: [...NOTE_GLOBAL_OPTIONS, ...PROGRESS_OPTIONS],
141
247
  output: {
142
248
  type: 'structured',
143
249
  summary: 'Export payload or file write acknowledgement for note backup/migration',
@@ -0,0 +1,19 @@
1
+ declare const CONTENT_FIELDS: {
2
+ readonly md: "content_md";
3
+ readonly text: "content_text";
4
+ readonly json: "content_json";
5
+ };
6
+ type NoteContentFormat = keyof typeof CONTENT_FIELDS;
7
+ export type NoteContentShape = {
8
+ formats: ReadonlySet<NoteContentFormat>;
9
+ maxChars: number | null;
10
+ };
11
+ /** Parses `--content md,text,json|none` (default: all three, as before). */
12
+ export declare function parseNoteContentShape(content: unknown, maxChars: unknown): NoteContentShape;
13
+ /**
14
+ * Keeps the requested content fields and shortens Markdown / plain text to
15
+ * `maxChars` characters (whole code points). `content_truncated` reports
16
+ * whether anything was cut, so an agent knows to fetch more.
17
+ */
18
+ export declare function shapeNoteContent(note: Record<string, unknown>, shape: NoteContentShape): Record<string, unknown>;
19
+ export {};
@@ -0,0 +1,62 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ const CONTENT_FIELDS = { md: 'content_md', text: 'content_text', json: 'content_json' };
3
+ /** Fields that --max-chars shortens; content_json is a document and is never cut. */
4
+ const TRUNCATABLE_FIELDS = ['content_md', 'content_text'];
5
+ /** Parses `--content md,text,json|none` (default: all three, as before). */
6
+ export function parseNoteContentShape(content, maxChars) {
7
+ let formats = new Set(['md', 'text', 'json']);
8
+ if (content !== undefined) {
9
+ const requested = String(content).split(',').map((value) => value.trim().toLowerCase()).filter(Boolean);
10
+ formats = new Set();
11
+ for (const value of requested) {
12
+ if (value === 'none' && requested.length === 1)
13
+ break;
14
+ if (!(value in CONTENT_FIELDS)) {
15
+ throw new DinoxError(`Invalid --content value: ${value}`, {
16
+ code: 'INVALID_ARGUMENT',
17
+ recoverable: true,
18
+ suggestion: 'Use a comma-separated list of md, text, json, or none.',
19
+ });
20
+ }
21
+ formats.add(value);
22
+ }
23
+ if (requested.length === 0) {
24
+ throw new DinoxError('--content needs md, text, json, or none', { code: 'INVALID_ARGUMENT', recoverable: true });
25
+ }
26
+ }
27
+ let limit = null;
28
+ if (maxChars !== undefined) {
29
+ limit = Number(maxChars);
30
+ if (!Number.isInteger(limit) || limit < 1) {
31
+ throw new DinoxError('--max-chars must be a positive integer', { code: 'INVALID_ARGUMENT', recoverable: true });
32
+ }
33
+ }
34
+ return { formats, maxChars: limit };
35
+ }
36
+ /**
37
+ * Keeps the requested content fields and shortens Markdown / plain text to
38
+ * `maxChars` characters (whole code points). `content_truncated` reports
39
+ * whether anything was cut, so an agent knows to fetch more.
40
+ */
41
+ export function shapeNoteContent(note, shape) {
42
+ const shaped = { ...note };
43
+ for (const [format, field] of Object.entries(CONTENT_FIELDS)) {
44
+ if (!shape.formats.has(format))
45
+ delete shaped[field];
46
+ }
47
+ if (shape.maxChars === null)
48
+ return shaped;
49
+ let truncated = false;
50
+ for (const field of TRUNCATABLE_FIELDS) {
51
+ const value = shaped[field];
52
+ if (typeof value !== 'string')
53
+ continue;
54
+ const characters = Array.from(value);
55
+ if (characters.length <= shape.maxChars)
56
+ continue;
57
+ shaped[field] = characters.slice(0, shape.maxChars).join('');
58
+ truncated = true;
59
+ }
60
+ shaped.content_truncated = truncated;
61
+ return shaped;
62
+ }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { stringify as stringifyYaml } from 'yaml';
4
4
  import { DinoxError } from '../../utils/errors.js';
5
5
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanReadableBlock, printSuccess, toJson, } from '../../utils/output.js';
6
+ import { reportProgress } from '../../utils/progress.js';
6
7
  import { commandStale, } from '../runtime.js';
7
8
  import { getNoteExportItems, } from './repo.js';
8
9
  import { parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
@@ -167,6 +168,7 @@ export async function writeExportFiles(args) {
167
168
  await fs.mkdir(path.dirname(targets[0].path), { recursive: true, mode: 0o700 });
168
169
  }
169
170
  const noteById = new Map(args.notes.map((note) => [note.id, note]));
171
+ let written = 0;
170
172
  for (const target of targets) {
171
173
  const note = noteById.get(target.id);
172
174
  if (!note) {
@@ -183,6 +185,7 @@ export async function writeExportFiles(args) {
183
185
  if (process.platform !== 'win32') {
184
186
  await fs.chmod(target.path, 0o600);
185
187
  }
188
+ reportProgress({ phase: 'write', current: ++written, total: targets.length });
186
189
  }
187
190
  return targets;
188
191
  }
@@ -203,6 +206,7 @@ export async function runNoteExport({ command, options, args }) {
203
206
  const { db } = runtime;
204
207
  await syncBeforeNoteCommand(runtime);
205
208
  const ids = await parseTargetNoteIds(args.id, exportOptions.ids, 'detail');
209
+ reportProgress({ phase: 'read', total: ids.length });
206
210
  const notes = await getNoteExportItems(db, ids);
207
211
  assertNoMissingNotes(ids, notes);
208
212
  assertActiveNotes(notes, Boolean(exportOptions.includeDeleted));
@@ -1,5 +1,5 @@
1
1
  import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
- import { type SearchNotesOptions, type SearchedNote } from './shared.js';
2
+ import { type SearchNotesOptions, type SearchResultRow, type SearchedNote } from './shared.js';
3
3
  /**
4
4
  * JSON-safe note-search request. Box *names/paths* are carried (not resolved ids) so the
5
5
  * same payload can be executed either in-process or sent to the daemon over the wire;
@@ -39,4 +39,5 @@ type SearchNotesPageResult = {
39
39
  totalCount: number;
40
40
  };
41
41
  export declare function searchNotesPage(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesPageOptions): Promise<SearchNotesPageResult>;
42
+ export declare function enrichSearchRows(db: AbstractPowerSyncDatabase, rows: SearchResultRow[]): Promise<SearchedNote[]>;
42
43
  export {};
@@ -196,7 +196,7 @@ function appendPaginationParams(params, limit, offset) {
196
196
  }
197
197
  return [...params, limit, offset ?? 0];
198
198
  }
199
- async function enrichSearchRows(db, rows) {
199
+ export async function enrichSearchRows(db, rows) {
200
200
  const allBoxIds = Array.from(new Set(rows.flatMap((row) => parseZettelBoxIds(row.zettel_boxes))));
201
201
  const boxNameMap = await loadZettelBoxNameMap(db, allBoxIds);
202
202
  return rows.map((row) => {
@@ -0,0 +1,14 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { SearchedNote } from './shared.js';
3
+ export type LocalNoteLookup = {
4
+ /** Live notes found in the local cache, by id. */
5
+ live: Map<string, SearchedNote>;
6
+ /** Ids the local cache knows to be deleted. */
7
+ deleted: Set<string>;
8
+ };
9
+ /**
10
+ * Looks up remote search hits in the local cache: live notes get the same
11
+ * fields as `note search`; deleted ones are reported so the caller can drop
12
+ * them. Ids missing locally appear in neither (not synced yet).
13
+ */
14
+ export declare function lookupLocalNotes(db: AbstractPowerSyncDatabase, ids: string[]): Promise<LocalNoteLookup>;
@@ -0,0 +1,31 @@
1
+ import { enrichSearchRows } from './search.js';
2
+ const LOOKUP_CHUNK = 500;
3
+ /**
4
+ * Looks up remote search hits in the local cache: live notes get the same
5
+ * fields as `note search`; deleted ones are reported so the caller can drop
6
+ * them. Ids missing locally appear in neither (not synced yet).
7
+ */
8
+ export async function lookupLocalNotes(db, ids) {
9
+ const unique = Array.from(new Set(ids.filter(Boolean)));
10
+ const live = new Map();
11
+ const deleted = new Set();
12
+ for (let start = 0; start < unique.length; start += LOOKUP_CHUNK) {
13
+ const chunk = unique.slice(start, start + LOOKUP_CHUNK);
14
+ const rows = await db.getAll(`
15
+ SELECT n.id, n.title, n.summary, n.content_text, n.tags, n.zettel_boxes, n.created_at, n.starred_at,
16
+ CASE WHEN n.starred_at IS NOT NULL THEN 1 ELSE 0 END AS is_starred,
17
+ n.is_del
18
+ FROM c_note n
19
+ WHERE n.id IN (${chunk.map(() => '?').join(', ')})
20
+ `, chunk);
21
+ const liveRows = rows.filter((row) => {
22
+ const isDeleted = row.is_del !== null && Number(row.is_del) !== 0;
23
+ if (isDeleted && row.id)
24
+ deleted.add(row.id);
25
+ return !isDeleted;
26
+ });
27
+ for (const note of await enrichSearchRows(db, liveRows))
28
+ live.set(note.id, note);
29
+ }
30
+ return { live, deleted };
31
+ }
@@ -9,6 +9,8 @@ export type NoteSearchResponse = {
9
9
  limit: number | null;
10
10
  offset: number;
11
11
  has_more: boolean;
12
+ /** Opaque token for the next page (`--cursor`); null on the last page. */
13
+ next_cursor: string | null;
12
14
  };
13
15
  fields: NoteSearchField[] | null;
14
16
  applied_filters: {
@@ -36,6 +38,7 @@ export type BuildNoteSearchResponseInput = {
36
38
  includeDeleted: boolean;
37
39
  limit?: number;
38
40
  offset?: number;
41
+ nextCursor?: (nextOffset: number) => string;
39
42
  fields?: NoteSearchField[];
40
43
  tagsExpression?: string;
41
44
  createdAtFrom?: string;
@@ -32,6 +32,7 @@ export function buildNoteSearchResponse(input) {
32
32
  limit,
33
33
  offset,
34
34
  has_more: hasMore,
35
+ next_cursor: hasMore && input.nextCursor ? input.nextCursor(offset + returnedCount) : null,
35
36
  },
36
37
  fields: input.fields && input.fields.length > 0 ? [...input.fields] : null,
37
38
  applied_filters: {
@@ -0,0 +1,116 @@
1
+ import type { CommandExecutionContext } from '../descriptor/types.js';
2
+ import { type LocalNoteLookup } from './repo/semantic.js';
3
+ export declare const SEMANTIC_MODES: {
4
+ readonly hybrid: "hybrid";
5
+ readonly semantic: "semantic";
6
+ readonly keyword: "fts";
7
+ };
8
+ export type SemanticMode = keyof typeof SEMANTIC_MODES;
9
+ export declare const SEMANTIC_LIMITS: {
10
+ readonly search: {
11
+ readonly default: 10;
12
+ readonly max: 60;
13
+ };
14
+ readonly similar: {
15
+ readonly default: 6;
16
+ readonly max: 20;
17
+ };
18
+ readonly contextTopK: {
19
+ readonly default: 12;
20
+ readonly max: 60;
21
+ };
22
+ readonly contextGroups: {
23
+ readonly default: 6;
24
+ readonly max: 20;
25
+ };
26
+ readonly contextTokens: {
27
+ readonly default: 2400;
28
+ readonly max: 16000;
29
+ };
30
+ };
31
+ type RagxAnchor = {
32
+ sourceType?: string | null;
33
+ sourceId?: string | null;
34
+ };
35
+ type RagxGroup = {
36
+ kind?: string | null;
37
+ id?: string | null;
38
+ };
39
+ export type RagxHit = {
40
+ anchor?: RagxAnchor | null;
41
+ group?: RagxGroup | null;
42
+ docTitle?: string | null;
43
+ sectionTitle?: string | null;
44
+ headingPath?: string[] | null;
45
+ snippet?: string | null;
46
+ scores?: {
47
+ final?: number | null;
48
+ } | null;
49
+ };
50
+ export type RagxContextBlock = {
51
+ ref?: string | null;
52
+ group?: RagxGroup | null;
53
+ title?: string | null;
54
+ retrievalRole?: string | null;
55
+ content?: string | null;
56
+ score?: number | null;
57
+ tokenCount?: number | null;
58
+ citations?: Array<{
59
+ anchor?: RagxAnchor | null;
60
+ }> | null;
61
+ };
62
+ export type SemanticNote = {
63
+ id: string;
64
+ title: string;
65
+ tags: string[];
66
+ boxes: string[];
67
+ created_at: string | null;
68
+ is_starred: boolean;
69
+ score: number | null;
70
+ section: string | null;
71
+ heading_path: string[];
72
+ snippet: string;
73
+ /** Whether the best match came from the note itself or from an attachment. */
74
+ matched_source: 'note' | 'resource';
75
+ /** Chunks of this note among the hits. */
76
+ matches: number;
77
+ /** False when the note is not in the local cache yet (fields come from the index). */
78
+ in_local_cache: boolean;
79
+ };
80
+ export type SemanticFilters = {
81
+ tags?: string[];
82
+ cardboxIds?: string[];
83
+ startDateTime?: string;
84
+ endDateTime?: string;
85
+ endExclusive?: false;
86
+ };
87
+ export declare function parseSemanticMode(value: unknown): SemanticMode;
88
+ /**
89
+ * Folds ranked chunk hits into one entry per note (keeping the best chunk),
90
+ * fills in local fields, and drops notes the local cache knows are deleted.
91
+ */
92
+ export declare function mergeHitsIntoNotes(hits: RagxHit[], lookup: LocalNoteLookup, limit: number): {
93
+ notes: SemanticNote[];
94
+ droppedDeleted: number;
95
+ };
96
+ /** Context blocks with the note id surfaced and blocks of locally deleted notes removed. */
97
+ export declare function shapeContextBlocks(blocks: RagxContextBlock[], lookup: LocalNoteLookup): {
98
+ blocks: {
99
+ ref: string | null;
100
+ note_id: string | null;
101
+ title: string;
102
+ role: string | null;
103
+ score: number | null;
104
+ token_count: number | null;
105
+ content: string;
106
+ citations: {
107
+ source_type: string;
108
+ source_id: string;
109
+ }[];
110
+ }[];
111
+ droppedDeleted: number;
112
+ };
113
+ export declare function runNoteSemantic(context: CommandExecutionContext): Promise<void>;
114
+ export declare function runNoteSimilar(context: CommandExecutionContext): Promise<void>;
115
+ export declare function runNoteContext(context: CommandExecutionContext): Promise<void>;
116
+ export {};