@m6d/cortex-cli 0.17.0 → 0.19.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 +33 -0
- package/package.json +1 -1
- package/src/commands/apply.ts +28 -13
- package/src/commands/management.ts +17 -15
- package/src/commands/setup.ts +72 -60
- package/src/contracts/datasets/dataset-sql.ts +65 -28
- package/src/contracts/management/index.ts +169 -0
- package/src/contracts/management/procedures.generated.ts +5891 -0
- package/src/contracts/manifest/index.ts +199 -33
- package/src/contracts/manifest-file/index.ts +58 -6
- package/src/management/client.ts +17 -77
- package/src/scaffold/files.ts +2 -2
|
@@ -1243,22 +1243,163 @@ const manifestWebhookSchema = z.strictObject({
|
|
|
1243
1243
|
});
|
|
1244
1244
|
export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
|
|
1245
1245
|
|
|
1246
|
-
/**
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1246
|
+
/**
|
|
1247
|
+
* A collection's name and description in characters, and how many documents
|
|
1248
|
+
* a manifest lists in one. The console caps its editor at the same.
|
|
1249
|
+
*/
|
|
1250
|
+
export const COLLECTION_LIMITS = {
|
|
1251
|
+
nameLength: 80,
|
|
1252
|
+
descriptionLength: 2_000,
|
|
1253
|
+
documents: 100,
|
|
1254
|
+
} as const;
|
|
1255
|
+
|
|
1256
|
+
/** A knowledge document's name in characters and its size in bytes, however it arrives. */
|
|
1257
|
+
export const DOCUMENT_LIMITS = { nameLength: 255, bytes: 10 * 1024 * 1024 } as const;
|
|
1258
|
+
|
|
1259
|
+
export const DOCX_MIME_TYPE =
|
|
1260
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
1261
|
+
|
|
1262
|
+
/** The content types Cortex parses a knowledge document as. */
|
|
1263
|
+
export const DOCUMENT_MIME_TYPES = [
|
|
1264
|
+
"application/pdf",
|
|
1265
|
+
DOCX_MIME_TYPE,
|
|
1266
|
+
"text/markdown",
|
|
1267
|
+
"text/plain",
|
|
1268
|
+
"text/html",
|
|
1269
|
+
] as const;
|
|
1270
|
+
export type DocumentMimeType = (typeof DOCUMENT_MIME_TYPES)[number];
|
|
1271
|
+
|
|
1272
|
+
const mimeTypeByExtension = new Map<string, DocumentMimeType>([
|
|
1273
|
+
["md", "text/markdown"],
|
|
1274
|
+
["txt", "text/plain"],
|
|
1275
|
+
["html", "text/html"],
|
|
1276
|
+
["htm", "text/html"],
|
|
1277
|
+
["pdf", "application/pdf"],
|
|
1278
|
+
["docx", DOCX_MIME_TYPE],
|
|
1279
|
+
]);
|
|
1280
|
+
|
|
1281
|
+
/** The content type a document is parsed as, from the extension its name ends in; none for any other name. */
|
|
1282
|
+
export function documentType(name: string) {
|
|
1283
|
+
const dot = name.lastIndexOf(".");
|
|
1284
|
+
return dot === -1 ? undefined : mimeTypeByExtension.get(name.slice(dot + 1).toLowerCase());
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** How many bytes a base64 string decodes to. */
|
|
1288
|
+
function base64Bytes(data: string) {
|
|
1289
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
1290
|
+
return (data.length / 4) * 3 - padding;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const documentNameSchema = z.string().trim().min(1).max(DOCUMENT_LIMITS.nameLength);
|
|
1294
|
+
|
|
1295
|
+
/** A document written in the manifest itself; its name's extension says how it is parsed. */
|
|
1296
|
+
const textDocumentSchema = z.strictObject({
|
|
1297
|
+
name: documentNameSchema,
|
|
1298
|
+
text: z
|
|
1299
|
+
.string()
|
|
1300
|
+
.min(1)
|
|
1301
|
+
.refine(
|
|
1302
|
+
(text) => new TextEncoder().encode(text).byteLength <= DOCUMENT_LIMITS.bytes,
|
|
1303
|
+
`larger than ${DOCUMENT_LIMITS.bytes} bytes`,
|
|
1304
|
+
),
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
/** A file `cortex apply` reads, its path relative to the manifest file; named after the file unless `name` says otherwise. */
|
|
1308
|
+
const pathDocumentSchema = z.strictObject({
|
|
1309
|
+
path: z.string().trim().min(1),
|
|
1310
|
+
name: documentNameSchema.optional(),
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
/** A file Cortex reads on every apply, with no credentials; named after the URL's last segment unless `name` says otherwise. */
|
|
1314
|
+
const urlDocumentSchema = z.strictObject({
|
|
1315
|
+
url: upstreamUrlSchema,
|
|
1316
|
+
name: documentNameSchema.optional(),
|
|
1259
1317
|
});
|
|
1318
|
+
|
|
1319
|
+
/** A file's bytes in base64, as `cortex apply` sends a `path`. */
|
|
1320
|
+
const dataDocumentSchema = z.strictObject({
|
|
1321
|
+
name: documentNameSchema,
|
|
1322
|
+
data: z
|
|
1323
|
+
.base64()
|
|
1324
|
+
.refine(
|
|
1325
|
+
(data) => base64Bytes(data) <= DOCUMENT_LIMITS.bytes,
|
|
1326
|
+
`larger than ${DOCUMENT_LIMITS.bytes} bytes`,
|
|
1327
|
+
),
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
/** The name a document goes by: its own, else its file's or its URL's last segment. */
|
|
1331
|
+
export function documentName(document: { name?: string; path?: string; url?: string }) {
|
|
1332
|
+
if (document.name !== undefined) return document.name;
|
|
1333
|
+
if (document.path !== undefined) return document.path.split(/[/\\]/).pop() ?? "";
|
|
1334
|
+
const segment = new URL(document.url ?? "", "https://x").pathname.split("/").pop() ?? "";
|
|
1335
|
+
try {
|
|
1336
|
+
return decodeURIComponent(segment);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return segment;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** A collection's documents: each name once, and each a name whose extension Cortex parses. */
|
|
1343
|
+
function documentsSchema<TDocument extends z.ZodType<Parameters<typeof documentName>[0]>>(
|
|
1344
|
+
document: TDocument,
|
|
1345
|
+
) {
|
|
1346
|
+
return z
|
|
1347
|
+
.array(document)
|
|
1348
|
+
.max(COLLECTION_LIMITS.documents)
|
|
1349
|
+
.superRefine((documents, ctx) => {
|
|
1350
|
+
const names = documents.map(documentName);
|
|
1351
|
+
names.forEach((name, index) => {
|
|
1352
|
+
if (documentType(name) === undefined) {
|
|
1353
|
+
ctx.addIssue({
|
|
1354
|
+
code: "custom",
|
|
1355
|
+
path: [index, "name"],
|
|
1356
|
+
message: "a document's name ends in .md, .txt, .html, .htm, .pdf or .docx",
|
|
1357
|
+
});
|
|
1358
|
+
} else if (names.indexOf(name) !== index) {
|
|
1359
|
+
ctx.addIssue({
|
|
1360
|
+
code: "custom",
|
|
1361
|
+
path: [index, "name"],
|
|
1362
|
+
message: "a name repeats",
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
})
|
|
1367
|
+
.default([]);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Holds documents. Its `documents` are the manifest's: an apply stores what
|
|
1372
|
+
* is new or changed and retires what it no longer lists, unless
|
|
1373
|
+
* `overridable` offers them, and then a document added or removed in
|
|
1374
|
+
* Cortex stays so until the manifest changes that document too.
|
|
1375
|
+
*/
|
|
1376
|
+
function collectionSchema<TDocument extends z.ZodType<Parameters<typeof documentName>[0]>>(
|
|
1377
|
+
document: TDocument,
|
|
1378
|
+
) {
|
|
1379
|
+
return z.strictObject({
|
|
1380
|
+
name: z.string().trim().min(1).max(COLLECTION_LIMITS.nameLength),
|
|
1381
|
+
description: z.string().trim().max(COLLECTION_LIMITS.descriptionLength).default(""),
|
|
1382
|
+
/** The key of the service every document in the collection belongs to. */
|
|
1383
|
+
service: slugSchema.optional(),
|
|
1384
|
+
/** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
|
|
1385
|
+
citable: z.boolean().default(true),
|
|
1386
|
+
documents: documentsSchema(document),
|
|
1387
|
+
audience: audienceSchema,
|
|
1388
|
+
overridable: overridable(["description", "documents"]),
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const manifestDocumentSchema = z.union([textDocumentSchema, pathDocumentSchema, urlDocumentSchema]);
|
|
1393
|
+
export type ManifestDocument = z.input<typeof manifestDocumentSchema>;
|
|
1394
|
+
|
|
1395
|
+
const manifestCollectionSchema = collectionSchema(manifestDocumentSchema);
|
|
1260
1396
|
export type ManifestCollection = z.input<typeof manifestCollectionSchema>;
|
|
1261
1397
|
|
|
1398
|
+
/** A collection as Cortex receives it: each `path` read into `data`. */
|
|
1399
|
+
const resolvedCollectionSchema = collectionSchema(
|
|
1400
|
+
z.union([textDocumentSchema, dataDocumentSchema, urlDocumentSchema]),
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1262
1403
|
export const AUTH_JWT_SECRET_ALGORITHMS = ["HS256", "HS384", "HS512"] as const;
|
|
1263
1404
|
|
|
1264
1405
|
const issuerSchema = z.string().trim().min(1).max(2048);
|
|
@@ -1335,22 +1476,6 @@ export const agentSettingsSchema = z.strictObject({
|
|
|
1335
1476
|
/** Off, end users cannot message the agent: only its triggers reach it. */
|
|
1336
1477
|
messageTrigger: z.boolean().default(true),
|
|
1337
1478
|
defaultLocale: localeSchema.default(BASE_LOCALE),
|
|
1338
|
-
rateLimits: z
|
|
1339
|
-
.strictObject({
|
|
1340
|
-
allUsers: z
|
|
1341
|
-
.strictObject({
|
|
1342
|
-
messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "allUsers"),
|
|
1343
|
-
tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "allUsers"),
|
|
1344
|
-
})
|
|
1345
|
-
.prefault({}),
|
|
1346
|
-
perUser: z
|
|
1347
|
-
.strictObject({
|
|
1348
|
-
messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "perUser"),
|
|
1349
|
-
tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "perUser"),
|
|
1350
|
-
})
|
|
1351
|
-
.prefault({}),
|
|
1352
|
-
})
|
|
1353
|
-
.prefault({}),
|
|
1354
1479
|
context: z
|
|
1355
1480
|
.strictObject({
|
|
1356
1481
|
maxContextTokens: setting(agentContext.maxContextTokens),
|
|
@@ -1395,9 +1520,34 @@ const manifestAgentSchema = z.strictObject({
|
|
|
1395
1520
|
})
|
|
1396
1521
|
.default({}),
|
|
1397
1522
|
settings: agentSettingsSchema.prefault({}),
|
|
1523
|
+
/** Messages a minute and model tokens a UTC day, for all users together and for each; null, no limit. */
|
|
1524
|
+
rateLimits: z
|
|
1525
|
+
.strictObject({
|
|
1526
|
+
allUsers: z
|
|
1527
|
+
.strictObject({
|
|
1528
|
+
messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "allUsers"),
|
|
1529
|
+
tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "allUsers"),
|
|
1530
|
+
})
|
|
1531
|
+
.prefault({}),
|
|
1532
|
+
perUser: z
|
|
1533
|
+
.strictObject({
|
|
1534
|
+
messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "perUser"),
|
|
1535
|
+
tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "perUser"),
|
|
1536
|
+
})
|
|
1537
|
+
.prefault({}),
|
|
1538
|
+
})
|
|
1539
|
+
.prefault({}),
|
|
1398
1540
|
suggestions: suggestionsSchema.default(() => byLocale(() => [])),
|
|
1399
1541
|
greeting: greetingSchema.default(() => byLocale(() => "")),
|
|
1400
|
-
overridable: overridable([
|
|
1542
|
+
overridable: overridable([
|
|
1543
|
+
"name",
|
|
1544
|
+
"prompt",
|
|
1545
|
+
"models",
|
|
1546
|
+
"settings",
|
|
1547
|
+
"rateLimits",
|
|
1548
|
+
"suggestions",
|
|
1549
|
+
"greeting",
|
|
1550
|
+
]),
|
|
1401
1551
|
});
|
|
1402
1552
|
export type ManifestAgent = z.input<typeof manifestAgentSchema>;
|
|
1403
1553
|
|
|
@@ -1477,6 +1627,15 @@ export const manifestSchema = z.strictObject({
|
|
|
1477
1627
|
/** A manifest as written: every field with a default may be left out. */
|
|
1478
1628
|
export type Manifest = z.input<typeof manifestSchema>;
|
|
1479
1629
|
|
|
1630
|
+
/**
|
|
1631
|
+
* A manifest as Cortex receives it: the one written, each document `path`
|
|
1632
|
+
* read into `data` by `cortex apply`, since Cortex cannot read the files
|
|
1633
|
+
* beside it.
|
|
1634
|
+
*/
|
|
1635
|
+
export const resolvedManifestSchema = manifestSchema.extend({
|
|
1636
|
+
collections: listOf(MANIFEST_NAME_FIELDS.collections, resolvedCollectionSchema),
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1480
1639
|
const manifestKindSchema = z.enum(MANIFEST_KINDS);
|
|
1481
1640
|
|
|
1482
1641
|
/**
|
|
@@ -1492,11 +1651,17 @@ export const manifestChangesSchema = z.object({
|
|
|
1492
1651
|
delete: namesByKindSchema,
|
|
1493
1652
|
});
|
|
1494
1653
|
|
|
1495
|
-
/**
|
|
1654
|
+
/**
|
|
1655
|
+
* A field of one entry that both the manifest and a live write changed since
|
|
1656
|
+
* the last apply. A field holding named parts (an agent's settings, its
|
|
1657
|
+
* greeting and suggestions by locale, a collection's documents by name) is
|
|
1658
|
+
* judged part by part, and `key` names the part.
|
|
1659
|
+
*/
|
|
1496
1660
|
const manifestConflictSchema = z.strictObject({
|
|
1497
1661
|
kind: manifestKindSchema,
|
|
1498
1662
|
name: z.string(),
|
|
1499
1663
|
field: z.string(),
|
|
1664
|
+
key: z.string().optional(),
|
|
1500
1665
|
});
|
|
1501
1666
|
|
|
1502
1667
|
/** Which value a conflict keeps: the one changed live in Cortex, or the manifest's. */
|
|
@@ -1506,9 +1671,10 @@ const conflictSideSchema = z.enum(["live", "manifest"]);
|
|
|
1506
1671
|
* What preview and sync take: the manifest, and how its conflicts resolve.
|
|
1507
1672
|
* A conflict takes its own entry in `resolutions`, else `prefer`; a sync
|
|
1508
1673
|
* left with one neither answers is refused as `unresolved_conflicts`,
|
|
1509
|
-
* naming them. A resolution
|
|
1674
|
+
* naming them. A resolution answers the conflict with its kind, name, field
|
|
1675
|
+
* and key; one for a field that no longer conflicts is ignored.
|
|
1510
1676
|
*/
|
|
1511
|
-
export const manifestApplyInputSchema =
|
|
1677
|
+
export const manifestApplyInputSchema = resolvedManifestSchema.extend({
|
|
1512
1678
|
prefer: conflictSideSchema.optional(),
|
|
1513
1679
|
resolutions: z.array(manifestConflictSchema.extend({ keep: conflictSideSchema })).default([]),
|
|
1514
1680
|
});
|
|
@@ -1,16 +1,68 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
1
3
|
import { pathToFileURL } from "node:url";
|
|
2
4
|
|
|
3
|
-
import {
|
|
5
|
+
import { isRecord } from "../json/index";
|
|
6
|
+
import { DOCUMENT_LIMITS, documentName, manifestSchema } from "../manifest/index";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Imports a manifest file, TypeScript or JSON (Bun imports both), and checks
|
|
7
|
-
* its default export against the contract: `
|
|
8
|
-
* written, for a caller that lets Cortex fill its
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* its default export against the contract: `parsed` is the check, and
|
|
11
|
+
* `written` the export as written, for a caller that lets Cortex fill its
|
|
12
|
+
* own defaults, with each document `path` read, relative to the file, into
|
|
13
|
+
* the `data` Cortex takes (`resolvedManifestSchema`). Throws when the file
|
|
14
|
+
* does not import, exports nothing, or names a document file it cannot
|
|
15
|
+
* read, that is empty, or that is too large. Kept out of the contract's
|
|
16
|
+
* module, which the browser imports too.
|
|
11
17
|
*/
|
|
12
18
|
export async function loadManifestFile(path: string) {
|
|
13
19
|
const module = (await import(pathToFileURL(path).href)) as { default?: unknown };
|
|
14
20
|
if (module.default === undefined) throw new Error("The module has no default export.");
|
|
15
|
-
|
|
21
|
+
const parsed = manifestSchema.safeParse(module.default);
|
|
22
|
+
return {
|
|
23
|
+
written: parsed.success
|
|
24
|
+
? await readDocuments(module.default, dirname(path))
|
|
25
|
+
: module.default,
|
|
26
|
+
parsed,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `written` with every document `path` read from `directory` into `data`. */
|
|
31
|
+
async function readDocuments(written: unknown, directory: string) {
|
|
32
|
+
if (!isRecord(written) || !Array.isArray(written.collections)) return written;
|
|
33
|
+
const collections = await Promise.all(
|
|
34
|
+
written.collections.map(async (collection: unknown) => {
|
|
35
|
+
if (!isRecord(collection) || !Array.isArray(collection.documents)) return collection;
|
|
36
|
+
const documents = await Promise.all(
|
|
37
|
+
collection.documents.map((document: unknown) =>
|
|
38
|
+
isRecord(document) && typeof document.path === "string"
|
|
39
|
+
? readDocument(
|
|
40
|
+
String(collection.name),
|
|
41
|
+
document.path,
|
|
42
|
+
document.name,
|
|
43
|
+
directory,
|
|
44
|
+
)
|
|
45
|
+
: document,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return { ...collection, documents };
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
return { ...written, collections };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One document file as `{ name, data }`, named after the file unless `name` says otherwise. */
|
|
55
|
+
async function readDocument(collection: string, path: string, name: unknown, directory: string) {
|
|
56
|
+
const where = `Collection ${collection}: ${path}`;
|
|
57
|
+
const bytes = await readFile(resolve(directory, path)).catch((cause: unknown) => {
|
|
58
|
+
throw new Error(`${where} could not be read.`, { cause });
|
|
59
|
+
});
|
|
60
|
+
if (bytes.byteLength === 0) throw new Error(`${where} is empty.`);
|
|
61
|
+
if (bytes.byteLength > DOCUMENT_LIMITS.bytes) {
|
|
62
|
+
throw new Error(`${where} is larger than ${DOCUMENT_LIMITS.bytes} bytes.`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name: documentName({ name: typeof name === "string" ? name : undefined, path }),
|
|
66
|
+
data: bytes.toString("base64"),
|
|
67
|
+
};
|
|
16
68
|
}
|
package/src/management/client.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { createManagementClient, MANAGEMENT_BASE_PATH } from "@cortex/contracts/management";
|
|
6
|
+
import type { ManagementTarget } from "@cortex/contracts/management";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
|
|
8
9
|
import { profilesPath } from "./profiles";
|
|
@@ -109,7 +110,7 @@ export async function loadOpenApi(url: string) {
|
|
|
109
110
|
const path = cachePath(url);
|
|
110
111
|
const cached = readCache(path);
|
|
111
112
|
try {
|
|
112
|
-
const response = await fetch(`${url}/
|
|
113
|
+
const response = await fetch(`${url}${MANAGEMENT_BASE_PATH}/openapi.json`, {
|
|
113
114
|
headers: cached ? { "If-None-Match": cached.etag } : {},
|
|
114
115
|
});
|
|
115
116
|
if (response.status === 304 && cached)
|
|
@@ -159,71 +160,28 @@ export function inputSchemaOf(operation: OpenApiOperation) {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* anything else JSON-encoded, which is how the document declares it. A
|
|
165
|
-
* mutation posts its input as the JSON body. A refusal throws
|
|
166
|
-
* `ManagementError` with the error body's code, message, detail and sentence.
|
|
163
|
+
* Where `profile`'s management calls go; a Cortex that does not answer at
|
|
164
|
+
* all rejects as `UnreachableError`, apart from one that refused.
|
|
167
165
|
*/
|
|
168
|
-
export
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const response = await fetch(url, {
|
|
177
|
-
method: method.toUpperCase(),
|
|
178
|
-
headers: {
|
|
179
|
-
Authorization: `Bearer ${profile.key}`,
|
|
180
|
-
...(method === "post" ? { "Content-Type": "application/json" } : {}),
|
|
181
|
-
},
|
|
182
|
-
body: method === "post" && input !== undefined ? JSON.stringify(input) : undefined,
|
|
183
|
-
}).catch((error: unknown) => {
|
|
184
|
-
throw new UnreachableError(error);
|
|
185
|
-
});
|
|
186
|
-
const body: unknown = await response.json().catch(() => null);
|
|
187
|
-
if (!response.ok) {
|
|
188
|
-
const { code, message, refusal, cause } = (body ?? {}) as {
|
|
189
|
-
code?: string;
|
|
190
|
-
message?: string;
|
|
191
|
-
refusal?: string;
|
|
192
|
-
cause?: unknown;
|
|
193
|
-
};
|
|
194
|
-
throw new ManagementError(
|
|
195
|
-
response.status,
|
|
196
|
-
code ?? "error",
|
|
197
|
-
message ?? response.statusText,
|
|
198
|
-
isRecord(cause) ? cause : undefined,
|
|
199
|
-
refusal,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
return body;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function isScalar(value: unknown): value is string | number | boolean | null {
|
|
206
|
-
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
166
|
+
export function managementTarget(profile: { url: string; key: string }) {
|
|
167
|
+
return {
|
|
168
|
+
...profile,
|
|
169
|
+
fetch: (url: string, init: RequestInit) =>
|
|
170
|
+
fetch(url, init).catch((error: unknown) => {
|
|
171
|
+
throw new UnreachableError(error);
|
|
172
|
+
}),
|
|
173
|
+
} satisfies ManagementTarget;
|
|
207
174
|
}
|
|
208
175
|
|
|
209
|
-
/**
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
for (const [name, value] of Object.entries(input)) {
|
|
213
|
-
if (value === undefined) continue;
|
|
214
|
-
if (isScalar(value)) params.set(name, String(value));
|
|
215
|
-
else if (Array.isArray(value) && value.length > 0 && value.every(isScalar)) {
|
|
216
|
-
for (const item of value) params.append(name, String(item));
|
|
217
|
-
} else params.set(name, JSON.stringify(value));
|
|
218
|
-
}
|
|
219
|
-
const encoded = params.toString();
|
|
220
|
-
return encoded === "" ? "" : `?${encoded}`;
|
|
176
|
+
/** The typed client for the calls the CLI makes by name, over `managementTarget`. */
|
|
177
|
+
export function managementClient(profile: { url: string; key: string }) {
|
|
178
|
+
return createManagementClient(managementTarget(profile));
|
|
221
179
|
}
|
|
222
180
|
|
|
223
181
|
/**
|
|
224
182
|
* Cortex did not answer at all: refused, reset, unresolvable. `fetch` rejects
|
|
225
183
|
* only for these, with whatever its runtime throws (Bun's is a plain `Error`
|
|
226
|
-
* with a `code`), so `
|
|
184
|
+
* with a `code`), so `managementTarget` names them once here.
|
|
227
185
|
*/
|
|
228
186
|
export class UnreachableError extends Error {
|
|
229
187
|
public constructor(cause: unknown) {
|
|
@@ -231,21 +189,3 @@ export class UnreachableError extends Error {
|
|
|
231
189
|
this.name = "UnreachableError";
|
|
232
190
|
}
|
|
233
191
|
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* A refusal; `detail` is what the refusal names, when it names something (an
|
|
237
|
-
* entry, the row holding a name), and `refusal` the sentence Cortex words it
|
|
238
|
-
* in, when it has one.
|
|
239
|
-
*/
|
|
240
|
-
export class ManagementError extends Error {
|
|
241
|
-
public constructor(
|
|
242
|
-
public readonly status: number,
|
|
243
|
-
public readonly code: string,
|
|
244
|
-
message: string,
|
|
245
|
-
public readonly detail?: Record<string, unknown>,
|
|
246
|
-
public readonly refusal?: string,
|
|
247
|
-
) {
|
|
248
|
-
super(message);
|
|
249
|
-
this.name = "ManagementError";
|
|
250
|
-
}
|
|
251
|
-
}
|
package/src/scaffold/files.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { applyPorts, type Ports } from "@/scaffold/ports";
|
|
|
29
29
|
* `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
|
|
30
30
|
* before the CLI publishes, so a scaffold pins what was current then.
|
|
31
31
|
*/
|
|
32
|
-
const CORTEX_VERSION = "0.
|
|
32
|
+
const CORTEX_VERSION = "0.18.0";
|
|
33
33
|
|
|
34
34
|
/** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
|
|
35
35
|
export const DOCKER_HOST = "host.docker.internal";
|
|
@@ -150,7 +150,7 @@ function packageJson({ name, datasets, setup }: Project) {
|
|
|
150
150
|
verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
|
|
151
151
|
},
|
|
152
152
|
dependencies: {
|
|
153
|
-
"@m6d/cortex": "~0.
|
|
153
|
+
"@m6d/cortex": "~0.6.0",
|
|
154
154
|
// drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
|
|
155
155
|
...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
|
|
156
156
|
hono: "^4.12.3",
|