@farming-labs/docs 0.1.50 → 0.1.52

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.
@@ -1,10 +1,10 @@
1
- import "./reading-time-DCZXC6-3.mjs";
2
- import { C as renderDocsMarkdownDocument, g as findDocsMarkdownPage } from "./agent-CbAtuZAc.mjs";
1
+ import { C as renderDocsMarkdownDocument, g as findDocsMarkdownPage } from "./agent-Xh0UaY5I.mjs";
2
+ import { d as hashGeneratedAgentContent, m as serializeGeneratedAgentDocument, p as parseGeneratedAgentDocument, u as GENERATED_AGENT_PROVENANCE_VERSION } from "./search-Cu_pxL8o.mjs";
3
3
  import "./index.mjs";
4
- import "./api-reference-y7cqtq4w.mjs";
4
+ import "./api-reference-GDAEzQn1.mjs";
5
5
  import { createFilesystemDocsMcpSource } from "./mcp.mjs";
6
6
  import "./server.mjs";
7
- import { a as loadProjectEnv, c as readNavTitle, d as readTopLevelStringProperty, f as resolveDocsConfigPath, i as loadDocsConfigModule, l as readNumberProperty, o as readBooleanProperty, p as resolveDocsContentDir, s as readEnvReferenceProperty, t as extractNestedObjectLiteral, u as readStringProperty } from "./config-C7sUsMkm.mjs";
7
+ import { a as loadProjectEnv, c as readNavTitle, d as readTopLevelStringProperty, f as resolveDocsConfigPath, i as loadDocsConfigModule, l as readNumberProperty, o as readBooleanProperty, p as resolveDocsContentDir, s as readEnvReferenceProperty, t as extractNestedObjectLiteral, u as readStringProperty } from "./config-Si-yUfM_.mjs";
8
8
  import matter from "gray-matter";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
10
10
  import path from "node:path";
@@ -50,6 +50,14 @@ function parseAgentCompactArgs(argv) {
50
50
  parsed.dryRun = true;
51
51
  continue;
52
52
  }
53
+ if (arg === "--stale") {
54
+ parsed.stale = true;
55
+ continue;
56
+ }
57
+ if (arg === "--include-missing") {
58
+ parsed.includeMissing = true;
59
+ continue;
60
+ }
53
61
  if (arg === "--protect-json") {
54
62
  const nextValue = argv[index + 1];
55
63
  if (nextValue && !nextValue.startsWith("--")) {
@@ -221,6 +229,95 @@ function readPageTokenBudget(pagePath) {
221
229
  const { data } = matter(readFileSync(pagePath, "utf-8"));
222
230
  return normalizeTokenBudget(data.agent?.tokenBudget);
223
231
  }
232
+ function buildCompactionSettingsHash(options) {
233
+ return hashGeneratedAgentContent(JSON.stringify({
234
+ model: options.model ?? DEFAULT_TTC_MODEL,
235
+ aggressiveness: options.aggressiveness ?? DEFAULT_TTC_AGGRESSIVENESS,
236
+ maxOutputTokens: options.maxOutputTokens ?? null,
237
+ minOutputTokens: options.minOutputTokens ?? null,
238
+ protectJson: options.protectJson ?? null
239
+ }));
240
+ }
241
+ function buildPageOptions(defaults, pagePath) {
242
+ const tokenBudget = readPageTokenBudget(pagePath);
243
+ const pageOptions = mergeAgentCompactOptions(defaults, { maxOutputTokens: tokenBudget });
244
+ if (pageOptions.minOutputTokens !== void 0 && pageOptions.maxOutputTokens !== void 0 && pageOptions.minOutputTokens > pageOptions.maxOutputTokens) pageOptions.minOutputTokens = pageOptions.maxOutputTokens;
245
+ return {
246
+ pageOptions,
247
+ tokenBudget
248
+ };
249
+ }
250
+ function buildResolvedPageSourceDocument(page) {
251
+ return renderDocsMarkdownDocument({
252
+ ...page,
253
+ agentRawContent: void 0
254
+ });
255
+ }
256
+ function buildAgentSourceDocument(page) {
257
+ if (typeof page.agentRawContent === "string") return page.agentRawContent;
258
+ return renderDocsMarkdownDocument(page);
259
+ }
260
+ function readCurrentAgentDocument(target) {
261
+ if (!target.hasAgentFile || !existsSync(target.agentPath)) return void 0;
262
+ return parseGeneratedAgentDocument(readFileSync(target.agentPath, "utf-8"));
263
+ }
264
+ function resolveSourceKindForCompaction(target, currentDocument) {
265
+ if (!target.hasAgentFile) return "resolved-page";
266
+ if (currentDocument?.provenance?.sourceKind === "resolved-page") return "resolved-page";
267
+ return "agent-md";
268
+ }
269
+ function buildSourceDocumentForCompaction(page, sourceKind) {
270
+ return sourceKind === "resolved-page" ? buildResolvedPageSourceDocument(page) : buildAgentSourceDocument(page);
271
+ }
272
+ function buildGeneratedAgentProvenance(sourceKind, sourceDocument, output, pageOptions) {
273
+ return {
274
+ version: GENERATED_AGENT_PROVENANCE_VERSION,
275
+ sourceKind,
276
+ sourceHash: hashGeneratedAgentContent(sourceDocument),
277
+ settingsHash: buildCompactionSettingsHash(pageOptions),
278
+ outputHash: hashGeneratedAgentContent(output),
279
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
280
+ };
281
+ }
282
+ function inspectAgentCompactionState(page, target, defaults) {
283
+ const { pageOptions, tokenBudget } = buildPageOptions(defaults, target.pagePath);
284
+ const currentDocument = readCurrentAgentDocument(target);
285
+ if (!currentDocument) return {
286
+ status: "missing",
287
+ sourceKind: "resolved-page",
288
+ pageOptions,
289
+ sourceDocument: buildResolvedPageSourceDocument(page),
290
+ tokenBudget
291
+ };
292
+ const sourceKind = resolveSourceKindForCompaction(target, currentDocument);
293
+ const sourceDocument = buildSourceDocumentForCompaction(page, sourceKind);
294
+ if (!currentDocument.provenance) return {
295
+ status: "unknown",
296
+ sourceKind,
297
+ pageOptions,
298
+ sourceDocument,
299
+ tokenBudget
300
+ };
301
+ const outputModified = hashGeneratedAgentContent(currentDocument.content) !== currentDocument.provenance.outputHash;
302
+ if (currentDocument.provenance.sourceKind === "agent-md") return {
303
+ status: outputModified ? "modified" : "unknown",
304
+ sourceKind,
305
+ pageOptions,
306
+ sourceDocument,
307
+ provenance: currentDocument.provenance,
308
+ tokenBudget
309
+ };
310
+ const sourceChanged = hashGeneratedAgentContent(sourceDocument) !== currentDocument.provenance.sourceHash;
311
+ const settingsChanged = buildCompactionSettingsHash(pageOptions) !== currentDocument.provenance.settingsHash;
312
+ return {
313
+ status: outputModified && (sourceChanged || settingsChanged) ? "stale-modified" : outputModified ? "modified" : sourceChanged || settingsChanged ? "stale" : "fresh",
314
+ sourceKind,
315
+ pageOptions,
316
+ sourceDocument,
317
+ provenance: currentDocument.provenance,
318
+ tokenBudget
319
+ };
320
+ }
224
321
  function protectForCompression(input) {
225
322
  const segments = [];
226
323
  const stash = (value) => {
@@ -311,8 +408,9 @@ async function compactAgentDocs(options = {}) {
311
408
  const contentDir = typeof loadedConfigModule?.config.contentDir === "string" ? loadedConfigModule.config.contentDir : resolveDocsContentDir(rootDir, configContent, entry);
312
409
  const siteTitle = typeof loadedConfigModule?.config.nav?.title === "string" ? loadedConfigModule.config.nav.title : readNavTitle(configContent) ?? "Documentation";
313
410
  if (resolvedOptions.all && resolvedOptions.pages && resolvedOptions.pages.length > 0) throw new Error("Use either --all or specific page arguments, not both.");
411
+ if (resolvedOptions.includeMissing && !resolvedOptions.stale) throw new Error("Use --include-missing together with --stale.");
314
412
  const requestedPages = resolvedOptions.pages?.filter((value) => value.trim().length > 0) ?? [];
315
- if (!resolvedOptions.all && requestedPages.length === 0) throw new Error("Pass --all or at least one docs page slug/path to compact.");
413
+ if (!resolvedOptions.all && requestedPages.length === 0 && !resolvedOptions.stale) throw new Error("Pass --all, --stale, or at least one docs page slug/path to compact.");
316
414
  const pages = await createFilesystemDocsMcpSource({
317
415
  rootDir,
318
416
  entry,
@@ -320,25 +418,58 @@ async function compactAgentDocs(options = {}) {
320
418
  siteTitle
321
419
  }).getPages();
322
420
  if (pages.length === 0) throw new Error(`No docs content was found under ${contentDir}.`);
323
- const selectedPages = resolveSelectedPages(pages, scanDocsPageTargets(rootDir, contentDir, entry), entry, requestedPages, resolvedOptions.all === true);
421
+ const selectedPages = resolveSelectedPages(pages, scanDocsPageTargets(rootDir, contentDir, entry), entry, requestedPages, resolvedOptions.all === true || resolvedOptions.stale === true && requestedPages.length === 0);
324
422
  if (selectedPages.length === 0) throw new Error("No compactable docs pages matched the request.");
325
423
  let created = 0;
326
424
  let overwritten = 0;
425
+ let processed = 0;
426
+ let skippedFresh = 0;
427
+ let skippedModified = 0;
428
+ let skippedUnknown = 0;
429
+ let skippedMissing = 0;
430
+ const requestedExplicitPages = requestedPages.length > 0;
327
431
  for (const { page, target } of selectedPages) {
328
- const sourceDocument = renderDocsMarkdownDocument(page);
329
- const pageOptions = mergeAgentCompactOptions(resolvedOptions, { maxOutputTokens: readPageTokenBudget(target.pagePath) });
330
- if (pageOptions.minOutputTokens !== void 0 && pageOptions.maxOutputTokens !== void 0 && pageOptions.minOutputTokens > pageOptions.maxOutputTokens) pageOptions.minOutputTokens = pageOptions.maxOutputTokens;
331
- const compressed = await compressDocument(sourceDocument, pageOptions);
432
+ const state = inspectAgentCompactionState(page, target, resolvedOptions);
433
+ if (resolvedOptions.stale) {
434
+ if (state.status === "fresh") {
435
+ skippedFresh += 1;
436
+ continue;
437
+ }
438
+ if (state.status === "modified" || state.status === "stale-modified") {
439
+ skippedModified += 1;
440
+ continue;
441
+ }
442
+ if (state.status === "unknown") {
443
+ skippedUnknown += 1;
444
+ continue;
445
+ }
446
+ if (state.status === "missing") {
447
+ if (!(resolvedOptions.includeMissing === true && (requestedExplicitPages || state.tokenBudget !== void 0))) {
448
+ skippedMissing += 1;
449
+ continue;
450
+ }
451
+ }
452
+ }
453
+ const compressed = await compressDocument(state.sourceDocument, state.pageOptions);
332
454
  const nextContent = compressed.output.trimEnd();
455
+ const generatedDocument = serializeGeneratedAgentDocument(nextContent, buildGeneratedAgentProvenance(state.sourceKind, state.sourceDocument, nextContent, state.pageOptions));
333
456
  console.log(pc.dim(`Compacting ${page.url} (${compressed.original_input_tokens ?? "?"} -> ${compressed.output_tokens ?? "?"} tokens)...`));
334
457
  if (resolvedOptions.dryRun) continue;
335
458
  mkdirSync(target.pageDir, { recursive: true });
336
- writeFileSync(target.agentPath, `${nextContent}\n`, "utf-8");
459
+ writeFileSync(target.agentPath, generatedDocument, "utf-8");
337
460
  if (target.hasAgentFile) overwritten += 1;
338
461
  else created += 1;
462
+ processed += 1;
463
+ }
464
+ if (resolvedOptions.dryRun) processed = selectedPages.length - skippedFresh - skippedModified - skippedUnknown - skippedMissing;
465
+ if (resolvedOptions.stale && processed === 0) {
466
+ console.log(pc.green("No stale generated agent.md files needed updates."));
467
+ if (skippedFresh + skippedModified + skippedUnknown + skippedMissing > 0) console.log(pc.dim(`Skipped ${skippedFresh} fresh, ${skippedModified} modified, ${skippedUnknown} unknown, and ${skippedMissing} missing page${skippedFresh + skippedModified + skippedUnknown + skippedMissing === 1 ? "" : "s"}.`));
468
+ return;
339
469
  }
340
470
  const summaryPrefix = resolvedOptions.dryRun ? "Dry run complete" : "Compaction complete";
341
- console.log(pc.green(`${summaryPrefix}: ${selectedPages.length} page${selectedPages.length === 1 ? "" : "s"} processed` + (resolvedOptions.dryRun ? "." : ` (${created} created, ${overwritten} overwritten).`)));
471
+ console.log(pc.green(`${summaryPrefix}: ${processed} page${processed === 1 ? "" : "s"} processed` + (resolvedOptions.dryRun ? "." : ` (${created} created, ${overwritten} overwritten).`)));
472
+ if (resolvedOptions.stale) console.log(pc.dim(`Skipped ${skippedFresh} fresh, ${skippedModified} modified, ${skippedUnknown} unknown, and ${skippedMissing} missing page${skippedFresh + skippedModified + skippedUnknown + skippedMissing === 1 ? "" : "s"}.`));
342
473
  }
343
474
  function printAgentCompactHelp() {
344
475
  console.log(`
@@ -352,6 +483,8 @@ ${pc.dim("Examples:")}
352
483
  ${pc.cyan("npx @farming-labs/docs@latest agent compact /docs/installation")}
353
484
  ${pc.cyan("npx @farming-labs/docs@latest agent compact --page installation --page configuration")}
354
485
  ${pc.cyan("npx @farming-labs/docs@latest agent compact --all")}
486
+ ${pc.cyan("npx @farming-labs/docs@latest agent compact --stale")}
487
+ ${pc.cyan("npx @farming-labs/docs@latest agent compact --stale --include-missing")}
355
488
 
356
489
  ${pc.dim("Per-page override:")}
357
490
  Add ${pc.cyan("agent.tokenBudget")} to a page frontmatter block to override the compact output target for that page.
@@ -359,6 +492,8 @@ ${pc.dim("Per-page override:")}
359
492
  ${pc.dim("Options:")}
360
493
  ${pc.cyan("--all")} Compact every folder-based docs page under the configured contentDir
361
494
  ${pc.cyan("--page <slug|path>")} Add a page explicitly (repeatable); positional page args work too
495
+ ${pc.cyan("--stale")} Re-compact only stale generated agent.md files
496
+ ${pc.cyan("--include-missing")} With ${pc.cyan("--stale")}, also create missing agent.md files for explicit pages or pages that define ${pc.cyan("agent.tokenBudget")}
362
497
  ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
363
498
  ${pc.cyan("--api-key <key>")} Token Company API key (or set ${pc.dim("TOKEN_COMPANY_API_KEY")})
364
499
  ${pc.cyan("--api-key-env <name>")} Custom env var name for the Token Company API key
@@ -373,4 +508,4 @@ ${pc.dim("Options:")}
373
508
  }
374
509
 
375
510
  //#endregion
376
- export { compactAgentDocs, parseAgentCompactArgs, printAgentCompactHelp };
511
+ export { compactAgentDocs, inspectAgentCompactionState, parseAgentCompactArgs, printAgentCompactHelp, scanDocsPageTargets };
@@ -1,5 +1,314 @@
1
- import { u as renderDocsRelatedMarkdownLines } from "./search-8oEskRtz.mjs";
1
+ import { _ as renderDocsRelatedMarkdownLines } from "./search-Cu_pxL8o.mjs";
2
+ import matter from "gray-matter";
2
3
 
4
+ //#region src/define-docs.ts
5
+ /**
6
+ * Define docs configuration. Validates and returns the config.
7
+ */
8
+ function defineDocs(config) {
9
+ return {
10
+ entry: config.entry ?? "docs",
11
+ contentDir: config.contentDir,
12
+ i18n: config.i18n,
13
+ theme: config.theme,
14
+ nav: config.nav,
15
+ github: config.github,
16
+ themeToggle: config.themeToggle,
17
+ breadcrumb: config.breadcrumb,
18
+ sidebar: config.sidebar,
19
+ components: config.components,
20
+ onCopyClick: config.onCopyClick,
21
+ feedback: config.feedback,
22
+ search: config.search,
23
+ mcp: config.mcp,
24
+ icons: config.icons,
25
+ pageActions: config.pageActions,
26
+ lastUpdated: config.lastUpdated,
27
+ readingTime: config.readingTime,
28
+ llmsTxt: config.llmsTxt,
29
+ ai: config.ai,
30
+ ordering: config.ordering,
31
+ metadata: config.metadata,
32
+ og: config.og,
33
+ changelog: config.changelog,
34
+ apiReference: config.apiReference,
35
+ agent: config.agent
36
+ };
37
+ }
38
+
39
+ //#endregion
40
+ //#region src/changelog.ts
41
+ function normalizePathSegment(value, fallback) {
42
+ return (value ?? fallback).trim().replace(/^\/+|\/+$/g, "") || fallback;
43
+ }
44
+ function normalizeContentDir(value) {
45
+ const trimmed = value?.trim();
46
+ if (!trimmed) return "changelog";
47
+ return trimmed.replace(/\/+$/, "") || "changelog";
48
+ }
49
+ function resolveChangelogConfig(value) {
50
+ if (value === false || value === void 0) return {
51
+ enabled: false,
52
+ path: "changelog",
53
+ contentDir: "changelog",
54
+ title: "Changelog",
55
+ description: void 0,
56
+ search: true
57
+ };
58
+ if (value === true) return {
59
+ enabled: true,
60
+ path: "changelog",
61
+ contentDir: "changelog",
62
+ title: "Changelog",
63
+ description: void 0,
64
+ search: true
65
+ };
66
+ return {
67
+ enabled: value.enabled !== false,
68
+ path: normalizePathSegment(value.path, "changelog"),
69
+ contentDir: normalizeContentDir(value.contentDir),
70
+ title: value.title?.trim() || "Changelog",
71
+ description: value.description?.trim() || void 0,
72
+ search: value.search !== false,
73
+ actionsComponent: value.actionsComponent
74
+ };
75
+ }
76
+
77
+ //#endregion
78
+ //#region src/utils.ts
79
+ /**
80
+ * Deep merge utility for theme overrides.
81
+ * Merges objects recursively; later values override earlier ones.
82
+ */
83
+ function deepMerge(target, ...sources) {
84
+ if (!sources.length) return target;
85
+ const source = sources.shift();
86
+ if (!source) return target;
87
+ const result = { ...target };
88
+ for (const key of Object.keys(source)) {
89
+ const sourceVal = source[key];
90
+ const targetVal = result[key];
91
+ if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) result[key] = deepMerge(targetVal, sourceVal);
92
+ else if (sourceVal !== void 0) result[key] = sourceVal;
93
+ }
94
+ if (sources.length) return deepMerge(result, ...sources);
95
+ return result;
96
+ }
97
+
98
+ //#endregion
99
+ //#region src/create-theme.ts
100
+ /**
101
+ * Create a theme preset factory.
102
+ *
103
+ * Returns a function that accepts optional overrides and deep-merges them
104
+ * with the base theme defaults. This is the same pattern used by the
105
+ * built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
106
+ *
107
+ * @param baseTheme - The default theme configuration
108
+ * @returns A factory function `(overrides?) => DocsTheme`
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { createTheme } from "@farming-labs/docs";
113
+ *
114
+ * export const myTheme = createTheme({
115
+ * name: "my-theme",
116
+ * ui: {
117
+ * colors: { primary: "#6366f1" },
118
+ * layout: { contentWidth: 800 },
119
+ * },
120
+ * });
121
+ * ```
122
+ */
123
+ function createTheme(baseTheme) {
124
+ return function themeFactory(overrides = {}) {
125
+ const merged = deepMerge(baseTheme, overrides);
126
+ if (overrides.ui?.colors) merged._userColorOverrides = { ...overrides.ui.colors };
127
+ return merged;
128
+ };
129
+ }
130
+ /**
131
+ * Extend an existing theme preset with additional defaults.
132
+ *
133
+ * Useful when you want to build on top of an existing theme (e.g. fumadocs)
134
+ * rather than starting from scratch.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * import { extendTheme } from "@farming-labs/docs";
139
+ * import { fumadocs } from "@farming-labs/theme/default";
140
+ *
141
+ * // Start with fumadocs defaults, override some values
142
+ * export const myTheme = extendTheme(fumadocs(), {
143
+ * name: "my-custom-fumadocs",
144
+ * ui: { colors: { primary: "#22c55e" } },
145
+ * });
146
+ * ```
147
+ */
148
+ function extendTheme(baseTheme, extensions) {
149
+ return deepMerge(baseTheme, extensions);
150
+ }
151
+
152
+ //#endregion
153
+ //#region src/i18n.ts
154
+ function normalizeSegment(value) {
155
+ return value.replace(/^\/+|\/+$/g, "");
156
+ }
157
+ function splitSegments(value) {
158
+ const cleaned = normalizeSegment(value);
159
+ return cleaned ? cleaned.split("/").filter(Boolean) : [];
160
+ }
161
+ function resolveDocsI18n(config) {
162
+ if (!config || !Array.isArray(config.locales)) return null;
163
+ const locales = Array.from(new Set(config.locales.map((l) => l.trim()).filter(Boolean)));
164
+ if (locales.length === 0) return null;
165
+ return {
166
+ locales,
167
+ defaultLocale: config.defaultLocale && locales.includes(config.defaultLocale) ? config.defaultLocale : locales[0]
168
+ };
169
+ }
170
+ function resolveDocsLocale(searchParams, i18n) {
171
+ if (!i18n) return void 0;
172
+ const raw = searchParams.get("lang") ?? searchParams.get("locale");
173
+ if (!raw) return void 0;
174
+ if (i18n.locales.includes(raw)) return raw;
175
+ return i18n.defaultLocale;
176
+ }
177
+ function resolveDocsPath(pathname, entry) {
178
+ const entryBase = normalizeSegment(entry || "docs") || "docs";
179
+ const entryParts = splitSegments(entryBase);
180
+ const pathParts = splitSegments(pathname);
181
+ let rest = pathParts;
182
+ if (entryParts.length > 0) {
183
+ if (pathParts.slice(0, entryParts.length).join("/") === entryParts.join("/")) rest = pathParts.slice(entryParts.length);
184
+ }
185
+ return {
186
+ slug: rest.join("/"),
187
+ entryPath: entryBase
188
+ };
189
+ }
190
+
191
+ //#endregion
192
+ //#region src/metadata.ts
193
+ /**
194
+ * Resolve page title using metadata titleTemplate.
195
+ * %s is replaced with page title.
196
+ */
197
+ function resolveTitle(pageTitle, metadata) {
198
+ return (metadata?.titleTemplate ?? "%s").replace("%s", pageTitle);
199
+ }
200
+ /**
201
+ * Resolve OG image URL for a page.
202
+ * Prefers page.openGraph.images[0], then page.ogImage, then config endpoint/default.
203
+ */
204
+ function resolveOGImage(page, ogConfig, baseUrl) {
205
+ if (page.openGraph?.images?.length) return resolveImageUrl(page.openGraph.images[0].url, baseUrl);
206
+ if (!ogConfig?.enabled) return void 0;
207
+ if (page.ogImage) return resolveImageUrl(page.ogImage, baseUrl);
208
+ if (ogConfig.type === "dynamic" && ogConfig.endpoint) return `${baseUrl ?? ""}${ogConfig.endpoint}`;
209
+ return ogConfig.defaultImage;
210
+ }
211
+ function resolveImageUrl(url, baseUrl) {
212
+ if (url.startsWith("/") || url.startsWith("http")) return url;
213
+ const base = baseUrl ?? "";
214
+ return `${base}${base.length > 0 && !base.endsWith("/") ? "/" : ""}${url}`;
215
+ }
216
+ /**
217
+ * Build the Open Graph metadata object for a page.
218
+ * When the page has openGraph in frontmatter, uses it (with title/description filled from page if omitted).
219
+ * Otherwise uses ogImage or config (dynamic endpoint / defaultImage).
220
+ */
221
+ function buildPageOpenGraph(page, ogConfig, baseUrl) {
222
+ if (page.openGraph) {
223
+ const images = page.openGraph.images?.length ? page.openGraph.images.map((img) => ({
224
+ url: resolveImageUrl(img.url, baseUrl),
225
+ width: img.width ?? 1200,
226
+ height: img.height ?? 630
227
+ })) : void 0;
228
+ return {
229
+ title: page.openGraph.title ?? page.title,
230
+ description: page.openGraph.description ?? page.description,
231
+ ...images && { images }
232
+ };
233
+ }
234
+ const url = resolveOGImage(page, ogConfig, baseUrl);
235
+ if (!url) return void 0;
236
+ return {
237
+ title: page.title,
238
+ ...page.description && { description: page.description },
239
+ images: [{
240
+ url,
241
+ width: 1200,
242
+ height: 630
243
+ }]
244
+ };
245
+ }
246
+ /**
247
+ * Build the Twitter card metadata object for a page.
248
+ * When the page has twitter in frontmatter, uses it.
249
+ * Otherwise builds from ogImage or config (dynamic endpoint).
250
+ */
251
+ function buildPageTwitter(page, ogConfig, baseUrl) {
252
+ if (page.twitter) {
253
+ const images = page.twitter.images?.length ? page.twitter.images.map((url) => resolveImageUrl(url, baseUrl)) : void 0;
254
+ return {
255
+ ...page.twitter.card && { card: page.twitter.card },
256
+ ...page.twitter.title !== void 0 && { title: page.twitter.title },
257
+ ...page.twitter.description !== void 0 && { description: page.twitter.description },
258
+ ...images && { images }
259
+ };
260
+ }
261
+ const url = resolveOGImage(page, ogConfig, baseUrl);
262
+ if (!url) return void 0;
263
+ return {
264
+ card: "summary_large_image",
265
+ title: page.title,
266
+ ...page.description && { description: page.description },
267
+ images: [url]
268
+ };
269
+ }
270
+
271
+ //#endregion
272
+ //#region src/reading-time.ts
273
+ function hasExplicitReadingTime(frontmatter) {
274
+ return Object.prototype.hasOwnProperty.call(frontmatter ?? {}, "readingTime");
275
+ }
276
+ function normalizeWordsPerMinute(wordsPerMinute) {
277
+ if (typeof wordsPerMinute !== "number" || !Number.isFinite(wordsPerMinute)) return 220;
278
+ return Math.max(1, Math.floor(wordsPerMinute));
279
+ }
280
+ function stripNonReadingContent(content) {
281
+ return content.replace(/(`{3,})[^\n]*\n[\s\S]*?\1/g, " ").replace(/(~{3,})[^\n]*\n[\s\S]*?\1/g, " ").replace(/`[^`\n]+`/g, " ").replace(/!\[[^\]]*\]\([^)]+\)/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, " $1 ").replace(/<[^>]+>/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[#>*_~|]/g, " ");
282
+ }
283
+ function estimateReadingTimeMinutes(content, wordsPerMinute) {
284
+ const wordCount = stripNonReadingContent(content).match(/\b[\p{L}\p{N}][\p{L}\p{N}'’-]*\b/gu)?.length ?? 0;
285
+ return Math.max(1, Math.ceil(wordCount / normalizeWordsPerMinute(wordsPerMinute)));
286
+ }
287
+ function resolveReadingTimeOptions(readingTime) {
288
+ if (readingTime === true) return { enabled: true };
289
+ if (readingTime === false || readingTime === void 0 || readingTime === null) return { enabled: false };
290
+ if (typeof readingTime !== "object") return { enabled: false };
291
+ return {
292
+ enabled: readingTime.enabled !== false,
293
+ wordsPerMinute: typeof readingTime.wordsPerMinute === "number" && Number.isFinite(readingTime.wordsPerMinute) ? readingTime.wordsPerMinute : void 0
294
+ };
295
+ }
296
+ function resolveReadingTimeFromContent(frontmatter, content, wordsPerMinute) {
297
+ const pageData = frontmatter ?? {};
298
+ if (pageData.readingTime === false) return null;
299
+ if (typeof pageData.readingTime === "number" && Number.isFinite(pageData.readingTime)) return Math.max(1, Math.ceil(pageData.readingTime));
300
+ return estimateReadingTimeMinutes(content, wordsPerMinute);
301
+ }
302
+ function resolvePageReadingTime(frontmatter, content, options) {
303
+ if (!(options?.enabledByDefault ?? false) && !hasExplicitReadingTime(frontmatter)) return;
304
+ return resolveReadingTimeFromContent(frontmatter, content, options?.wordsPerMinute);
305
+ }
306
+ function resolveReadingTimeFromSource(source, wordsPerMinute) {
307
+ const { data, content } = matter(source);
308
+ return resolveReadingTimeFromContent(data, content, wordsPerMinute);
309
+ }
310
+
311
+ //#endregion
3
312
  //#region src/agent.ts
4
313
  const DEFAULT_DOCS_API_ROUTE = "/api/docs";
5
314
  const DEFAULT_AGENT_SPEC_ROUTE = "/api/docs/agent/spec";
@@ -318,4 +627,4 @@ function toYamlString(value) {
318
627
  }
319
628
 
320
629
  //#endregion
321
- export { renderDocsMarkdownDocument as C, resolveDocsMarkdownRequest as D, resolveDocsLlmsTxtFormat as E, resolveDocsSkillFormat as O, normalizeDocsUrlPath as S, resolveDocsAgentMdxContent as T, isDocsAgentDiscoveryRequest as _, DEFAULT_DOCS_API_ROUTE as a, isDocsSkillRequest as b, DEFAULT_LLMS_TXT_ROUTE as c, DEFAULT_MCP_ROUTE as d, DEFAULT_MCP_WELL_KNOWN_ROUTE as f, findDocsMarkdownPage as g, buildDocsAgentDiscoverySpec as h, DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE as i, DEFAULT_LLMS_TXT_WELL_KNOWN_ROUTE as l, DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE as m, DEFAULT_AGENT_SPEC_ROUTE as n, DEFAULT_LLMS_FULL_TXT_ROUTE as o, DEFAULT_SKILL_MD_ROUTE as p, DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE as r, DEFAULT_LLMS_FULL_TXT_WELL_KNOWN_ROUTE as s, DEFAULT_AGENT_FEEDBACK_ROUTE as t, DEFAULT_MCP_PUBLIC_ROUTE as u, isDocsMcpRequest as v, renderDocsSkillDocument as w, normalizeDocsPathSegment as x, isDocsPublicGetRequest as y };
630
+ export { resolvePageReadingTime as A, resolveDocsPath as B, renderDocsMarkdownDocument as C, resolveDocsMarkdownRequest as D, resolveDocsLlmsTxtFormat as E, buildPageTwitter as F, defineDocs as G, extendTheme as H, resolveOGImage as I, resolveTitle as L, resolveReadingTimeFromSource as M, resolveReadingTimeOptions as N, resolveDocsSkillFormat as O, buildPageOpenGraph as P, resolveDocsI18n as R, normalizeDocsUrlPath as S, resolveDocsAgentMdxContent as T, deepMerge as U, createTheme as V, resolveChangelogConfig as W, isDocsAgentDiscoveryRequest as _, DEFAULT_DOCS_API_ROUTE as a, isDocsSkillRequest as b, DEFAULT_LLMS_TXT_ROUTE as c, DEFAULT_MCP_ROUTE as d, DEFAULT_MCP_WELL_KNOWN_ROUTE as f, findDocsMarkdownPage as g, buildDocsAgentDiscoverySpec as h, DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE as i, resolveReadingTimeFromContent as j, estimateReadingTimeMinutes as k, DEFAULT_LLMS_TXT_WELL_KNOWN_ROUTE as l, DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE as m, DEFAULT_AGENT_SPEC_ROUTE as n, DEFAULT_LLMS_FULL_TXT_ROUTE as o, DEFAULT_SKILL_MD_ROUTE as p, DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE as r, DEFAULT_LLMS_FULL_TXT_WELL_KNOWN_ROUTE as s, DEFAULT_AGENT_FEEDBACK_ROUTE as t, DEFAULT_MCP_PUBLIC_ROUTE as u, isDocsMcpRequest as v, renderDocsSkillDocument as w, normalizeDocsPathSegment as x, isDocsPublicGetRequest as y, resolveDocsLocale as z };
@@ -69,13 +69,13 @@ async function main() {
69
69
  searchApiKey: typeof flags["search-api-key"] === "string" ? flags["search-api-key"] : void 0
70
70
  };
71
71
  if (!parsedCommand.command || parsedCommand.command === "init") {
72
- const { init } = await import("../init-CShiL8Ch.mjs");
72
+ const { init } = await import("../init-C1tsFxXo.mjs");
73
73
  await init(initOptions);
74
74
  } else if (parsedCommand.command === "mcp") {
75
- const { runMcp } = await import("../mcp-CYpMeMfi.mjs");
75
+ const { runMcp } = await import("../mcp-C-TmMrdw.mjs");
76
76
  await runMcp(mcpOptions);
77
77
  } else if (parsedCommand.command === "agent" && subcommand === "compact") {
78
- const { compactAgentDocs, parseAgentCompactArgs, printAgentCompactHelp } = await import("../agent-CuYI2Nji.mjs");
78
+ const { compactAgentDocs, parseAgentCompactArgs, printAgentCompactHelp } = await import("../agent-C3rj3o1l.mjs");
79
79
  const agentCompactOptions = parseAgentCompactArgs(args.slice(2));
80
80
  if (agentCompactOptions.help) {
81
81
  printAgentCompactHelp();
@@ -85,11 +85,11 @@ async function main() {
85
85
  } else if (parsedCommand.command === "agent") {
86
86
  console.error(pc.red(`Unknown agent subcommand: ${subcommand ?? "(missing)"}`));
87
87
  console.error();
88
- const { printAgentCompactHelp } = await import("../agent-CuYI2Nji.mjs");
88
+ const { printAgentCompactHelp } = await import("../agent-C3rj3o1l.mjs");
89
89
  printAgentCompactHelp();
90
90
  process.exit(1);
91
91
  } else if (parsedCommand.command === "doctor") {
92
- const { parseDoctorArgs, printDoctorHelp, runDoctor } = await import("../doctor-D0UYKEFN.mjs");
92
+ const { parseDoctorArgs, printDoctorHelp, runDoctor } = await import("../doctor-B0xEGzBU.mjs");
93
93
  const doctorOptions = parseDoctorArgs(args.slice(1));
94
94
  if (doctorOptions.help) {
95
95
  printDoctorHelp();
@@ -97,7 +97,7 @@ async function main() {
97
97
  }
98
98
  await runDoctor(doctorOptions);
99
99
  } else if (parsedCommand.command === "search" && subcommand === "sync") {
100
- const { syncSearch } = await import("../search-D7DS0MLc.mjs");
100
+ const { syncSearch } = await import("../search-C7mKYgqQ.mjs");
101
101
  await syncSearch(searchSyncOptions);
102
102
  } else if (parsedCommand.command === "search") {
103
103
  console.error(pc.red(`Unknown search subcommand: ${subcommand ?? "(missing)"}`));
@@ -105,7 +105,7 @@ async function main() {
105
105
  printHelp();
106
106
  process.exit(1);
107
107
  } else if (parsedCommand.command === "upgrade") {
108
- const { upgrade } = await import("../upgrade-2xcgMsj6.mjs");
108
+ const { upgrade } = await import("../upgrade-a185_G5A.mjs");
109
109
  await upgrade({
110
110
  framework: (typeof flags.framework === "string" ? flags.framework : void 0) ?? (args[1] && !args[1].startsWith("--") ? args[1] : void 0),
111
111
  tag: args.includes("--beta") ? "beta" : args.includes("--latest") ? "latest" : parsedCommand.tag ?? "latest"
@@ -152,7 +152,9 @@ ${pc.dim("Options for mcp:")}
152
152
  ${pc.dim("Options for agent compact:")}
153
153
  ${pc.cyan("agent compact <page...>")} Compact pages and write sibling ${pc.dim("agent.md")} files
154
154
  ${pc.cyan("agent compact --all")} Compact every folder-based docs page
155
+ ${pc.cyan("agent compact --stale")} Refresh only stale generated ${pc.dim("agent.md")} files
155
156
  ${pc.cyan("--page <slug|path>")} Repeatable explicit page flag; positional page args work too
157
+ ${pc.cyan("--include-missing")} With ${pc.cyan("--stale")}, also create explicit or token-budget pages missing ${pc.dim("agent.md")}
156
158
  ${pc.cyan("--api-key <key>")} Token Company API key (or use ${pc.dim("TOKEN_COMPANY_API_KEY")})
157
159
  ${pc.cyan("--api-key-env <name>")} Custom env var name for the Token Company API key
158
160
  ${pc.cyan("--base-url <url>")} Override the Token Company API base URL
@@ -164,6 +166,7 @@ ${pc.dim("Options for doctor:")}
164
166
  ${pc.cyan("doctor --agent")} Same as ${pc.cyan("doctor")}; explicit agent scoring mode
165
167
  ${pc.cyan("doctor --site")} Score the current docs app for reader-facing docs quality
166
168
  ${pc.cyan("doctor --human")} Alias for ${pc.cyan("doctor --site")}
169
+ ${pc.cyan("doctor --json")} Print the report as JSON for CI, scripts, and automation
167
170
  ${pc.cyan("doctor agent")} Subcommand alias for agent scoring
168
171
  ${pc.cyan("doctor site")} Subcommand alias for reader-facing scoring
169
172
  ${pc.cyan("doctor human")} Legacy alias for reader-facing scoring