@frontera-sdk/cli 1.45.7 → 1.45.8
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/package.json +4 -4
- package/src/api/blueprint-authoring-api.ts +20 -2
- package/src/api/dataset-api.ts +34 -1
- package/src/api/validation-detail.ts +87 -0
- package/src/commands/agent/index-commands.ts +24 -7
- package/src/commands/blueprint/authoring.ts +37 -7
- package/src/commands/blueprint/declarative.ts +7 -1
- package/src/commands/blueprint/grants.ts +32 -5
- package/src/commands/dataset/index-commands.ts +152 -5
- package/src/commands/knowledge/index-commands.ts +21 -2
- package/src/commands/knowledge/upload-batch.ts +55 -0
- package/src/commands/source/index-commands.ts +79 -1
- package/src/vendor/kit-assets.json +9 -9
- package/src/vendor/sdk-sources.json +1 -1
|
@@ -121,6 +121,61 @@ export function summarizeUploads(
|
|
|
121
121
|
* the count of what did not land has to be the first thing on the page rather
|
|
122
122
|
* than a column someone has to notice.
|
|
123
123
|
*/
|
|
124
|
+
/**
|
|
125
|
+
* How many source rows already carry these filenames.
|
|
126
|
+
*
|
|
127
|
+
* A failed upload does NOT mean nothing was recorded: the service registers the
|
|
128
|
+
* source and then fails downstream — ingestion, embedding — so `all 1
|
|
129
|
+
* upload(s) failed` was printed while a `pending` row sat in the base. Retrying
|
|
130
|
+
* added a second row for the same file, which is the opposite of the "a retry
|
|
131
|
+
* duplicates nothing" the hint used to promise.
|
|
132
|
+
*
|
|
133
|
+
* Counted rather than guessed, and matched on the filename because that is all
|
|
134
|
+
* a failed upload leaves behind — no id comes back. Names are not unique, so
|
|
135
|
+
* this is reported as "rows carrying this name", never as "your upload".
|
|
136
|
+
*/
|
|
137
|
+
export async function countExistingSources(
|
|
138
|
+
api: { knowledgeSources(id: string): Promise<unknown[]> },
|
|
139
|
+
baseId: string,
|
|
140
|
+
filenames: string[],
|
|
141
|
+
): Promise<Map<string, number>> {
|
|
142
|
+
const wanted = new Set(filenames)
|
|
143
|
+
const counts = new Map<string, number>()
|
|
144
|
+
// Best effort: this runs on a path that is already failing, and a second
|
|
145
|
+
// failure here must not replace the cause the caller needs to see.
|
|
146
|
+
const rows = await api.knowledgeSources(baseId).catch(() => [] as unknown[])
|
|
147
|
+
for (const row of rows as Array<{ fileName?: string }>) {
|
|
148
|
+
const name = row?.fileName
|
|
149
|
+
if (!name || !wanted.has(name)) continue
|
|
150
|
+
counts.set(name, (counts.get(name) ?? 0) + 1)
|
|
151
|
+
}
|
|
152
|
+
return counts
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What to say about rows a failed upload may have left behind.
|
|
157
|
+
*
|
|
158
|
+
* Returns `undefined` when there is nothing to warn about, so a clean failure
|
|
159
|
+
* keeps a clean message.
|
|
160
|
+
*/
|
|
161
|
+
export function describeExistingSources(counts: Map<string, number>): string | undefined {
|
|
162
|
+
const present = [...counts.entries()].filter(([, n]) => n > 0)
|
|
163
|
+
if (present.length === 0) return undefined
|
|
164
|
+
|
|
165
|
+
const total = present.reduce((sum, [, n]) => sum + n, 0)
|
|
166
|
+
const duplicated = present.filter(([, n]) => n > 1)
|
|
167
|
+
return (
|
|
168
|
+
`${total} source row${total === 1 ? '' : 's'} already `
|
|
169
|
+
+ `${total === 1 ? 'carries' : 'carry'} `
|
|
170
|
+
+ `${present.length === 1 ? 'that filename' : 'those filenames'}`
|
|
171
|
+
+ (duplicated.length > 0
|
|
172
|
+
? `, and ${duplicated.length} filename${duplicated.length === 1 ? '' : 's'} `
|
|
173
|
+
+ `${duplicated.length === 1 ? 'has' : 'have'} more than one — `
|
|
174
|
+
+ 'a failed upload still registers a source, so re-running adds another'
|
|
175
|
+
: ' — a failed upload still registers a source')
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
124
179
|
export function renderUploadSummary(summary: UploadSummary): string {
|
|
125
180
|
const lines: string[] = []
|
|
126
181
|
const total = summary.sources.length
|
|
@@ -172,7 +172,85 @@ function requireConfig(doc: SourceFile, path: string): Record<string, unknown> {
|
|
|
172
172
|
hint: 'config: { kind: "postgres", host, port, database, username, sslMode, connectTimeoutMs }',
|
|
173
173
|
})
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
const config = doc.config as Record<string, unknown>
|
|
176
|
+
validateConnectionConfig(config, path)
|
|
177
|
+
return config
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const SSL_MODES = ['require', 'verify-ca', 'verify-full', 'disable']
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The connection block, checked here for the reason `validateDatasets` gives.
|
|
184
|
+
*
|
|
185
|
+
* That function exists because the service's refusal for this body names the
|
|
186
|
+
* wrong field — `strictJsonBody` substitutes a sentinel that omits `mode`, so
|
|
187
|
+
* ANY failure anywhere in the document comes back as "Expected 'virtual' at
|
|
188
|
+
* /datasets/0/mode". It pre-empted that for `datasets` and not for `config`,
|
|
189
|
+
* which left the commonest mistakes unreachable: a `config` missing `kind` or
|
|
190
|
+
* `connectTimeoutMs` was reported as a bad `mode` on a dataset whose `mode` was
|
|
191
|
+
* already `"virtual"`.
|
|
192
|
+
*
|
|
193
|
+
* That is how `source create` came to look like a write path with no passing
|
|
194
|
+
* document. It has one — the errors were describing the wrong half of the file.
|
|
195
|
+
*
|
|
196
|
+
* `password` is deliberately absent: it never travels in the file, and a file
|
|
197
|
+
* carrying one is refused rather than stripped, above.
|
|
198
|
+
*/
|
|
199
|
+
export function validateConnectionConfig(config: Record<string, unknown>, path: string): void {
|
|
200
|
+
const where = `${path}: config`
|
|
201
|
+
|
|
202
|
+
if (config.kind !== 'postgres') {
|
|
203
|
+
throw new CliError(`${where} has kind ${JSON.stringify(config.kind)}.`, {
|
|
204
|
+
code: 'USAGE',
|
|
205
|
+
hint: 'kind must be "postgres" — it is the only connector the service accepts today',
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const field of ['host', 'database', 'username'] as const) {
|
|
210
|
+
const value = config[field]
|
|
211
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
212
|
+
throw new CliError(`${where}.${field} is ${JSON.stringify(value)}.`, {
|
|
213
|
+
code: 'USAGE',
|
|
214
|
+
hint: `${field} must be a non-empty string`,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const port = config.port
|
|
220
|
+
if (!Number.isInteger(port) || (port as number) < 1 || (port as number) > 65_535) {
|
|
221
|
+
throw new CliError(`${where}.port is ${JSON.stringify(port)}.`, {
|
|
222
|
+
code: 'USAGE',
|
|
223
|
+
hint: 'port must be a whole number between 1 and 65535 — 5432 for a default PostgreSQL',
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (typeof config.sslMode !== 'string' || !SSL_MODES.includes(config.sslMode)) {
|
|
228
|
+
throw new CliError(`${where}.sslMode is ${JSON.stringify(config.sslMode)}.`, {
|
|
229
|
+
code: 'USAGE',
|
|
230
|
+
hint: `sslMode must be one of: ${SSL_MODES.join(', ')}`,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const timeout = config.connectTimeoutMs
|
|
235
|
+
if (!Number.isInteger(timeout) || (timeout as number) < 1_000 || (timeout as number) > 15_000) {
|
|
236
|
+
throw new CliError(`${where}.connectTimeoutMs is ${JSON.stringify(timeout)}.`, {
|
|
237
|
+
code: 'USAGE',
|
|
238
|
+
// Easily missed: it has no sensible default the CLI could supply, because
|
|
239
|
+
// the right value depends on where the database is.
|
|
240
|
+
hint: 'connectTimeoutMs must be a whole number of milliseconds between 1000 and 15000',
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The service sets `additionalProperties: false`, so an extra key fails the
|
|
245
|
+
// whole document — and fails it by naming a dataset's `mode`.
|
|
246
|
+
const KNOWN = new Set(['kind', 'host', 'port', 'database', 'username', 'sslMode', 'connectTimeoutMs'])
|
|
247
|
+
const unknown = Object.keys(config).filter((key) => !KNOWN.has(key))
|
|
248
|
+
if (unknown.length > 0) {
|
|
249
|
+
throw new CliError(`${where} carries unknown field(s): ${unknown.join(', ')}.`, {
|
|
250
|
+
code: 'USAGE',
|
|
251
|
+
hint: `config accepts exactly: ${[...KNOWN].join(', ')}`,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
176
254
|
}
|
|
177
255
|
|
|
178
256
|
const list: Command = {
|