@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.
- package/README.md +2 -0
- package/dist/ai/ragxClient.d.ts +15 -0
- package/dist/ai/ragxClient.js +71 -0
- package/dist/cli.js +27 -3
- package/dist/commands/batch.d.ts +35 -0
- package/dist/commands/batch.js +202 -0
- package/dist/commands/boxes/index.js +21 -21
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +47 -0
- package/dist/commands/notes/actions.js +18 -2
- package/dist/commands/notes/descriptors.js +111 -5
- package/dist/commands/notes/detailContent.d.ts +19 -0
- package/dist/commands/notes/detailContent.js +62 -0
- package/dist/commands/notes/export.js +4 -0
- package/dist/commands/notes/repo/search.d.ts +2 -1
- package/dist/commands/notes/repo/search.js +1 -1
- package/dist/commands/notes/repo/semantic.d.ts +14 -0
- package/dist/commands/notes/repo/semantic.js +31 -0
- package/dist/commands/notes/searchResponse.d.ts +3 -0
- package/dist/commands/notes/searchResponse.js +1 -0
- package/dist/commands/notes/semantic.d.ts +116 -0
- package/dist/commands/notes/semantic.js +332 -0
- package/dist/commands/prompt/index.js +7 -5
- package/dist/commands/savedViews/index.js +12 -3
- package/dist/commands/schema/globalOptions.d.ts +6 -0
- package/dist/commands/schema/globalOptions.js +6 -0
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +18 -7
- package/dist/commands/storage/index.js +7 -5
- package/dist/commands/sync.js +3 -3
- package/dist/commands/tags/index.js +21 -21
- package/dist/commands/todo/index.js +22 -2
- package/dist/commands/todo/repo.d.ts +5 -0
- package/dist/commands/todo/repo.js +7 -2
- package/dist/commands/writeReceipt.d.ts +13 -0
- package/dist/commands/writeReceipt.js +12 -0
- package/dist/config/serviceEndpoints.d.ts +2 -0
- package/dist/config/serviceEndpoints.js +2 -0
- package/dist/daemon/client.d.ts +2 -0
- package/dist/daemon/client.js +22 -2
- package/dist/daemon/forward.d.ts +4 -1
- package/dist/daemon/forward.js +13 -4
- package/dist/daemon/server.d.ts +1 -1
- package/dist/daemon/server.js +17 -1
- package/dist/daemon/types.d.ts +6 -0
- package/dist/dinox.js +12 -10
- package/dist/gateway/adapters/acp.d.ts +5 -1
- package/dist/gateway/adapters/acp.js +21 -9
- package/dist/gateway/adapters/echo.js +7 -4
- package/dist/gateway/adapters/index.d.ts +1 -0
- package/dist/gateway/adapters/index.js +5 -3
- package/dist/gateway/deviceHost.d.ts +3 -1
- package/dist/gateway/deviceHost.js +75 -5
- package/dist/gateway/mcpCommandTools.d.ts +15 -5
- package/dist/gateway/mcpCommandTools.js +30 -19
- package/dist/gateway/mcpEnv.d.ts +4 -0
- package/dist/gateway/mcpEnv.js +4 -0
- package/dist/gateway/mcpGroups.d.ts +3 -0
- package/dist/gateway/mcpGroups.js +4 -0
- package/dist/gateway/mcpServer.d.ts +10 -1
- package/dist/gateway/mcpServer.js +26 -5
- package/dist/gateway/protocol.d.ts +31 -2
- package/dist/gateway/protocol.js +15 -3
- package/dist/gateway/runExecutor.js +23 -12
- package/dist/gateway/service.d.ts +2 -0
- package/dist/gateway/service.js +29 -4
- package/dist/gateway/threadStore.d.ts +2 -0
- package/dist/gateway/threadStore.js +13 -0
- package/dist/gateway/types.d.ts +5 -4
- package/dist/gateway/workspace.d.ts +5 -0
- package/dist/gateway/workspace.js +30 -0
- package/dist/gateway/workspaceFolders.d.ts +5 -3
- package/dist/gateway/workspaceFolders.js +20 -7
- package/dist/gateway/workspaceListing.d.ts +5 -0
- package/dist/gateway/workspaceListing.js +78 -0
- package/dist/powersync/syncPass.js +13 -1
- package/dist/powersync/tokenIndex.d.ts +2 -0
- package/dist/powersync/tokenIndex.js +1 -0
- package/dist/requests/execute.d.ts +11 -0
- package/dist/requests/execute.js +26 -0
- package/dist/skills/commandRefs.js +2 -10
- package/dist/utils/cursor.d.ts +9 -0
- package/dist/utils/cursor.js +61 -0
- package/dist/utils/defaultFormat.d.ts +8 -0
- package/dist/utils/defaultFormat.js +38 -0
- package/dist/utils/errorCodes.d.ts +4 -0
- package/dist/utils/errorCodes.js +1 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +41 -3
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +59 -0
- package/dist/utils/requestContext.d.ts +8 -0
- package/dist/utils/select.d.ts +20 -0
- package/dist/utils/select.js +92 -0
- package/dist/utils/usageError.d.ts +37 -0
- package/dist/utils/usageError.js +127 -0
- 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: [
|
|
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: [
|
|
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 {};
|