@dinoxx/dinox-cli 1.0.14 → 1.0.16

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 (52) hide show
  1. package/dist/auth/userInfo.d.ts +1 -0
  2. package/dist/auth/userInfo.js +29 -10
  3. package/dist/cli.js +6 -0
  4. package/dist/commands/auth/index.js +16 -19
  5. package/dist/commands/boxes/index.js +1 -10
  6. package/dist/commands/graph/index.d.ts +2 -0
  7. package/dist/commands/graph/index.js +98 -0
  8. package/dist/commands/graph/repo.d.ts +33 -0
  9. package/dist/commands/graph/repo.js +76 -0
  10. package/dist/commands/mcp/auth.d.ts +4 -0
  11. package/dist/commands/mcp/auth.js +75 -0
  12. package/dist/commands/mcp/index.d.ts +2 -0
  13. package/dist/commands/mcp/index.js +84 -0
  14. package/dist/commands/mcp/server.d.ts +30 -0
  15. package/dist/commands/mcp/server.js +324 -0
  16. package/dist/commands/mcp/tools.d.ts +2 -0
  17. package/dist/commands/mcp/tools.js +90 -0
  18. package/dist/commands/mcp/types.d.ts +24 -0
  19. package/dist/commands/mcp/types.js +1 -0
  20. package/dist/commands/notes/index.d.ts +39 -0
  21. package/dist/commands/notes/index.js +61 -12
  22. package/dist/commands/prompt/index.js +1 -10
  23. package/dist/commands/sync.js +1 -11
  24. package/dist/commands/tags/index.js +1 -10
  25. package/dist/commands/todo/index.js +1 -10
  26. package/dist/config/resolve.js +3 -2
  27. package/dist/config/store.js +17 -2
  28. package/dist/config/types.d.ts +1 -2
  29. package/dist/daemon/client.d.ts +2 -0
  30. package/dist/daemon/client.js +86 -0
  31. package/dist/daemon/ensure.d.ts +1 -0
  32. package/dist/daemon/ensure.js +23 -0
  33. package/dist/daemon/index.d.ts +2 -0
  34. package/dist/daemon/index.js +151 -0
  35. package/dist/daemon/pid.d.ts +10 -0
  36. package/dist/daemon/pid.js +55 -0
  37. package/dist/daemon/server.d.ts +10 -0
  38. package/dist/daemon/server.js +137 -0
  39. package/dist/daemon/types.d.ts +19 -0
  40. package/dist/daemon/types.js +1 -0
  41. package/dist/powersync/connector.d.ts +2 -1
  42. package/dist/powersync/connector.js +27 -9
  43. package/dist/powersync/runtime.js +4 -9
  44. package/dist/powersync/uploader.d.ts +1 -0
  45. package/dist/powersync/uploader.js +36 -13
  46. package/dist/utils/abort.d.ts +6 -0
  47. package/dist/utils/abort.js +23 -0
  48. package/dist/utils/authToken.d.ts +2 -0
  49. package/dist/utils/authToken.js +23 -0
  50. package/dist/utils/syncTimeout.d.ts +1 -0
  51. package/dist/utils/syncTimeout.js +19 -0
  52. package/package.json +1 -1
@@ -0,0 +1,324 @@
1
+ import { createServer } from 'node:http';
2
+ import { mcpTools } from './tools.js';
3
+ import { isAuthorizedMCPRequest } from './auth.js';
4
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
5
+ import { loadConfig } from '../../config/store.js';
6
+ import { resolveConfig } from '../../config/resolve.js';
7
+ import { connectPowerSync } from '../../powersync/runtime.js';
8
+ import { parseCreatedAtRange } from '../notes/searchTime.js';
9
+ import { createNote, getNoteDetails, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForCreate, } from '../notes/repo.js';
10
+ import { getBacklinks, getOutlinks, getRelatedNotes } from '../graph/repo.js';
11
+ import { searchTodoTasks } from '../todo/repo.js';
12
+ import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
13
+ import { generateId } from '../../utils/id.js';
14
+ import { getPackageVersion } from '../../utils/version.js';
15
+ const MAX_MCP_REQUEST_BYTES = 1 * 1024 * 1024;
16
+ const DEFAULT_SEARCH_LIMIT = 50;
17
+ const MAX_SEARCH_LIMIT = 500;
18
+ function normalizeLimit(value) {
19
+ if (value === undefined || value === null) {
20
+ return DEFAULT_SEARCH_LIMIT;
21
+ }
22
+ const raw = typeof value === 'number'
23
+ ? value
24
+ : typeof value === 'string'
25
+ ? Number(value)
26
+ : NaN;
27
+ if (!Number.isFinite(raw)) {
28
+ return DEFAULT_SEARCH_LIMIT;
29
+ }
30
+ const int = Math.trunc(raw);
31
+ if (int <= 0) {
32
+ return DEFAULT_SEARCH_LIMIT;
33
+ }
34
+ return Math.min(int, MAX_SEARCH_LIMIT);
35
+ }
36
+ function normalizeStringArray(value) {
37
+ if (!Array.isArray(value)) {
38
+ return [];
39
+ }
40
+ return value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean);
41
+ }
42
+ function parseBoxFilter(value) {
43
+ if (Array.isArray(value)) {
44
+ return { names: normalizeStringArray(value), matchEmpty: false };
45
+ }
46
+ if (typeof value !== 'string') {
47
+ return { names: [], matchEmpty: false };
48
+ }
49
+ const trimmed = value.trim();
50
+ if (!trimmed) {
51
+ return { names: [], matchEmpty: false };
52
+ }
53
+ if (trimmed === '[]') {
54
+ return { names: [], matchEmpty: true };
55
+ }
56
+ const names = trimmed
57
+ .split(/[,\n]/)
58
+ .map((name) => name.trim())
59
+ .filter(Boolean);
60
+ return { names, matchEmpty: false };
61
+ }
62
+ async function readJsonBody(req) {
63
+ return new Promise((resolve, reject) => {
64
+ const chunks = [];
65
+ let totalBytes = 0;
66
+ req.on('data', (chunk) => {
67
+ totalBytes += chunk.length;
68
+ if (totalBytes > MAX_MCP_REQUEST_BYTES) {
69
+ req.destroy(new Error('Request body too large'));
70
+ return;
71
+ }
72
+ chunks.push(chunk);
73
+ });
74
+ req.on('end', () => {
75
+ try {
76
+ const body = Buffer.concat(chunks).toString('utf-8');
77
+ resolve(JSON.parse(body));
78
+ }
79
+ catch (error) {
80
+ reject(error);
81
+ }
82
+ });
83
+ req.on('error', reject);
84
+ });
85
+ }
86
+ export class MCPServer {
87
+ db = null;
88
+ async initialize() {
89
+ const config = resolveConfig(await loadConfig());
90
+ const { db } = await connectPowerSync({ config, offline: false, timeoutMs: config.sync.timeoutMs });
91
+ this.db = db;
92
+ }
93
+ async handleRequest(req) {
94
+ try {
95
+ switch (req.method) {
96
+ case 'initialize':
97
+ return this.handleInitialize(req);
98
+ case 'tools/list':
99
+ return this.handleToolsList(req);
100
+ case 'tools/call':
101
+ return await this.handleToolsCall(req);
102
+ default:
103
+ return {
104
+ jsonrpc: '2.0',
105
+ id: req.id,
106
+ error: { code: -32601, message: 'Method not found' },
107
+ };
108
+ }
109
+ }
110
+ catch (error) {
111
+ return {
112
+ jsonrpc: '2.0',
113
+ id: req.id,
114
+ error: { code: -32603, message: error.message || 'Internal error' },
115
+ };
116
+ }
117
+ }
118
+ handleInitialize(req) {
119
+ const serverInfo = {
120
+ name: 'dinox-mcp-server',
121
+ version: getPackageVersion(),
122
+ protocolVersion: '2024-11-05',
123
+ capabilities: {
124
+ tools: true,
125
+ resources: false,
126
+ },
127
+ };
128
+ return { jsonrpc: '2.0', id: req.id, result: serverInfo };
129
+ }
130
+ handleToolsList(req) {
131
+ return { jsonrpc: '2.0', id: req.id, result: { tools: mcpTools } };
132
+ }
133
+ async handleToolsCall(req) {
134
+ if (!this.db) {
135
+ throw new Error('Database not initialized');
136
+ }
137
+ const name = req.params?.name;
138
+ const args = req.params?.arguments ?? {};
139
+ if (typeof name !== 'string') {
140
+ throw new Error('Invalid tools/call: missing tool name');
141
+ }
142
+ switch (name) {
143
+ case 'dinox_search_notes': {
144
+ const keyword = typeof args.query === 'string' ? args.query : '';
145
+ const tagsExpression = typeof args.tags === 'string' ? args.tags.trim() : '';
146
+ const { names: boxNames, matchEmpty: matchEmptyBoxes } = parseBoxFilter(args.box);
147
+ const zettelBoxIds = await resolveZettelBoxIdsForSearch(this.db, boxNames);
148
+ const daysInput = typeof args.days === 'number' ? String(args.days) : args.days;
149
+ const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
150
+ from: args.from,
151
+ to: args.to,
152
+ days: daysInput,
153
+ });
154
+ const limit = normalizeLimit(args.limit);
155
+ const rows = await searchNotes(this.db, keyword, {
156
+ includeDeleted: false,
157
+ tagsExpression: tagsExpression || undefined,
158
+ createdAtFrom,
159
+ createdAtTo,
160
+ zettelBoxIds,
161
+ matchEmptyZettelBoxes: matchEmptyBoxes,
162
+ });
163
+ const result = rows.slice(0, limit);
164
+ const text = JSON.stringify(result);
165
+ return {
166
+ jsonrpc: '2.0',
167
+ id: req.id,
168
+ result: { content: [{ type: 'text', text }] },
169
+ };
170
+ }
171
+ case 'dinox_get_note': {
172
+ if (typeof args.id !== 'string' || !args.id.trim()) {
173
+ throw new Error('dinox_get_note: id is required');
174
+ }
175
+ const notes = await getNoteDetails(this.db, [args.id.trim()]);
176
+ const text = JSON.stringify(notes[0] ?? null);
177
+ return {
178
+ jsonrpc: '2.0',
179
+ id: req.id,
180
+ result: { content: [{ type: 'text', text }] },
181
+ };
182
+ }
183
+ case 'dinox_create_note': {
184
+ const title = typeof args.title === 'string' ? args.title.trim() : '';
185
+ const contentMd = typeof args.content === 'string' ? args.content : '';
186
+ if (!title) {
187
+ throw new Error('dinox_create_note: title is required');
188
+ }
189
+ if (!contentMd) {
190
+ throw new Error('dinox_create_note: content is required');
191
+ }
192
+ const rawConfig = await loadConfig();
193
+ const config = resolveConfig(rawConfig);
194
+ const identity = await resolveAuthIdentity(rawConfig, config);
195
+ const { contentJson, contentText } = convertMarkdownToNoteContent(contentMd);
196
+ const tags = await validateTagsForCreate(this.db, normalizeStringArray(args.tags));
197
+ const zettelBoxIds = await resolveZettelBoxIdsForCreate(this.db, normalizeStringArray(args.boxes));
198
+ const id = generateId();
199
+ await createNote(this.db, {
200
+ id,
201
+ title,
202
+ noteType: 'crawl',
203
+ contentMd,
204
+ contentText,
205
+ contentJson,
206
+ tags,
207
+ zettelBoxIds,
208
+ userId: identity.userId,
209
+ });
210
+ const text = JSON.stringify({ id });
211
+ return {
212
+ jsonrpc: '2.0',
213
+ id: req.id,
214
+ result: { content: [{ type: 'text', text }] },
215
+ };
216
+ }
217
+ case 'dinox_graph_backlinks': {
218
+ if (typeof args.id !== 'string' || !args.id.trim()) {
219
+ throw new Error('dinox_graph_backlinks: id is required');
220
+ }
221
+ const result = await getBacklinks(this.db, args.id.trim());
222
+ const text = JSON.stringify(result);
223
+ return {
224
+ jsonrpc: '2.0',
225
+ id: req.id,
226
+ result: { content: [{ type: 'text', text }] },
227
+ };
228
+ }
229
+ case 'dinox_graph_outlinks': {
230
+ if (typeof args.id !== 'string' || !args.id.trim()) {
231
+ throw new Error('dinox_graph_outlinks: id is required');
232
+ }
233
+ const result = await getOutlinks(this.db, args.id.trim());
234
+ const text = JSON.stringify(result);
235
+ return {
236
+ jsonrpc: '2.0',
237
+ id: req.id,
238
+ result: { content: [{ type: 'text', text }] },
239
+ };
240
+ }
241
+ case 'dinox_graph_related': {
242
+ if (typeof args.id !== 'string' || !args.id.trim()) {
243
+ throw new Error('dinox_graph_related: id is required');
244
+ }
245
+ const depthRaw = typeof args.depth === 'number' && Number.isFinite(args.depth)
246
+ ? Math.trunc(args.depth)
247
+ : 2;
248
+ const depth = Math.min(Math.max(depthRaw, 1), 5);
249
+ const result = await getRelatedNotes(this.db, args.id.trim(), depth);
250
+ const text = JSON.stringify(result);
251
+ return {
252
+ jsonrpc: '2.0',
253
+ id: req.id,
254
+ result: { content: [{ type: 'text', text }] },
255
+ };
256
+ }
257
+ case 'dinox_search_todos': {
258
+ const query = typeof args.query === 'string' ? args.query : '';
259
+ const status = typeof args.status === 'string' && ['all', 'completed', 'uncompleted'].includes(args.status)
260
+ ? args.status
261
+ : 'all';
262
+ const limit = normalizeLimit(args.limit);
263
+ const result = await searchTodoTasks(this.db, query, {
264
+ includeDeleted: false,
265
+ taskStatus: status || 'all',
266
+ taskTagFilter: normalizeStringArray(args.tags),
267
+ limit,
268
+ scanLimit: 200,
269
+ });
270
+ const text = JSON.stringify(result);
271
+ return {
272
+ jsonrpc: '2.0',
273
+ id: req.id,
274
+ result: { content: [{ type: 'text', text }] },
275
+ };
276
+ }
277
+ default:
278
+ throw new Error(`Unknown tool: ${name}`);
279
+ }
280
+ }
281
+ async close() {
282
+ if (this.db) {
283
+ await this.db.close().catch(() => undefined);
284
+ }
285
+ }
286
+ }
287
+ export async function startMCPServer(port = 3000, host = '127.0.0.1', authToken) {
288
+ const mcpServer = new MCPServer();
289
+ await mcpServer.initialize();
290
+ const server = createServer(async (req, res) => {
291
+ if (req.method === 'POST') {
292
+ if (!isAuthorizedMCPRequest(req.headers, authToken)) {
293
+ res.writeHead(401, {
294
+ 'Content-Type': 'application/json',
295
+ 'WWW-Authenticate': 'Bearer realm="dinox-mcp"',
296
+ });
297
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
298
+ return;
299
+ }
300
+ try {
301
+ const mcpReq = await readJsonBody(req);
302
+ const mcpRes = await mcpServer.handleRequest(mcpReq);
303
+ res.writeHead(200, { 'Content-Type': 'application/json' });
304
+ res.end(JSON.stringify(mcpRes));
305
+ }
306
+ catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ const statusCode = message.includes('too large') ? 413 : 400;
309
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
310
+ res.end(JSON.stringify({ error: message }));
311
+ }
312
+ }
313
+ else {
314
+ res.writeHead(405);
315
+ res.end();
316
+ }
317
+ });
318
+ server.headersTimeout = 5_000;
319
+ server.requestTimeout = 10_000;
320
+ server.listen(port, host, () => {
321
+ console.log(`MCP server listening on http://${host}:${port}`);
322
+ });
323
+ return { server, mcpServer };
324
+ }
@@ -0,0 +1,2 @@
1
+ import type { MCPTool } from './types.js';
2
+ export declare const mcpTools: MCPTool[];
@@ -0,0 +1,90 @@
1
+ export const mcpTools = [
2
+ {
3
+ name: 'dinox_search_notes',
4
+ description: 'Search notes by keyword, tags, date range, or SQL expression',
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {
8
+ query: { type: 'string', description: 'Search keyword' },
9
+ tags: { type: 'string', description: 'Tag expression (AND/OR/NOT)' },
10
+ from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
11
+ to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
12
+ days: { type: 'number', description: 'Recent N days' },
13
+ box: { type: 'string', description: 'Zettel box name' },
14
+ limit: { type: 'number', description: 'Max results (default: 50)' },
15
+ },
16
+ },
17
+ },
18
+ {
19
+ name: 'dinox_get_note',
20
+ description: 'Get note details by ID',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ id: { type: 'string', description: 'Note ID' },
25
+ },
26
+ required: ['id'],
27
+ },
28
+ },
29
+ {
30
+ name: 'dinox_create_note',
31
+ description: 'Create a new note',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ title: { type: 'string', description: 'Note title' },
36
+ content: { type: 'string', description: 'Markdown content' },
37
+ tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
38
+ boxes: { type: 'array', items: { type: 'string' }, description: 'Zettel boxes' },
39
+ },
40
+ required: ['title', 'content'],
41
+ },
42
+ },
43
+ {
44
+ name: 'dinox_graph_backlinks',
45
+ description: 'Get notes that link to this note',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ id: { type: 'string', description: 'Note ID' },
50
+ },
51
+ required: ['id'],
52
+ },
53
+ },
54
+ {
55
+ name: 'dinox_graph_outlinks',
56
+ description: 'Get notes that this note links to',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ id: { type: 'string', description: 'Note ID' },
61
+ },
62
+ required: ['id'],
63
+ },
64
+ },
65
+ {
66
+ name: 'dinox_graph_related',
67
+ description: 'Get related notes within N degrees',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ id: { type: 'string', description: 'Note ID' },
72
+ depth: { type: 'number', description: 'Degrees (1-5, default: 2)' },
73
+ },
74
+ required: ['id'],
75
+ },
76
+ },
77
+ {
78
+ name: 'dinox_search_todos',
79
+ description: 'Search todo tasks',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: {
83
+ query: { type: 'string', description: 'Task text query' },
84
+ status: { type: 'string', enum: ['all', 'completed', 'uncompleted'], description: 'Task status' },
85
+ tags: { type: 'array', items: { type: 'string' }, description: 'Task tags' },
86
+ limit: { type: 'number', description: 'Max results (default: 50)' },
87
+ },
88
+ },
89
+ },
90
+ ];
@@ -0,0 +1,24 @@
1
+ export type MCPTool = {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ properties: Record<string, any>;
7
+ required?: string[];
8
+ };
9
+ };
10
+ export type MCPResource = {
11
+ uri: string;
12
+ name: string;
13
+ description?: string;
14
+ mimeType?: string;
15
+ };
16
+ export type MCPServerInfo = {
17
+ name: string;
18
+ version: string;
19
+ protocolVersion: string;
20
+ capabilities: {
21
+ tools?: boolean;
22
+ resources?: boolean;
23
+ };
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,2 +1,41 @@
1
1
  import { Command } from 'commander';
2
+ import { type SearchedNote } from './repo.js';
3
+ type NoteSearchResponse = {
4
+ meta: {
5
+ query: string;
6
+ returned_count: number;
7
+ total_count: number;
8
+ stale: boolean;
9
+ applied_filters: {
10
+ include_deleted: boolean;
11
+ tags_expression: string | null;
12
+ created_at_range: {
13
+ from: string | null;
14
+ to: string | null;
15
+ } | null;
16
+ days: number | null;
17
+ box_names: string[];
18
+ box_ids: string[];
19
+ match_empty_boxes: boolean;
20
+ sql_expression: string | null;
21
+ };
22
+ };
23
+ notes: SearchedNote[];
24
+ };
25
+ type BuildNoteSearchResponseInput = {
26
+ query: string;
27
+ notes: SearchedNote[];
28
+ stale: boolean;
29
+ includeDeleted: boolean;
30
+ tagsExpression?: string;
31
+ createdAtFrom?: string;
32
+ createdAtTo?: string;
33
+ days?: number;
34
+ boxNames: string[];
35
+ boxIds: string[];
36
+ matchEmptyBoxes: boolean;
37
+ sqlExpression?: string;
38
+ };
39
+ export declare function buildNoteSearchResponse(input: BuildNoteSearchResponseInput): NoteSearchResponse;
2
40
  export declare function registerNoteCommands(program: Command): void;
41
+ export {};
@@ -5,21 +5,12 @@ import { readStringOrFile } from '../../utils/argValue.js';
5
5
  import { generateId } from '../../utils/id.js';
6
6
  import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
7
7
  import { printYaml } from '../../utils/output.js';
8
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
8
9
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
9
10
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
10
11
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
11
12
  import { createNote, getNote, getNoteDetails, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
12
13
  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
14
  function parseNoteType(value) {
24
15
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
25
16
  if (normalized === 'note' || normalized === 'crawl') {
@@ -105,6 +96,50 @@ async function syncBeforeNoteCommand(db, offline, timeoutMs) {
105
96
  await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
106
97
  await syncNoteTokenIndex(db);
107
98
  }
99
+ function normalizeDaysForMeta(value) {
100
+ if (typeof value !== 'string') {
101
+ return undefined;
102
+ }
103
+ const trimmed = value.trim();
104
+ if (!/^\d+$/.test(trimmed)) {
105
+ return undefined;
106
+ }
107
+ const parsed = Number(trimmed);
108
+ if (!Number.isFinite(parsed) || parsed <= 0) {
109
+ return undefined;
110
+ }
111
+ return Math.trunc(parsed);
112
+ }
113
+ export function buildNoteSearchResponse(input) {
114
+ const query = input.query.trim();
115
+ const tagsExpression = input.tagsExpression?.trim() ?? '';
116
+ const sqlExpression = input.sqlExpression?.trim() ?? '';
117
+ const hasCreatedAtRange = Boolean(input.createdAtFrom || input.createdAtTo);
118
+ return {
119
+ meta: {
120
+ query,
121
+ returned_count: input.notes.length,
122
+ total_count: input.notes.length,
123
+ stale: input.stale,
124
+ applied_filters: {
125
+ include_deleted: input.includeDeleted,
126
+ tags_expression: tagsExpression || null,
127
+ created_at_range: hasCreatedAtRange
128
+ ? {
129
+ from: input.createdAtFrom ?? null,
130
+ to: input.createdAtTo ?? null,
131
+ }
132
+ : null,
133
+ days: input.days ?? null,
134
+ box_names: [...input.boxNames],
135
+ box_ids: [...input.boxIds],
136
+ match_empty_boxes: input.matchEmptyBoxes,
137
+ sql_expression: sqlExpression || null,
138
+ },
139
+ },
140
+ notes: input.notes,
141
+ };
142
+ }
108
143
  export function registerNoteCommands(program) {
109
144
  const note = program.command('note').description('Manage notes');
110
145
  note
@@ -123,7 +158,7 @@ export function registerNoteCommands(program) {
123
158
  const offline = Boolean(globals.offline);
124
159
  const config = resolveConfig(await loadConfig());
125
160
  const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
126
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
161
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
127
162
  try {
128
163
  await syncBeforeNoteCommand(db, offline, timeoutMs);
129
164
  const keyword = query?.trim() ?? '';
@@ -148,7 +183,21 @@ export function registerNoteCommands(program) {
148
183
  matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
149
184
  sqlExpression: sqlExpression || undefined,
150
185
  });
151
- printYaml(rows);
186
+ const response = buildNoteSearchResponse({
187
+ query: keyword,
188
+ notes: rows,
189
+ stale: offline ? true : stale,
190
+ includeDeleted: Boolean(options.includeDeleted),
191
+ tagsExpression: tagsExpression || undefined,
192
+ createdAtFrom,
193
+ createdAtTo,
194
+ days: normalizeDaysForMeta(options.days),
195
+ boxNames: parsedBoxInput.values,
196
+ boxIds: zettelBoxIds,
197
+ matchEmptyBoxes: parsedBoxInput.isExplicitEmptyArray,
198
+ sqlExpression: sqlExpression || undefined,
199
+ });
200
+ printYaml(response);
152
201
  }
153
202
  finally {
154
203
  await db.close().catch(() => undefined);
@@ -2,20 +2,11 @@ import { loadConfig } from '../../config/store.js';
2
2
  import { resolveConfig } from '../../config/resolve.js';
3
3
  import { DinoxError } from '../../utils/errors.js';
4
4
  import { printYaml } from '../../utils/output.js';
5
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
5
6
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
6
7
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
7
8
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
8
9
  import { addPrompt, listPrompts } 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
10
  async function syncBeforePromptCommand(db, offline, timeoutMs) {
20
11
  if (offline) {
21
12
  return;
@@ -1,19 +1,9 @@
1
1
  import { loadConfig } from '../config/store.js';
2
2
  import { resolveConfig } from '../config/resolve.js';
3
- import { DinoxError } from '../utils/errors.js';
4
3
  import { printYaml } from '../utils/output.js';
4
+ import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
5
5
  import { connectPowerSync, getStatusSnapshot } from '../powersync/runtime.js';
6
6
  import { runSingleSyncPass } from '../powersync/syncPass.js';
7
- function parseSyncTimeoutMs(value) {
8
- if (typeof value !== 'string') {
9
- return undefined;
10
- }
11
- const parsed = Number(value);
12
- if (!Number.isFinite(parsed) || parsed <= 0) {
13
- throw new DinoxError('--sync-timeout must be a positive integer');
14
- }
15
- return Math.trunc(parsed);
16
- }
17
7
  export function registerSyncCommand(program) {
18
8
  program
19
9
  .command('sync')
@@ -2,20 +2,11 @@ import { loadConfig } from '../../config/store.js';
2
2
  import { resolveConfig } from '../../config/resolve.js';
3
3
  import { DinoxError } from '../../utils/errors.js';
4
4
  import { printYaml } from '../../utils/output.js';
5
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
5
6
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
6
7
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
7
8
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
8
9
  import { addTag, listTags } 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
10
  async function syncBeforeTagCommand(db, offline, timeoutMs) {
20
11
  if (offline) {
21
12
  return;