@gmickel/gno 0.22.6 → 0.24.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 +71 -1
- package/package.json +14 -2
- package/src/cli/program.ts +30 -0
- package/src/core/structured-query.ts +198 -0
- package/src/mcp/tools/query.ts +17 -3
- package/src/pipeline/query-modes.ts +17 -12
- package/src/sdk/client.ts +584 -0
- package/src/sdk/documents.ts +348 -0
- package/src/sdk/embed.ts +287 -0
- package/src/sdk/errors.ts +42 -0
- package/src/sdk/index.ts +51 -0
- package/src/sdk/types.ts +137 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +30 -2
- package/src/serve/public/pages/Search.tsx +47 -7
- package/src/serve/routes/api.ts +67 -14
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
|
|
|
24
24
|
- [Agent Integration](#agent-integration)
|
|
25
25
|
- [Web UI](#web-ui)
|
|
26
26
|
- [REST API](#rest-api)
|
|
27
|
+
- [SDK](#sdk)
|
|
27
28
|
- [How It Works](#how-it-works)
|
|
28
29
|
- [Features](#features)
|
|
29
30
|
- [Local Models](#local-models)
|
|
@@ -33,7 +34,20 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
|
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
36
|
-
## What's New in v0.
|
|
37
|
+
## What's New in v0.24
|
|
38
|
+
|
|
39
|
+
- **Structured Query Documents**: first-class multi-line query syntax using `term:`, `intent:`, and `hyde:`
|
|
40
|
+
- **Cross-Surface Rollout**: works across CLI, API, MCP, SDK, and Web Search/Ask
|
|
41
|
+
- **Portable Retrieval Prompts**: save/share advanced retrieval intent as one text payload instead of repeated flags or JSON arrays
|
|
42
|
+
|
|
43
|
+
### v0.23
|
|
44
|
+
|
|
45
|
+
- **SDK / Library Mode**: package-root importable SDK with `createGnoClient(...)` for direct retrieval, document access, and indexing flows
|
|
46
|
+
- **Inline Config Support**: embed GNO in another app without writing YAML config files
|
|
47
|
+
- **Programmatic Indexing**: call `update`, `embed`, and `index` directly from Bun/TypeScript
|
|
48
|
+
- **Docs & Website**: dedicated SDK guide, feature page, homepage section, and architecture docs
|
|
49
|
+
|
|
50
|
+
### v0.22
|
|
37
51
|
|
|
38
52
|
- **Promoted Slim Retrieval Model**: published `slim-retrieval-v1` on Hugging Face for direct `hf:` installation in GNO
|
|
39
53
|
- **Fine-Tuning Workflow**: local MLX LoRA training, portable GGUF export, automatic checkpoint selection, promotion bundles, and repeatable benchmark comparisons
|
|
@@ -187,6 +201,58 @@ gno skill install --target all # Both Claude + Codex
|
|
|
187
201
|
|
|
188
202
|
---
|
|
189
203
|
|
|
204
|
+
## SDK
|
|
205
|
+
|
|
206
|
+
Embed GNO directly in another Bun or TypeScript app. No CLI subprocesses. No local server required.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { createDefaultConfig, createGnoClient } from "@gmickel/gno";
|
|
210
|
+
|
|
211
|
+
const config = createDefaultConfig();
|
|
212
|
+
config.collections = [
|
|
213
|
+
{
|
|
214
|
+
name: "notes",
|
|
215
|
+
path: "/Users/me/notes",
|
|
216
|
+
pattern: "**/*",
|
|
217
|
+
include: [],
|
|
218
|
+
exclude: [],
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const client = await createGnoClient({
|
|
223
|
+
config,
|
|
224
|
+
dbPath: "/tmp/gno-sdk.sqlite",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await client.index({ noEmbed: true });
|
|
228
|
+
|
|
229
|
+
const results = await client.query("JWT token flow", {
|
|
230
|
+
noExpand: true,
|
|
231
|
+
noRerank: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
console.log(results.results[0]?.uri);
|
|
235
|
+
await client.close();
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Core SDK surface:
|
|
239
|
+
|
|
240
|
+
- `createGnoClient({ config | configPath, dbPath? })`
|
|
241
|
+
- `search`, `vsearch`, `query`, `ask`
|
|
242
|
+
- `get`, `multiGet`, `list`, `status`
|
|
243
|
+
- `update`, `embed`, `index`
|
|
244
|
+
- `close`
|
|
245
|
+
|
|
246
|
+
Install in an app:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
bun add @gmickel/gno
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Full guide: [SDK docs](https://gno.sh/docs/SDK/)
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
190
256
|
## Search Modes
|
|
191
257
|
|
|
192
258
|
| Command | Mode | Best For |
|
|
@@ -228,11 +294,15 @@ gno query "auth flow" \
|
|
|
228
294
|
--query-mode intent:"how refresh token rotation works" \
|
|
229
295
|
--query-mode hyde:"Refresh tokens rotate on each use and previous tokens are revoked." \
|
|
230
296
|
--explain
|
|
297
|
+
|
|
298
|
+
# Multi-line structured query document
|
|
299
|
+
gno query $'auth flow\nterm: "refresh token" -oauth1\nintent: how refresh token rotation works\nhyde: Refresh tokens rotate on each use and previous tokens are revoked.' --fast
|
|
231
300
|
```
|
|
232
301
|
|
|
233
302
|
- Modes: `term` (BM25-focused), `intent` (semantic-focused), `hyde` (single hypothetical passage)
|
|
234
303
|
- Explain includes stage timings, fallback/cache counters, and per-result score components
|
|
235
304
|
- `gno ask --json` includes `meta.answerContext` for adaptive source selection traces
|
|
305
|
+
- Search and Ask web text boxes also accept multi-line structured query documents with `Shift+Enter`
|
|
236
306
|
|
|
237
307
|
---
|
|
238
308
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"embeddings",
|
|
@@ -33,7 +33,19 @@
|
|
|
33
33
|
"vendor"
|
|
34
34
|
],
|
|
35
35
|
"type": "module",
|
|
36
|
-
"
|
|
36
|
+
"main": "src/sdk/index.ts",
|
|
37
|
+
"module": "src/sdk/index.ts",
|
|
38
|
+
"types": "src/sdk/index.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./src/sdk/index.ts",
|
|
42
|
+
"default": "./src/sdk/index.ts"
|
|
43
|
+
},
|
|
44
|
+
"./cli": {
|
|
45
|
+
"default": "./src/index.ts"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
37
49
|
"publishConfig": {
|
|
38
50
|
"access": "public"
|
|
39
51
|
},
|
package/src/cli/program.ts
CHANGED
|
@@ -497,6 +497,21 @@ function wireSearchCommands(program: Command): void {
|
|
|
497
497
|
queryModes = parsed.value;
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
const { normalizeStructuredQueryInput } =
|
|
501
|
+
await import("../core/structured-query");
|
|
502
|
+
const normalizedInput = normalizeStructuredQueryInput(
|
|
503
|
+
queryText,
|
|
504
|
+
queryModes ?? []
|
|
505
|
+
);
|
|
506
|
+
if (!normalizedInput.ok) {
|
|
507
|
+
throw new CliError("VALIDATION", normalizedInput.error.message);
|
|
508
|
+
}
|
|
509
|
+
queryText = normalizedInput.value.query;
|
|
510
|
+
queryModes =
|
|
511
|
+
normalizedInput.value.queryModes.length > 0
|
|
512
|
+
? normalizedInput.value.queryModes
|
|
513
|
+
: undefined;
|
|
514
|
+
|
|
500
515
|
// Parse and validate tag filters
|
|
501
516
|
let tagsAll: string[] | undefined;
|
|
502
517
|
let tagsAny: string[] | undefined;
|
|
@@ -656,6 +671,21 @@ function wireSearchCommands(program: Command): void {
|
|
|
656
671
|
queryModes = parsed.value;
|
|
657
672
|
}
|
|
658
673
|
|
|
674
|
+
const { normalizeStructuredQueryInput } =
|
|
675
|
+
await import("../core/structured-query");
|
|
676
|
+
const normalizedInput = normalizeStructuredQueryInput(
|
|
677
|
+
queryText,
|
|
678
|
+
queryModes ?? []
|
|
679
|
+
);
|
|
680
|
+
if (!normalizedInput.ok) {
|
|
681
|
+
throw new CliError("VALIDATION", normalizedInput.error.message);
|
|
682
|
+
}
|
|
683
|
+
queryText = normalizedInput.value.query;
|
|
684
|
+
queryModes =
|
|
685
|
+
normalizedInput.value.queryModes.length > 0
|
|
686
|
+
? normalizedInput.value.queryModes
|
|
687
|
+
: undefined;
|
|
688
|
+
|
|
659
689
|
// Determine expansion/rerank settings based on flags
|
|
660
690
|
// Default: skip expansion (balanced mode)
|
|
661
691
|
let noExpand = true;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured multi-line query document parsing.
|
|
3
|
+
*
|
|
4
|
+
* Pure parser used across CLI, API, MCP, SDK, and Web.
|
|
5
|
+
*
|
|
6
|
+
* @module src/core/structured-query
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { QueryModeInput } from "../pipeline/types";
|
|
10
|
+
|
|
11
|
+
export interface StructuredQueryError {
|
|
12
|
+
line: number | null;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StructuredQueryNormalization {
|
|
17
|
+
query: string;
|
|
18
|
+
queryModes: QueryModeInput[];
|
|
19
|
+
usedStructuredQuerySyntax: boolean;
|
|
20
|
+
derivedQuery: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type StructuredQueryResult =
|
|
24
|
+
| { ok: true; value: StructuredQueryNormalization }
|
|
25
|
+
| { ok: false; error: StructuredQueryError };
|
|
26
|
+
|
|
27
|
+
const RECOGNIZED_MODE_PREFIXES = new Set(["term", "intent", "hyde"]);
|
|
28
|
+
const ANY_PREFIX_PATTERN = /^\s*([a-z][a-z0-9_-]*)\s*:\s*(.*)$/i;
|
|
29
|
+
const RECOGNIZED_PREFIX_PATTERN = /^\s*(term|intent|hyde)\s*:\s*(.*)$/i;
|
|
30
|
+
|
|
31
|
+
function buildError(
|
|
32
|
+
message: string,
|
|
33
|
+
line: number | null
|
|
34
|
+
): StructuredQueryResult {
|
|
35
|
+
return { ok: false, error: { message, line } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function trimNonBlankLines(query: string): string[] {
|
|
39
|
+
return query.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse multi-line structured query syntax.
|
|
44
|
+
*
|
|
45
|
+
* Rules:
|
|
46
|
+
* - single-line queries remain unchanged
|
|
47
|
+
* - blank lines are ignored
|
|
48
|
+
* - recognized typed lines: term:, intent:, hyde:
|
|
49
|
+
* - if structured syntax is used, unknown prefix lines like foo:bar are rejected
|
|
50
|
+
* - untyped lines contribute to the base query text
|
|
51
|
+
* - if no untyped lines exist, base query is derived from term lines first, then intent lines
|
|
52
|
+
* - hyde-only documents are rejected
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeStructuredQueryInput(
|
|
55
|
+
query: string,
|
|
56
|
+
explicitQueryModes: QueryModeInput[] = []
|
|
57
|
+
): StructuredQueryResult {
|
|
58
|
+
if (!query.includes("\n")) {
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
value: {
|
|
62
|
+
query,
|
|
63
|
+
queryModes: explicitQueryModes,
|
|
64
|
+
usedStructuredQuerySyntax: false,
|
|
65
|
+
derivedQuery: false,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lines = trimNonBlankLines(query);
|
|
71
|
+
if (lines.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
value: {
|
|
75
|
+
query,
|
|
76
|
+
queryModes: explicitQueryModes,
|
|
77
|
+
usedStructuredQuerySyntax: false,
|
|
78
|
+
derivedQuery: false,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hasRecognizedTypedLine = lines.some((line) => {
|
|
84
|
+
const match = line.match(RECOGNIZED_PREFIX_PATTERN);
|
|
85
|
+
return Boolean(match?.[1]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!hasRecognizedTypedLine) {
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
value: {
|
|
92
|
+
query,
|
|
93
|
+
queryModes: explicitQueryModes,
|
|
94
|
+
usedStructuredQuerySyntax: false,
|
|
95
|
+
derivedQuery: false,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const queryModes: QueryModeInput[] = [];
|
|
101
|
+
const bodyLines: string[] = [];
|
|
102
|
+
let hydeCount = 0;
|
|
103
|
+
|
|
104
|
+
for (const [index, line] of query.split(/\r?\n/).entries()) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (trimmed.length === 0) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const recognized = trimmed.match(RECOGNIZED_PREFIX_PATTERN);
|
|
111
|
+
if (recognized) {
|
|
112
|
+
const mode = recognized[1]?.toLowerCase() as QueryModeInput["mode"];
|
|
113
|
+
const text = recognized[2]?.trim() ?? "";
|
|
114
|
+
if (text.length === 0) {
|
|
115
|
+
return buildError(
|
|
116
|
+
`Structured query line ${index + 1} must contain non-empty text after ${mode}:`,
|
|
117
|
+
index + 1
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (mode === "hyde") {
|
|
121
|
+
hydeCount += 1;
|
|
122
|
+
if (hydeCount > 1) {
|
|
123
|
+
return buildError(
|
|
124
|
+
"Only one hyde line is allowed in a structured query document.",
|
|
125
|
+
index + 1
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
queryModes.push({ mode, text });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const prefixed = trimmed.match(ANY_PREFIX_PATTERN);
|
|
134
|
+
if (prefixed?.[1]) {
|
|
135
|
+
const prefix = prefixed[1].toLowerCase();
|
|
136
|
+
if (!RECOGNIZED_MODE_PREFIXES.has(prefix)) {
|
|
137
|
+
return buildError(
|
|
138
|
+
`Unknown structured query line prefix "${prefix}:" on line ${index + 1}. Expected term:, intent:, or hyde:.`,
|
|
139
|
+
index + 1
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
bodyLines.push(trimmed);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const combinedQueryModes = [...queryModes, ...explicitQueryModes];
|
|
148
|
+
const totalHydeCount = combinedQueryModes.filter(
|
|
149
|
+
(entry) => entry.mode === "hyde"
|
|
150
|
+
).length;
|
|
151
|
+
if (totalHydeCount > 1) {
|
|
152
|
+
return buildError(
|
|
153
|
+
"Only one hyde entry is allowed across structured query syntax and explicit query modes.",
|
|
154
|
+
null
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let normalizedQuery = bodyLines.join(" ").trim();
|
|
159
|
+
let derivedQuery = false;
|
|
160
|
+
|
|
161
|
+
if (!normalizedQuery) {
|
|
162
|
+
const termQuery = queryModes
|
|
163
|
+
.filter((entry) => entry.mode === "term")
|
|
164
|
+
.map((entry) => entry.text)
|
|
165
|
+
.join(" ")
|
|
166
|
+
.trim();
|
|
167
|
+
const intentQuery = queryModes
|
|
168
|
+
.filter((entry) => entry.mode === "intent")
|
|
169
|
+
.map((entry) => entry.text)
|
|
170
|
+
.join(" ")
|
|
171
|
+
.trim();
|
|
172
|
+
|
|
173
|
+
normalizedQuery = termQuery || intentQuery;
|
|
174
|
+
derivedQuery = normalizedQuery.length > 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!normalizedQuery) {
|
|
178
|
+
return buildError(
|
|
179
|
+
"Structured query documents must include at least one plain query line, term line, or intent line. hyde-only documents are not allowed.",
|
|
180
|
+
null
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
value: {
|
|
187
|
+
query: normalizedQuery,
|
|
188
|
+
queryModes: combinedQueryModes,
|
|
189
|
+
usedStructuredQuerySyntax: true,
|
|
190
|
+
derivedQuery,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function hasStructuredQuerySyntax(query: string): boolean {
|
|
196
|
+
const result = normalizeStructuredQueryInput(query);
|
|
197
|
+
return result.ok && result.value.usedStructuredQuerySyntax;
|
|
198
|
+
}
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { ToolContext } from "../server";
|
|
|
20
20
|
|
|
21
21
|
import { parseUri } from "../../app/constants";
|
|
22
22
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
23
|
+
import { normalizeStructuredQueryInput } from "../../core/structured-query";
|
|
23
24
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
24
25
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
25
26
|
import { getActivePreset } from "../../llm/registry";
|
|
@@ -143,6 +144,19 @@ export function handleQuery(
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
const normalizedInput = normalizeStructuredQueryInput(
|
|
148
|
+
args.query,
|
|
149
|
+
args.queryModes ?? []
|
|
150
|
+
);
|
|
151
|
+
if (!normalizedInput.ok) {
|
|
152
|
+
throw new Error(normalizedInput.error.message);
|
|
153
|
+
}
|
|
154
|
+
const queryText = normalizedInput.value.query;
|
|
155
|
+
const queryModes =
|
|
156
|
+
normalizedInput.value.queryModes.length > 0
|
|
157
|
+
? normalizedInput.value.queryModes
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
146
160
|
const preset = getActivePreset(ctx.config);
|
|
147
161
|
const llm = new LlmAdapter(ctx.config);
|
|
148
162
|
|
|
@@ -170,7 +184,7 @@ export function handleQuery(
|
|
|
170
184
|
// Determine noExpand/noRerank based on mode flags
|
|
171
185
|
// Priority: fast > thorough > expand/rerank params > defaults
|
|
172
186
|
// Default: noExpand=true (skip expansion), noRerank=false (with reranking)
|
|
173
|
-
const hasStructuredModes = Boolean(
|
|
187
|
+
const hasStructuredModes = Boolean(queryModes?.length);
|
|
174
188
|
let noExpand = true;
|
|
175
189
|
let noRerank = false;
|
|
176
190
|
|
|
@@ -245,7 +259,7 @@ export function handleQuery(
|
|
|
245
259
|
// Note: per spec, lang is a "hint" for query, not a filter
|
|
246
260
|
// Pass as queryLanguageHint to affect expansion prompt selection
|
|
247
261
|
// but NOT retrieval filtering (that would be options.lang)
|
|
248
|
-
const result = await searchHybrid(deps,
|
|
262
|
+
const result = await searchHybrid(deps, queryText, {
|
|
249
263
|
limit: args.limit ?? 5,
|
|
250
264
|
minScore: args.minScore,
|
|
251
265
|
collection: args.collection,
|
|
@@ -259,7 +273,7 @@ export function handleQuery(
|
|
|
259
273
|
author: args.author,
|
|
260
274
|
noExpand,
|
|
261
275
|
noRerank,
|
|
262
|
-
queryModes
|
|
276
|
+
queryModes,
|
|
263
277
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
264
278
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
265
279
|
});
|
|
@@ -46,26 +46,31 @@ export function parseQueryModeSpecs(
|
|
|
46
46
|
specs: string[]
|
|
47
47
|
): StoreResult<QueryModeInput[]> {
|
|
48
48
|
const parsed: QueryModeInput[] = [];
|
|
49
|
-
let hydeCount = 0;
|
|
50
|
-
|
|
51
49
|
for (const spec of specs) {
|
|
52
50
|
const entry = parseQueryModeSpec(spec);
|
|
53
51
|
if (!entry.ok) {
|
|
54
52
|
return entry;
|
|
55
53
|
}
|
|
56
|
-
if (entry.value.mode === "hyde") {
|
|
57
|
-
hydeCount += 1;
|
|
58
|
-
if (hydeCount > 1) {
|
|
59
|
-
return err(
|
|
60
|
-
"INVALID_INPUT",
|
|
61
|
-
"Only one hyde mode is allowed in structured query input."
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
54
|
parsed.push(entry.value);
|
|
66
55
|
}
|
|
67
56
|
|
|
68
|
-
return
|
|
57
|
+
return validateQueryModes(parsed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate normalized query mode objects.
|
|
62
|
+
*/
|
|
63
|
+
export function validateQueryModes(
|
|
64
|
+
queryModes: QueryModeInput[]
|
|
65
|
+
): StoreResult<QueryModeInput[]> {
|
|
66
|
+
const hydeCount = queryModes.filter((entry) => entry.mode === "hyde").length;
|
|
67
|
+
if (hydeCount > 1) {
|
|
68
|
+
return err(
|
|
69
|
+
"INVALID_INPUT",
|
|
70
|
+
"Only one hyde mode is allowed in structured query input."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return ok(queryModes);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
/**
|