saturndocs 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth-FRCFGNYT.js +14 -0
  3. package/dist/brokenLinks-TTOBP4P7.js +48 -0
  4. package/dist/build-F5PWGP57.js +39 -0
  5. package/dist/chunk-3P2OFTXR.js +71 -0
  6. package/dist/chunk-3V753MRI.js +100 -0
  7. package/dist/chunk-5KX4ZK5E.js +104 -0
  8. package/dist/chunk-CMNX5DLQ.js +119 -0
  9. package/dist/chunk-GYRVGWDM.js +3499 -0
  10. package/dist/chunk-I6U4V7B4.js +1552 -0
  11. package/dist/chunk-J6P3VGGE.js +21 -0
  12. package/dist/chunk-KGJESHJC.js +37 -0
  13. package/dist/chunk-M2BZEDAS.js +57 -0
  14. package/dist/chunk-N7QPZMLP.js +82 -0
  15. package/dist/chunk-OEAEX5YW.js +51 -0
  16. package/dist/chunk-OZWTXMO3.js +584 -0
  17. package/dist/chunk-QLFODLVN.js +46 -0
  18. package/dist/chunk-R3X2DPFF.js +568 -0
  19. package/dist/chunk-S45UWCML.js +588 -0
  20. package/dist/chunk-SABK3R2P.js +2452 -0
  21. package/dist/chunk-STCCGFKC.js +246 -0
  22. package/dist/chunk-TKISMN3P.js +39 -0
  23. package/dist/chunk-U7PKMSB3.js +465 -0
  24. package/dist/chunk-VNYDYHXM.js +22 -0
  25. package/dist/chunk-VXEUNRVA.js +26 -0
  26. package/dist/chunk-XO4CUC7V.js +99 -0
  27. package/dist/cli.d.ts +1 -0
  28. package/dist/cli.js +95 -0
  29. package/dist/deploy-QU7DCKWU.js +11 -0
  30. package/dist/dev-VDSDCBO2.js +43 -0
  31. package/dist/index.d.ts +382 -0
  32. package/dist/index.js +191 -0
  33. package/dist/init-7T5TEPBH.js +14 -0
  34. package/dist/manage-MKH27U3B.js +12 -0
  35. package/dist/openapiCheck-KGBQJTIQ.js +66 -0
  36. package/dist/pages-HIPXRLSY.js +15 -0
  37. package/dist/read-DMSJUEDA.js +160 -0
  38. package/dist/read-ZEVUZNNB.js +146 -0
  39. package/dist/requests-H5MIGBIS.js +13 -0
  40. package/dist/schema-XJUEZSIW.js +13 -0
  41. package/dist/sites-I4WI2MPG.js +14 -0
  42. package/dist/status-7R3NOI5H.js +14 -0
  43. package/dist/suggestions-4WFQ3APX.js +739 -0
  44. package/dist/validate-VXL4PL42.js +30 -0
  45. package/package.json +68 -0
@@ -0,0 +1,568 @@
1
+ // ../config/dist/index.js
2
+ import { z } from "zod";
3
+ import { parse as parseToml } from "smol-toml";
4
+ import { readFile } from "fs/promises";
5
+ var DEFAULT_BACKGROUND_PRESET = "linear-wash";
6
+ var CODE_THEMES = [
7
+ "andromeeda",
8
+ "aurora-x",
9
+ "ayu-dark",
10
+ "ayu-light",
11
+ "ayu-mirage",
12
+ "catppuccin-frappe",
13
+ "catppuccin-latte",
14
+ "catppuccin-macchiato",
15
+ "catppuccin-mocha",
16
+ "dark-plus",
17
+ "dracula",
18
+ "dracula-soft",
19
+ "everforest-dark",
20
+ "everforest-light",
21
+ "github-dark",
22
+ "github-dark-default",
23
+ "github-dark-dimmed",
24
+ "github-dark-high-contrast",
25
+ "github-light",
26
+ "github-light-default",
27
+ "github-light-high-contrast",
28
+ "gruvbox-dark-hard",
29
+ "gruvbox-dark-medium",
30
+ "gruvbox-dark-soft",
31
+ "gruvbox-light-hard",
32
+ "gruvbox-light-medium",
33
+ "gruvbox-light-soft",
34
+ "horizon",
35
+ "horizon-bright",
36
+ "houston",
37
+ "kanagawa-dragon",
38
+ "kanagawa-lotus",
39
+ "kanagawa-wave",
40
+ "laserwave",
41
+ "light-plus",
42
+ "material-theme",
43
+ "material-theme-darker",
44
+ "material-theme-lighter",
45
+ "material-theme-ocean",
46
+ "material-theme-palenight",
47
+ "min-dark",
48
+ "min-light",
49
+ "monokai",
50
+ "night-owl",
51
+ "night-owl-light",
52
+ "nord",
53
+ "one-dark-pro",
54
+ "one-light",
55
+ "plastic",
56
+ "poimandres",
57
+ "red",
58
+ "rose-pine",
59
+ "rose-pine-dawn",
60
+ "rose-pine-moon",
61
+ "slack-dark",
62
+ "slack-ochin",
63
+ "snazzy-light",
64
+ "solarized-dark",
65
+ "solarized-light",
66
+ "synthwave-84",
67
+ "tokyo-night",
68
+ "vesper",
69
+ "vitesse-black",
70
+ "vitesse-dark",
71
+ "vitesse-light"
72
+ ];
73
+ var MAX_ASSISTANT_GREETING_CHARS = 200;
74
+ var MAX_ASSISTANT_SUGGESTED_QUESTIONS = 6;
75
+ var MAX_ASSISTANT_SUGGESTED_QUESTION_CHARS = 160;
76
+ var MAX_ASSISTANT_INSTRUCTIONS_CHARS = 4e3;
77
+ var docsColorsSchema = z.object({
78
+ primary: z.string(),
79
+ light: z.string().optional(),
80
+ dark: z.string().optional()
81
+ }).strict();
82
+ var appearanceSchema = z.object({
83
+ default: z.enum(["light", "dark", "system"]).optional(),
84
+ strict: z.boolean().optional()
85
+ }).strict().superRefine((appearance, ctx) => {
86
+ if (appearance.strict === true && (appearance.default ?? "system") === "system") {
87
+ ctx.addIssue({
88
+ code: z.ZodIssueCode.custom,
89
+ path: ["strict"],
90
+ message: 'strict appearance requires default to be "light" or "dark"'
91
+ });
92
+ }
93
+ });
94
+ var fontFamilySchema = z.string().min(1).regex(/^[A-Za-z0-9 +-]+$/, {
95
+ message: "Font family may only contain letters, numbers, spaces, plus signs, and hyphens"
96
+ });
97
+ var fontSpecSchema = z.union([
98
+ fontFamilySchema,
99
+ z.object({
100
+ family: fontFamilySchema,
101
+ source: z.enum(["google", "system"]).optional(),
102
+ weights: z.array(z.number().int().min(1).max(1e3)).min(1).optional()
103
+ }).strict()
104
+ ]);
105
+ var fontsSchema = z.object({
106
+ body: fontSpecSchema.optional(),
107
+ heading: fontSpecSchema.optional(),
108
+ mono: fontSpecSchema.optional()
109
+ }).strict();
110
+ var stylingSchema = z.object({
111
+ radius: z.enum(["none", "small", "medium", "large"]).optional(),
112
+ accentStrength: z.enum(["subtle", "medium", "strong"]).optional()
113
+ }).strict();
114
+ var nonEmptyThemeValueSchema = z.string().min(1);
115
+ var modePairSchema = z.object({
116
+ light: nonEmptyThemeValueSchema.optional(),
117
+ dark: nonEmptyThemeValueSchema.optional()
118
+ }).strict();
119
+ var themeObjectSchema = z.object({
120
+ base: z.enum(["default", "minimal", "soft", "bold"]).optional(),
121
+ code: z.object({
122
+ light: z.enum(CODE_THEMES).optional(),
123
+ dark: z.enum(CODE_THEMES).optional()
124
+ }).strict().optional(),
125
+ seeds: z.object({
126
+ background: modePairSchema.optional(),
127
+ surface: modePairSchema.optional(),
128
+ text: modePairSchema.optional(),
129
+ accent: modePairSchema.optional()
130
+ }).strict().optional(),
131
+ radius: z.enum(["none", "small", "medium", "large"]).optional(),
132
+ shadow: z.enum(["none", "soft", "soft-large", "strong"]).optional(),
133
+ accentStrength: z.enum(["subtle", "medium", "strong"]).optional(),
134
+ tokens: z.object({
135
+ light: z.record(z.string(), nonEmptyThemeValueSchema).optional(),
136
+ dark: z.record(z.string(), nonEmptyThemeValueSchema).optional()
137
+ }).strict().optional()
138
+ }).strict();
139
+ var navigationLinkSchema = z.object({
140
+ label: z.string(),
141
+ href: z.string(),
142
+ external: z.literal(true).optional()
143
+ }).strict();
144
+ var navigationPageSchema = z.object({
145
+ page: z.string().min(1, "Page paths must not be empty"),
146
+ label: z.string().min(1, "Page labels must not be empty")
147
+ }).strict();
148
+ var navigationEntrySchema = z.lazy(
149
+ () => z.union([
150
+ z.string(),
151
+ navigationPageSchema,
152
+ navigationLinkSchema,
153
+ navigationGroupSchema,
154
+ navigationSectionSchema
155
+ ])
156
+ );
157
+ var navigationGroupSchema = z.lazy(
158
+ () => z.object({
159
+ group: z.string(),
160
+ pages: z.array(navigationEntrySchema),
161
+ openapi: z.string().optional(),
162
+ collapsed: z.boolean().optional()
163
+ }).strict()
164
+ );
165
+ var navigationSectionSchema = z.lazy(
166
+ () => z.object({
167
+ section: z.string(),
168
+ pages: z.array(navigationEntrySchema)
169
+ }).strict()
170
+ );
171
+ var navigationContainerSchema = z.union([
172
+ navigationGroupSchema,
173
+ navigationSectionSchema
174
+ ]);
175
+ var navigationVersionSchema = z.object({
176
+ version: z.string().min(1).regex(/^[a-zA-Z0-9._-]+$/, {
177
+ message: "Version must be URL-safe: letters, numbers, dot, underscore, and hyphen only"
178
+ }),
179
+ default: z.boolean().optional(),
180
+ groups: z.array(navigationContainerSchema)
181
+ }).strict();
182
+ var navigationTabSchema = z.object({
183
+ tab: z.string().min(1, "Tab labels must not be empty"),
184
+ groups: z.array(navigationContainerSchema)
185
+ }).strict();
186
+ var navigationSchema = z.union([
187
+ z.object({
188
+ groups: z.array(navigationContainerSchema)
189
+ }).strict(),
190
+ z.object({
191
+ tabs: z.array(navigationTabSchema).min(1, "At least one tab is required")
192
+ }).strict().superRefine((navigation, ctx) => {
193
+ const seen = /* @__PURE__ */ new Set();
194
+ for (let index = 0; index < navigation.tabs.length; index++) {
195
+ const entry = navigation.tabs[index];
196
+ if (seen.has(entry.tab)) {
197
+ ctx.addIssue({
198
+ code: z.ZodIssueCode.custom,
199
+ path: ["tabs", index, "tab"],
200
+ message: `Duplicate tab "${entry.tab}"`
201
+ });
202
+ }
203
+ seen.add(entry.tab);
204
+ if (entry.groups.every(
205
+ (group) => group.pages.length === 0 && (!isNavigationGroup(group) || !group.openapi)
206
+ )) {
207
+ ctx.addIssue({
208
+ code: z.ZodIssueCode.custom,
209
+ path: ["tabs", index, "groups"],
210
+ message: `Tab "${entry.tab}" has no pages; every tab needs a landing page`
211
+ });
212
+ }
213
+ }
214
+ }),
215
+ z.object({
216
+ versions: z.array(navigationVersionSchema).min(1, "At least one version is required")
217
+ }).strict().superRefine((navigation, ctx) => {
218
+ const seen = /* @__PURE__ */ new Set();
219
+ let defaults = 0;
220
+ for (let index = 0; index < navigation.versions.length; index++) {
221
+ const entry = navigation.versions[index];
222
+ if (seen.has(entry.version)) {
223
+ ctx.addIssue({
224
+ code: z.ZodIssueCode.custom,
225
+ path: ["versions", index, "version"],
226
+ message: `Duplicate version "${entry.version}"`
227
+ });
228
+ }
229
+ seen.add(entry.version);
230
+ if (entry.default === true) defaults++;
231
+ }
232
+ if (defaults > 1) {
233
+ ctx.addIssue({
234
+ code: z.ZodIssueCode.custom,
235
+ path: ["versions"],
236
+ message: "At most one version may set default: true"
237
+ });
238
+ }
239
+ })
240
+ ]).superRefine((navigation, ctx) => {
241
+ const value = navigation;
242
+ if ("groups" in value && "versions" in value) {
243
+ ctx.addIssue({
244
+ code: z.ZodIssueCode.custom,
245
+ message: "Navigation must define exactly one of groups or versions"
246
+ });
247
+ }
248
+ });
249
+ var logoSchema = z.union([
250
+ z.string(),
251
+ z.object({
252
+ light: z.string().optional(),
253
+ dark: z.string().optional()
254
+ }).strict()
255
+ ]);
256
+ var docsAiConfigSchema = z.object({
257
+ assistant: z.boolean().optional(),
258
+ graphEnrichment: z.boolean().optional(),
259
+ home: z.boolean().default(false),
260
+ greeting: z.string().trim().min(1).max(MAX_ASSISTANT_GREETING_CHARS).optional(),
261
+ suggestedQuestions: z.array(z.string().refine((question) => !/[\r\n]/u.test(question), { message: "a suggested question is one line" }).trim().min(1).max(MAX_ASSISTANT_SUGGESTED_QUESTION_CHARS)).max(MAX_ASSISTANT_SUGGESTED_QUESTIONS).optional(),
262
+ instructions: z.string().trim().min(1).max(MAX_ASSISTANT_INSTRUCTIONS_CHARS).optional()
263
+ }).strict().superRefine((ai, ctx) => {
264
+ if (ai.home === true && ai.assistant === false) {
265
+ ctx.addIssue({
266
+ code: z.ZodIssueCode.custom,
267
+ path: ["home"],
268
+ message: "ai.home cannot be true when ai.assistant is false"
269
+ });
270
+ }
271
+ });
272
+ var docsAgentsConfigSchema = z.object({
273
+ contentSignals: z.object({
274
+ search: z.boolean().optional(),
275
+ aiInput: z.boolean().optional(),
276
+ aiTrain: z.boolean().optional()
277
+ }).strict().optional(),
278
+ openapi: z.boolean().optional(),
279
+ webmcp: z.boolean().optional(),
280
+ a2a: z.boolean().optional()
281
+ }).strict();
282
+ var docsAnalyticsConfigSchema = z.object({
283
+ posthog: z.object({
284
+ api_host: z.string().url().optional(),
285
+ key: z.string()
286
+ }).strict().optional()
287
+ }).strict();
288
+ var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
289
+ var httpsUrlSchema = z.string().url().refine((value) => {
290
+ try {
291
+ const url = new URL(value);
292
+ return url.protocol === "https:" || url.protocol === "http:" && LOCAL_HOSTNAMES.has(url.hostname);
293
+ } catch {
294
+ return false;
295
+ }
296
+ }, "Must be an https URL (plain http is allowed for localhost only)");
297
+ var docsAuthOidcConfigSchema = z.object({
298
+ issuer: httpsUrlSchema,
299
+ client_id: z.string(),
300
+ groups_claim: z.string().optional()
301
+ }).strict();
302
+ var docsAuthConfigSchema = z.object({
303
+ oidc: docsAuthOidcConfigSchema.optional()
304
+ }).strict();
305
+ var socialLinkSchema = z.object({
306
+ icon: z.string(),
307
+ label: z.string(),
308
+ href: z.string()
309
+ }).strict();
310
+ var navigationHrefSchema = z.string().refine((value) => {
311
+ if (value.startsWith("/")) return !value.startsWith("//");
312
+ try {
313
+ const url = new URL(value);
314
+ return url.protocol === "http:" || url.protocol === "https:";
315
+ } catch {
316
+ return false;
317
+ }
318
+ }, "Must start with / or be an absolute http(s) URL");
319
+ var docsHeaderConfigSchema = z.object({
320
+ website: httpsUrlSchema.optional(),
321
+ showName: z.boolean().optional(),
322
+ cta: z.object({
323
+ label: z.string().trim().min(1),
324
+ href: navigationHrefSchema
325
+ }).strict().optional()
326
+ }).strict();
327
+ var editLinkSchema = z.object({
328
+ baseUrl: z.string()
329
+ }).strict();
330
+ var siteUrlSchema = z.string().url().refine((value) => {
331
+ try {
332
+ return new URL(value).protocol === "https:";
333
+ } catch {
334
+ return false;
335
+ }
336
+ }, "Must be an https URL");
337
+ var redirectSchema = z.object({
338
+ source: z.string().refine((value) => value.startsWith("/"), "Must start with /").refine((value) => !value.startsWith("//"), "Must not start with //"),
339
+ destination: z.string().refine((value) => {
340
+ if (value.startsWith("/")) return !value.startsWith("//");
341
+ try {
342
+ const url = new URL(value);
343
+ return url.protocol === "http:" || url.protocol === "https:";
344
+ } catch {
345
+ return false;
346
+ }
347
+ }, "Must start with / or be an absolute http(s) URL"),
348
+ permanent: z.boolean().optional()
349
+ }).strict();
350
+ var docsConfigSchema = z.object({
351
+ name: z.string(),
352
+ description: z.string().optional(),
353
+ type: z.enum(["docs", "hub", "portal"]).default("docs"),
354
+ theme: z.union([
355
+ themeObjectSchema,
356
+ z.enum(["default", "minimal", "soft", "bold"])
357
+ ]).optional(),
358
+ background: z.enum(["none", "linear-wash", "radial-grid", "soft-grain"]).default(DEFAULT_BACKGROUND_PRESET),
359
+ appearance: appearanceSchema.optional(),
360
+ fonts: fontsSchema.optional(),
361
+ styling: stylingSchema.optional(),
362
+ siteUrl: siteUrlSchema.optional(),
363
+ colors: docsColorsSchema,
364
+ logo: logoSchema.optional(),
365
+ favicon: z.string().optional(),
366
+ social: z.array(socialLinkSchema).optional(),
367
+ header: docsHeaderConfigSchema.optional(),
368
+ editLink: editLinkSchema.optional(),
369
+ lastUpdated: z.boolean().default(false),
370
+ contextMenu: z.union([
371
+ z.boolean(),
372
+ z.object({ thirdParty: z.boolean().optional() }).strict()
373
+ ]).default(true),
374
+ navigation: navigationSchema,
375
+ footer: z.object({
376
+ text: z.string().optional()
377
+ }).strict().optional(),
378
+ instructions: z.string().optional(),
379
+ ai: docsAiConfigSchema.optional(),
380
+ analytics: docsAnalyticsConfigSchema.optional(),
381
+ auth: docsAuthConfigSchema.optional(),
382
+ agents: docsAgentsConfigSchema.optional(),
383
+ redirects: z.array(redirectSchema).optional()
384
+ }).strict().superRefine((config, ctx) => {
385
+ const seen = /* @__PURE__ */ new Set();
386
+ for (let index = 0; index < (config.redirects ?? []).length; index++) {
387
+ const redirect = config.redirects?.[index];
388
+ if (!redirect) continue;
389
+ if (seen.has(redirect.source)) {
390
+ ctx.addIssue({
391
+ code: z.ZodIssueCode.custom,
392
+ path: ["redirects", index, "source"],
393
+ message: `Duplicate redirect source "${redirect.source}"`
394
+ });
395
+ }
396
+ seen.add(redirect.source);
397
+ }
398
+ });
399
+ var modelsConfigSchema = z.object({
400
+ models: z.object({
401
+ synthesis: z.string(),
402
+ suggestion: z.string().optional()
403
+ }).strict(),
404
+ endpoint: z.object({
405
+ api_base: z.string().optional(),
406
+ api_key_env: z.string().optional()
407
+ }).strict().optional(),
408
+ behavior: z.object({
409
+ cache: z.boolean().optional(),
410
+ max_iterations: z.number().int().min(1).max(8).optional()
411
+ }).strict().optional()
412
+ }).strict();
413
+ function formatPath(path) {
414
+ let out = "";
415
+ for (const segment of path) {
416
+ if (typeof segment === "number") {
417
+ out += `[${segment}]`;
418
+ } else {
419
+ out += out === "" ? String(segment) : `.${String(segment)}`;
420
+ }
421
+ }
422
+ return out;
423
+ }
424
+ function formatZodError(error, label) {
425
+ const lines = renderIssues(error.issues);
426
+ return `Invalid ${label}:
427
+ ${lines.map((l) => ` - ${l}`).join("\n")}`;
428
+ }
429
+ function renderIssues(issues) {
430
+ return issues.flatMap((issue) => {
431
+ if (issue.code === "unrecognized_keys") {
432
+ return issue.keys.map((key) => {
433
+ const path2 = formatPath([...issue.path, key]);
434
+ const where2 = path2 === "" ? "(root)" : path2;
435
+ return `${where2}: Unrecognized key`;
436
+ });
437
+ }
438
+ if (issue.code === "invalid_union") {
439
+ const branches = issue.errors.map(
440
+ (branch) => renderIssues(
441
+ branch.map((branchIssue) => ({
442
+ ...branchIssue,
443
+ path: [...issue.path, ...branchIssue.path]
444
+ }))
445
+ )
446
+ );
447
+ const best = branches.filter((branch) => branch.length > 0).sort((a, b) => a.length - b.length)[0];
448
+ if (best) return best;
449
+ }
450
+ const path = formatPath(issue.path);
451
+ const where = path === "" ? "(root)" : path;
452
+ return `${where}: ${issue.message}`;
453
+ });
454
+ }
455
+ function parseDocsConfig(data) {
456
+ const result = docsConfigSchema.safeParse(data);
457
+ if (!result.success) {
458
+ throw new Error(formatZodError(result.error, "docs.json"));
459
+ }
460
+ return result.data;
461
+ }
462
+ function topLevelNavigationGroups(config) {
463
+ if ("groups" in config.navigation) return config.navigation.groups;
464
+ if ("tabs" in config.navigation) return config.navigation.tabs.flatMap((tab) => tab.groups);
465
+ return config.navigation.versions.flatMap((version) => version.groups);
466
+ }
467
+ function isNavigationGroup(entry) {
468
+ return typeof entry === "object" && entry !== null && "group" in entry;
469
+ }
470
+ function isNavigationSection(entry) {
471
+ return typeof entry === "object" && entry !== null && "section" in entry;
472
+ }
473
+ function isNavigationContainer(entry) {
474
+ return isNavigationGroup(entry) || isNavigationSection(entry);
475
+ }
476
+ function isNavigationPage(entry) {
477
+ return typeof entry === "object" && entry !== null && "page" in entry;
478
+ }
479
+ function authoredNavigationGroups(groups) {
480
+ return groups.flatMap((group) => [
481
+ {
482
+ group: isNavigationGroup(group) ? group.group : group.section,
483
+ pages: group.pages.flatMap(
484
+ (entry) => typeof entry === "string" ? [entry] : isNavigationPage(entry) ? [entry.page] : []
485
+ ),
486
+ ...isNavigationGroup(group) && group.openapi ? { openapi: group.openapi } : {},
487
+ ...isNavigationGroup(group) && group.collapsed !== void 0 ? { collapsed: group.collapsed } : {}
488
+ },
489
+ ...authoredNavigationGroups(group.pages.filter(isNavigationContainer))
490
+ ]);
491
+ }
492
+ function navigationGroups(config) {
493
+ return authoredNavigationGroups(topLevelNavigationGroups(config));
494
+ }
495
+ async function loadDocsConfig(filePath) {
496
+ let raw;
497
+ try {
498
+ raw = await readFile(filePath, "utf8");
499
+ } catch (cause) {
500
+ throw new Error(`Could not read docs.json at ${filePath}: ${errorMessage(cause)}`);
501
+ }
502
+ let parsed;
503
+ try {
504
+ parsed = JSON.parse(raw);
505
+ } catch (cause) {
506
+ throw new Error(`docs.json at ${filePath} is not valid JSON: ${errorMessage(cause)}`);
507
+ }
508
+ return parseDocsConfig(parsed);
509
+ }
510
+ async function loadModelsConfig(filePath) {
511
+ let raw;
512
+ try {
513
+ raw = await readFile(filePath, "utf8");
514
+ } catch (cause) {
515
+ throw new Error(`Could not read models.toml at ${filePath}: ${errorMessage(cause)}`);
516
+ }
517
+ return parseModelsConfigValue(raw, `models.toml at ${filePath}`);
518
+ }
519
+ function parseModelsConfig(toml, platformApiBase) {
520
+ return parseModelsConfigValue(toml, "models.toml", platformApiBase);
521
+ }
522
+ function parseModelsConfigValue(toml, label, platformApiBase) {
523
+ let parsed;
524
+ try {
525
+ parsed = parseToml(toml);
526
+ } catch (cause) {
527
+ throw new Error(`${label} is not valid TOML: ${errorMessage(cause)}`);
528
+ }
529
+ const result = modelsConfigSchema.safeParse(parsed);
530
+ if (!result.success) {
531
+ throw new Error(formatZodError(result.error, "models.toml"));
532
+ }
533
+ return applyModelsDefaults(result.data, platformApiBase);
534
+ }
535
+ function applyModelsDefaults(config, platformApiBase) {
536
+ const endpoint = config.endpoint ?? {};
537
+ const behavior = config.behavior ?? {};
538
+ return {
539
+ ...config,
540
+ models: {
541
+ synthesis: config.models.synthesis,
542
+ suggestion: config.models.suggestion ?? config.models.synthesis
543
+ },
544
+ endpoint: {
545
+ api_base: endpoint.api_base ?? platformBase(platformApiBase),
546
+ api_key_env: endpoint.api_key_env ?? "OPENROUTER_API_KEY"
547
+ },
548
+ behavior: {
549
+ cache: behavior.cache ?? true,
550
+ max_iterations: behavior.max_iterations ?? 4
551
+ }
552
+ };
553
+ }
554
+ function platformBase(platformApiBase) {
555
+ const trimmed = platformApiBase?.trim() ?? "";
556
+ return trimmed === "" ? "https://openrouter.ai/api/v1" : trimmed;
557
+ }
558
+ function errorMessage(cause) {
559
+ return cause instanceof Error ? cause.message : String(cause);
560
+ }
561
+
562
+ export {
563
+ parseDocsConfig,
564
+ navigationGroups,
565
+ loadDocsConfig,
566
+ loadModelsConfig,
567
+ parseModelsConfig
568
+ };