@dinoxx/dinox-cli 2.13.1 → 2.15.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/dist/ai/ragxClient.d.ts +15 -0
- package/dist/ai/ragxClient.js +71 -0
- package/dist/cli.js +31 -4
- package/dist/commands/auth/index.d.ts +2 -1
- package/dist/commands/auth/index.js +18 -1
- 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/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +20 -1
- package/dist/commands/graph/index.js +2 -1
- 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/contentPatch.js +2 -1
- 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 +7 -2
- 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/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/savedViews/index.js +12 -3
- package/dist/commands/schema/globalOptions.d.ts +7 -0
- package/dist/commands/schema/globalOptions.js +7 -0
- package/dist/commands/schema/renderSkills.js +6 -2
- package/dist/commands/schema/spec.js +20 -1
- package/dist/commands/storage/index.js +7 -5
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +27 -8
- 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 +53 -3
- package/dist/daemon/connectivity.d.ts +20 -0
- package/dist/daemon/connectivity.js +42 -0
- package/dist/daemon/ensure.js +48 -12
- package/dist/daemon/forward.d.ts +37 -0
- package/dist/daemon/forward.js +155 -0
- package/dist/daemon/fresh.d.ts +4 -0
- package/dist/daemon/fresh.js +10 -0
- package/dist/daemon/index.d.ts +11 -6
- package/dist/daemon/index.js +90 -25
- package/dist/daemon/launch.d.ts +13 -1
- package/dist/daemon/launch.js +31 -2
- package/dist/daemon/lifecycle.d.ts +3 -1
- package/dist/daemon/lifecycle.js +27 -3
- package/dist/daemon/pid.d.ts +8 -0
- package/dist/daemon/pid.js +20 -0
- package/dist/daemon/route.js +2 -1
- package/dist/daemon/server.d.ts +45 -1
- package/dist/daemon/server.js +192 -4
- package/dist/daemon/supervise.d.ts +6 -6
- package/dist/daemon/supervise.js +12 -10
- package/dist/daemon/supervisor.d.ts +21 -5
- package/dist/daemon/supervisor.js +125 -6
- package/dist/daemon/types.d.ts +22 -0
- package/dist/dinox.js +43 -64
- package/dist/gateway/deviceHost.js +2 -1
- package/dist/gateway/mcpCommandTools.d.ts +24 -5
- package/dist/gateway/mcpCommandTools.js +70 -29
- package/dist/gateway/mcpEnv.d.ts +2 -0
- package/dist/gateway/mcpEnv.js +2 -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 +25 -5
- package/dist/powersync/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- 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/requests/store.d.ts +10 -0
- package/dist/requests/store.js +190 -0
- package/dist/runCli.d.ts +2 -0
- package/dist/runCli.js +64 -0
- package/dist/skills/commandRefs.js +2 -10
- package/dist/utils/argValue.js +2 -1
- 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 +24 -0
- package/dist/utils/errorCodes.js +6 -0
- package/dist/utils/output.d.ts +4 -0
- package/dist/utils/output.js +72 -10
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +59 -0
- package/dist/utils/requestContext.d.ts +32 -0
- package/dist/utils/requestContext.js +13 -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 +2 -1
|
@@ -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,9 +3,11 @@ 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';
|
|
10
|
+
import { resolveUserPath } from '../../utils/requestContext.js';
|
|
9
11
|
export function parseExportFormat(value) {
|
|
10
12
|
if (value === undefined || value === null || value === '') {
|
|
11
13
|
return 'markdown';
|
|
@@ -107,7 +109,7 @@ async function pathExists(filePath) {
|
|
|
107
109
|
}
|
|
108
110
|
async function resolveOutputTargets(args) {
|
|
109
111
|
const outputLooksLikeDirectory = /[\\/]$/.test(args.outputPath);
|
|
110
|
-
const outputPath =
|
|
112
|
+
const outputPath = resolveUserPath(args.outputPath);
|
|
111
113
|
if (args.notes.length === 1) {
|
|
112
114
|
if (outputLooksLikeDirectory) {
|
|
113
115
|
return [{
|
|
@@ -160,12 +162,13 @@ export async function writeExportFiles(args) {
|
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
if (args.notes.length > 1) {
|
|
163
|
-
await fs.mkdir(
|
|
165
|
+
await fs.mkdir(resolveUserPath(args.outputPath), { recursive: true, mode: 0o700 });
|
|
164
166
|
}
|
|
165
167
|
else {
|
|
166
168
|
await fs.mkdir(path.dirname(targets[0].path), { recursive: true, mode: 0o700 });
|
|
167
169
|
}
|
|
168
170
|
const noteById = new Map(args.notes.map((note) => [note.id, note]));
|
|
171
|
+
let written = 0;
|
|
169
172
|
for (const target of targets) {
|
|
170
173
|
const note = noteById.get(target.id);
|
|
171
174
|
if (!note) {
|
|
@@ -182,6 +185,7 @@ export async function writeExportFiles(args) {
|
|
|
182
185
|
if (process.platform !== 'win32') {
|
|
183
186
|
await fs.chmod(target.path, 0o600);
|
|
184
187
|
}
|
|
188
|
+
reportProgress({ phase: 'write', current: ++written, total: targets.length });
|
|
185
189
|
}
|
|
186
190
|
return targets;
|
|
187
191
|
}
|
|
@@ -202,6 +206,7 @@ export async function runNoteExport({ command, options, args }) {
|
|
|
202
206
|
const { db } = runtime;
|
|
203
207
|
await syncBeforeNoteCommand(runtime);
|
|
204
208
|
const ids = await parseTargetNoteIds(args.id, exportOptions.ids, 'detail');
|
|
209
|
+
reportProgress({ phase: 'read', total: ids.length });
|
|
205
210
|
const notes = await getNoteExportItems(db, ids);
|
|
206
211
|
assertNoMissingNotes(ids, notes);
|
|
207
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 {};
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { postRagx } from '../../ai/ragxClient.js';
|
|
2
|
+
import { AI_SERVICE_URL } from '../../config/serviceEndpoints.js';
|
|
3
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
+
import { clearPendingStructuredNoticeProvider, getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, setPendingStructuredNoticeProvider, } from '../../utils/output.js';
|
|
5
|
+
import { commandStale, printStaleCacheWarning, syncBeforeCommand } from '../runtime.js';
|
|
6
|
+
import { resolveZettelBoxIdsForSearch } from './repo/resolve.js';
|
|
7
|
+
import { lookupLocalNotes } from './repo/semantic.js';
|
|
8
|
+
import { parseSearchListInput, withNoteDatabase } from './runtime.js';
|
|
9
|
+
import { parseCreatedAtRange } from './searchTime.js';
|
|
10
|
+
const SEARCH_TIMEOUT_MS = 20_000;
|
|
11
|
+
const CONTEXT_TIMEOUT_MS = 45_000;
|
|
12
|
+
export const SEMANTIC_MODES = { hybrid: 'hybrid', semantic: 'semantic', keyword: 'fts' };
|
|
13
|
+
export const SEMANTIC_LIMITS = {
|
|
14
|
+
search: { default: 10, max: 60 },
|
|
15
|
+
similar: { default: 6, max: 20 },
|
|
16
|
+
contextTopK: { default: 12, max: 60 },
|
|
17
|
+
contextGroups: { default: 6, max: 20 },
|
|
18
|
+
contextTokens: { default: 2_400, max: 16_000 },
|
|
19
|
+
};
|
|
20
|
+
function boundedInteger(value, flag, limits) {
|
|
21
|
+
if (value === undefined || value === null || value === '')
|
|
22
|
+
return limits.default;
|
|
23
|
+
const parsed = typeof value === 'number' ? value : Number(String(value).trim());
|
|
24
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > limits.max) {
|
|
25
|
+
throw new DinoxError(`${flag} must be an integer from 1 to ${limits.max}`, { code: 'INVALID_ARGUMENT', recoverable: true });
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
export function parseSemanticMode(value) {
|
|
30
|
+
const mode = typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'hybrid';
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(SEMANTIC_MODES, mode)) {
|
|
32
|
+
throw new DinoxError(`--mode must be one of: ${Object.keys(SEMANTIC_MODES).join(', ')}`, {
|
|
33
|
+
code: 'INVALID_ARGUMENT',
|
|
34
|
+
recoverable: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return mode;
|
|
38
|
+
}
|
|
39
|
+
function assertOnline(globals, command) {
|
|
40
|
+
if (globals.offline) {
|
|
41
|
+
throw new DinoxError(`${command} queries the Dinox semantic index online and cannot run with --offline`, {
|
|
42
|
+
code: 'USAGE_ERROR',
|
|
43
|
+
recoverable: true,
|
|
44
|
+
suggestion: 'Drop --offline, or use `dino note search <query> --offline --format json` for local keyword search.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Resolves tag, box and date options into the backend's rag-filter-v1 fields. */
|
|
49
|
+
async function resolveFilters(runtime, options) {
|
|
50
|
+
const tags = typeof options.tags === 'string' ? (await parseSearchListInput(options.tags, '--tags')).values : [];
|
|
51
|
+
const boxNames = typeof options.boxes === 'string' ? (await parseSearchListInput(options.boxes, '--boxes')).values : [];
|
|
52
|
+
const { createdAtFrom, createdAtTo } = parseCreatedAtRange({ from: options.from, to: options.to, days: options.days });
|
|
53
|
+
const cardboxIds = boxNames.length > 0 ? await resolveZettelBoxIdsForSearch(runtime.db, boxNames) : [];
|
|
54
|
+
return {
|
|
55
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
56
|
+
...(cardboxIds.length > 0 ? { cardboxIds } : {}),
|
|
57
|
+
// Local ranges end at an inclusive instant (end of day or now).
|
|
58
|
+
...(createdAtFrom ? { startDateTime: createdAtFrom } : {}),
|
|
59
|
+
...(createdAtTo ? { endDateTime: createdAtTo, endExclusive: false } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function noteIdOf(group) {
|
|
63
|
+
return group?.kind === 'note' && typeof group.id === 'string' && group.id ? group.id : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Folds ranked chunk hits into one entry per note (keeping the best chunk),
|
|
67
|
+
* fills in local fields, and drops notes the local cache knows are deleted.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeHitsIntoNotes(hits, lookup, limit) {
|
|
70
|
+
const byId = new Map();
|
|
71
|
+
const dropped = new Set();
|
|
72
|
+
for (const hit of hits) {
|
|
73
|
+
const id = noteIdOf(hit.group);
|
|
74
|
+
if (!id)
|
|
75
|
+
continue;
|
|
76
|
+
if (lookup.deleted.has(id)) {
|
|
77
|
+
dropped.add(id);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const existing = byId.get(id);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.matches += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const local = lookup.live.get(id);
|
|
86
|
+
byId.set(id, {
|
|
87
|
+
id,
|
|
88
|
+
title: local?.title ?? hit.docTitle ?? '',
|
|
89
|
+
tags: local?.tags ?? [],
|
|
90
|
+
boxes: local?.zettel_boxes ?? [],
|
|
91
|
+
created_at: local?.created_at ?? null,
|
|
92
|
+
is_starred: local?.is_starred ?? false,
|
|
93
|
+
score: typeof hit.scores?.final === 'number' ? hit.scores.final : null,
|
|
94
|
+
section: hit.sectionTitle ?? null,
|
|
95
|
+
heading_path: Array.isArray(hit.headingPath) ? hit.headingPath : [],
|
|
96
|
+
snippet: hit.snippet ?? '',
|
|
97
|
+
matched_source: hit.anchor?.sourceType === 'resource' ? 'resource' : 'note',
|
|
98
|
+
matches: 1,
|
|
99
|
+
in_local_cache: Boolean(local),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return { notes: [...byId.values()].slice(0, limit), droppedDeleted: dropped.size };
|
|
103
|
+
}
|
|
104
|
+
/** Context blocks with the note id surfaced and blocks of locally deleted notes removed. */
|
|
105
|
+
export function shapeContextBlocks(blocks, lookup) {
|
|
106
|
+
let droppedDeleted = 0;
|
|
107
|
+
const shaped = [];
|
|
108
|
+
for (const block of blocks) {
|
|
109
|
+
const noteId = noteIdOf(block.group);
|
|
110
|
+
if (noteId && lookup.deleted.has(noteId)) {
|
|
111
|
+
droppedDeleted += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
shaped.push({
|
|
115
|
+
ref: block.ref ?? null,
|
|
116
|
+
note_id: noteId,
|
|
117
|
+
title: (noteId ? lookup.live.get(noteId)?.title : undefined) ?? block.title ?? '',
|
|
118
|
+
role: block.retrievalRole ?? null,
|
|
119
|
+
score: typeof block.score === 'number' ? block.score : null,
|
|
120
|
+
token_count: typeof block.tokenCount === 'number' ? block.tokenCount : null,
|
|
121
|
+
content: block.content ?? '',
|
|
122
|
+
citations: (block.citations ?? [])
|
|
123
|
+
.map((citation) => citation.anchor)
|
|
124
|
+
.filter((anchor) => Boolean(anchor?.sourceType && anchor.sourceId))
|
|
125
|
+
.map((anchor) => ({ source_type: anchor.sourceType, source_id: anchor.sourceId })),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return { blocks: shaped, droppedDeleted };
|
|
129
|
+
}
|
|
130
|
+
/** Pending uploads have not reached the server index yet; say so instead of implying absence. */
|
|
131
|
+
async function indexLagNotice(runtime) {
|
|
132
|
+
let pending = 0;
|
|
133
|
+
try {
|
|
134
|
+
pending = (await runtime.db.getUploadQueueStats(false)).count;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (pending === 0)
|
|
140
|
+
return null;
|
|
141
|
+
return {
|
|
142
|
+
index_may_lag: {
|
|
143
|
+
message: 'Local changes are still uploading, so the semantic index may miss recent notes or edits. Absence from these results is not proof a note does not exist; confirm with `dino note search`.',
|
|
144
|
+
upload_queue_remaining: pending,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Shared flow: refuse --offline, then query the service while the local cache
|
|
150
|
+
* finishes catching up (needed to enrich and filter hits), and attach the
|
|
151
|
+
* index-lag notice to whatever is printed.
|
|
152
|
+
*/
|
|
153
|
+
async function withSemanticRuntime(context, label, remote, print) {
|
|
154
|
+
assertOnline(context.globals, label);
|
|
155
|
+
await withNoteDatabase(context.command, async (runtime) => {
|
|
156
|
+
const [result] = await Promise.all([
|
|
157
|
+
remote({ runtime, baseUrl: AI_SERVICE_URL, authorization: runtime.config.auth.authorization }),
|
|
158
|
+
syncBeforeCommand(runtime),
|
|
159
|
+
]);
|
|
160
|
+
const notice = await indexLagNotice(runtime);
|
|
161
|
+
setPendingStructuredNoticeProvider(() => notice);
|
|
162
|
+
try {
|
|
163
|
+
await print(result, runtime, commandStale(runtime));
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
clearPendingStructuredNoticeProvider();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function printNoteLines(notes, runtime) {
|
|
171
|
+
for (const note of notes) {
|
|
172
|
+
printHumanReadableLine(`${note.id}\t${note.score === null ? '-' : note.score.toFixed(3)}\t${note.title}`);
|
|
173
|
+
}
|
|
174
|
+
printStaleCacheWarning(runtime);
|
|
175
|
+
}
|
|
176
|
+
export async function runNoteSemantic(context) {
|
|
177
|
+
const query = context.args.query?.trim() ?? '';
|
|
178
|
+
if (!query)
|
|
179
|
+
throw new DinoxError('A search query is required', { code: 'INVALID_ARGUMENT', recoverable: true });
|
|
180
|
+
const mode = parseSemanticMode(context.options.mode);
|
|
181
|
+
const limit = boundedInteger(context.options.limit, '--limit', SEMANTIC_LIMITS.search);
|
|
182
|
+
const includeResources = Boolean(context.options.includeResources);
|
|
183
|
+
await withSemanticRuntime(context, 'note semantic', async ({ runtime, baseUrl, authorization }) => {
|
|
184
|
+
const filters = await resolveFilters(runtime, context.options);
|
|
185
|
+
const data = await postRagx({
|
|
186
|
+
baseUrl,
|
|
187
|
+
authorization,
|
|
188
|
+
path: '/api/ragx/search',
|
|
189
|
+
timeoutMs: SEARCH_TIMEOUT_MS,
|
|
190
|
+
body: {
|
|
191
|
+
query,
|
|
192
|
+
mode: SEMANTIC_MODES[mode],
|
|
193
|
+
filters: { sourceTypes: includeResources ? ['note', 'resource'] : ['note'], ...filters },
|
|
194
|
+
// Ask for extra chunks: several can fold into one note, and deleted notes are dropped.
|
|
195
|
+
options: { topK: Math.min(SEMANTIC_LIMITS.search.max, limit * 2), dedupBySource: true, returnDiagnostics: false },
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const hits = data?.hits ?? [];
|
|
199
|
+
const lookup = await lookupLocalNotes(runtime.db, hits.map((hit) => noteIdOf(hit.group) ?? ''));
|
|
200
|
+
return { filters, ...mergeHitsIntoNotes(hits, lookup, limit) };
|
|
201
|
+
}, async ({ filters, notes, droppedDeleted }, runtime, stale) => {
|
|
202
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
203
|
+
printSuccess({
|
|
204
|
+
stale,
|
|
205
|
+
meta: {
|
|
206
|
+
query,
|
|
207
|
+
mode,
|
|
208
|
+
returned_count: notes.length,
|
|
209
|
+
limit,
|
|
210
|
+
include_resources: includeResources,
|
|
211
|
+
dropped_deleted_count: droppedDeleted,
|
|
212
|
+
applied_filters: {
|
|
213
|
+
tags: filters.tags ?? [],
|
|
214
|
+
box_ids: filters.cardboxIds ?? [],
|
|
215
|
+
created_at_range: filters.startDateTime || filters.endDateTime
|
|
216
|
+
? { from: filters.startDateTime ?? null, to: filters.endDateTime ?? null }
|
|
217
|
+
: null,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
notes,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
printNoteLines(notes, runtime);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export async function runNoteSimilar(context) {
|
|
228
|
+
const noteId = context.args.id?.trim() ?? '';
|
|
229
|
+
if (!noteId)
|
|
230
|
+
throw new DinoxError('Note id is required', { code: 'INVALID_ARGUMENT', recoverable: true });
|
|
231
|
+
const limit = boundedInteger(context.options.limit, '--limit', SEMANTIC_LIMITS.similar);
|
|
232
|
+
await withSemanticRuntime(context, 'note similar', async ({ runtime, baseUrl, authorization }) => {
|
|
233
|
+
const anchor = await lookupLocalNotes(runtime.db, [noteId]);
|
|
234
|
+
if (anchor.deleted.has(noteId)) {
|
|
235
|
+
throw new DinoxError(`Note not found: ${noteId}`, {
|
|
236
|
+
code: 'NOTE_NOT_FOUND',
|
|
237
|
+
recoverable: true,
|
|
238
|
+
suggestion: "Run 'dino note search <keyword>' to locate the note id.",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
let data;
|
|
242
|
+
try {
|
|
243
|
+
data = await postRagx({
|
|
244
|
+
baseUrl,
|
|
245
|
+
authorization,
|
|
246
|
+
path: '/api/ragx/similar',
|
|
247
|
+
timeoutMs: SEARCH_TIMEOUT_MS,
|
|
248
|
+
body: {
|
|
249
|
+
anchor: { sourceType: 'note', sourceId: noteId },
|
|
250
|
+
targetSourceTypes: ['note'],
|
|
251
|
+
topK: limit,
|
|
252
|
+
excludeSameSource: true,
|
|
253
|
+
options: { returnDiagnostics: false },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error instanceof DinoxError && error.code === 'NOT_FOUND') {
|
|
259
|
+
throw new DinoxError(`Note is not in the semantic index yet: ${noteId}`, {
|
|
260
|
+
code: 'NOTE_NOT_FOUND',
|
|
261
|
+
recoverable: true,
|
|
262
|
+
suggestion: 'New or just-edited notes are indexed shortly after they upload; retry later, or use `dino graph related <id> --format json` for link-based neighbours.',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
const items = (data?.items ?? []).filter((item) => noteIdOf(item.group) !== noteId);
|
|
268
|
+
const lookup = await lookupLocalNotes(runtime.db, items.map((item) => noteIdOf(item.group) ?? ''));
|
|
269
|
+
return mergeHitsIntoNotes(items, lookup, limit);
|
|
270
|
+
}, async ({ notes, droppedDeleted }, runtime, stale) => {
|
|
271
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
272
|
+
printSuccess({
|
|
273
|
+
stale,
|
|
274
|
+
meta: { note_id: noteId, returned_count: notes.length, limit, dropped_deleted_count: droppedDeleted },
|
|
275
|
+
notes,
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
printNoteLines(notes, runtime);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
export async function runNoteContext(context) {
|
|
283
|
+
const query = context.args.query?.trim() ?? '';
|
|
284
|
+
if (!query)
|
|
285
|
+
throw new DinoxError('A question or query is required', { code: 'INVALID_ARGUMENT', recoverable: true });
|
|
286
|
+
const mode = parseSemanticMode(context.options.mode);
|
|
287
|
+
const topK = boundedInteger(context.options.limit, '--limit', SEMANTIC_LIMITS.contextTopK);
|
|
288
|
+
const maxGroups = boundedInteger(context.options.maxNotes, '--max-notes', SEMANTIC_LIMITS.contextGroups);
|
|
289
|
+
const maxTokens = boundedInteger(context.options.maxTokens, '--max-tokens', SEMANTIC_LIMITS.contextTokens);
|
|
290
|
+
const includeResources = !context.options.notesOnly;
|
|
291
|
+
await withSemanticRuntime(context, 'note context', async ({ runtime, baseUrl, authorization }) => {
|
|
292
|
+
const filters = await resolveFilters(runtime, context.options);
|
|
293
|
+
const data = await postRagx({
|
|
294
|
+
baseUrl,
|
|
295
|
+
authorization,
|
|
296
|
+
path: '/api/ragx/context',
|
|
297
|
+
timeoutMs: CONTEXT_TIMEOUT_MS,
|
|
298
|
+
body: {
|
|
299
|
+
query,
|
|
300
|
+
intent: 'answer',
|
|
301
|
+
filters: { sourceTypes: includeResources ? ['note', 'resource'] : ['note'], ...filters },
|
|
302
|
+
plan: { searchMode: SEMANTIC_MODES[mode] },
|
|
303
|
+
options: { topK, maxGroups, maxContextTokens: maxTokens, includeDiagnostics: false },
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
const blocks = data?.contextBlocks ?? [];
|
|
307
|
+
const lookup = await lookupLocalNotes(runtime.db, blocks.map((block) => noteIdOf(block.group) ?? ''));
|
|
308
|
+
return { data, ...shapeContextBlocks(blocks, lookup) };
|
|
309
|
+
}, async ({ data, blocks, droppedDeleted }, runtime, stale) => {
|
|
310
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
311
|
+
printSuccess({
|
|
312
|
+
stale,
|
|
313
|
+
meta: {
|
|
314
|
+
query,
|
|
315
|
+
mode,
|
|
316
|
+
returned_count: blocks.length,
|
|
317
|
+
context_tokens: typeof data?.contextTokens === 'number' ? data.contextTokens : null,
|
|
318
|
+
max_tokens: maxTokens,
|
|
319
|
+
dropped_deleted_count: droppedDeleted,
|
|
320
|
+
},
|
|
321
|
+
blocks,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
for (const block of blocks) {
|
|
326
|
+
printHumanReadableLine(`## ${block.title}${block.note_id ? ` (${block.note_id})` : ''}`);
|
|
327
|
+
printHumanReadableLine(block.content);
|
|
328
|
+
printHumanReadableLine('');
|
|
329
|
+
}
|
|
330
|
+
printStaleCacheWarning(runtime);
|
|
331
|
+
});
|
|
332
|
+
}
|