@farming-labs/docs 0.1.49 → 0.1.51

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,11 @@
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
+ import matter from "gray-matter";
8
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
10
  import path from "node:path";
10
11
  import pc from "picocolors";
@@ -49,6 +50,14 @@ function parseAgentCompactArgs(argv) {
49
50
  parsed.dryRun = true;
50
51
  continue;
51
52
  }
53
+ if (arg === "--stale") {
54
+ parsed.stale = true;
55
+ continue;
56
+ }
57
+ if (arg === "--include-missing") {
58
+ parsed.includeMissing = true;
59
+ continue;
60
+ }
52
61
  if (arg === "--protect-json") {
53
62
  const nextValue = argv[index + 1];
54
63
  if (nextValue && !nextValue.startsWith("--")) {
@@ -212,6 +221,103 @@ function mergeAgentCompactOptions(defaults, overrides) {
212
221
  ...Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0))
213
222
  };
214
223
  }
224
+ function normalizeTokenBudget(value) {
225
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
226
+ return Math.max(1, Math.ceil(value));
227
+ }
228
+ function readPageTokenBudget(pagePath) {
229
+ const { data } = matter(readFileSync(pagePath, "utf-8"));
230
+ return normalizeTokenBudget(data.agent?.tokenBudget);
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
+ }
215
321
  function protectForCompression(input) {
216
322
  const segments = [];
217
323
  const stash = (value) => {
@@ -302,8 +408,9 @@ async function compactAgentDocs(options = {}) {
302
408
  const contentDir = typeof loadedConfigModule?.config.contentDir === "string" ? loadedConfigModule.config.contentDir : resolveDocsContentDir(rootDir, configContent, entry);
303
409
  const siteTitle = typeof loadedConfigModule?.config.nav?.title === "string" ? loadedConfigModule.config.nav.title : readNavTitle(configContent) ?? "Documentation";
304
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.");
305
412
  const requestedPages = resolvedOptions.pages?.filter((value) => value.trim().length > 0) ?? [];
306
- 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.");
307
414
  const pages = await createFilesystemDocsMcpSource({
308
415
  rootDir,
309
416
  entry,
@@ -311,22 +418,58 @@ async function compactAgentDocs(options = {}) {
311
418
  siteTitle
312
419
  }).getPages();
313
420
  if (pages.length === 0) throw new Error(`No docs content was found under ${contentDir}.`);
314
- 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);
315
422
  if (selectedPages.length === 0) throw new Error("No compactable docs pages matched the request.");
316
423
  let created = 0;
317
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;
318
431
  for (const { page, target } of selectedPages) {
319
- const compressed = await compressDocument(renderDocsMarkdownDocument(page), resolvedOptions);
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);
320
454
  const nextContent = compressed.output.trimEnd();
455
+ const generatedDocument = serializeGeneratedAgentDocument(nextContent, buildGeneratedAgentProvenance(state.sourceKind, state.sourceDocument, nextContent, state.pageOptions));
321
456
  console.log(pc.dim(`Compacting ${page.url} (${compressed.original_input_tokens ?? "?"} -> ${compressed.output_tokens ?? "?"} tokens)...`));
322
457
  if (resolvedOptions.dryRun) continue;
323
458
  mkdirSync(target.pageDir, { recursive: true });
324
- writeFileSync(target.agentPath, `${nextContent}\n`, "utf-8");
459
+ writeFileSync(target.agentPath, generatedDocument, "utf-8");
325
460
  if (target.hasAgentFile) overwritten += 1;
326
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;
327
469
  }
328
470
  const summaryPrefix = resolvedOptions.dryRun ? "Dry run complete" : "Compaction complete";
329
- 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"}.`));
330
473
  }
331
474
  function printAgentCompactHelp() {
332
475
  console.log(`
@@ -340,10 +483,17 @@ ${pc.dim("Examples:")}
340
483
  ${pc.cyan("npx @farming-labs/docs@latest agent compact /docs/installation")}
341
484
  ${pc.cyan("npx @farming-labs/docs@latest agent compact --page installation --page configuration")}
342
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")}
488
+
489
+ ${pc.dim("Per-page override:")}
490
+ Add ${pc.cyan("agent.tokenBudget")} to a page frontmatter block to override the compact output target for that page.
343
491
 
344
492
  ${pc.dim("Options:")}
345
493
  ${pc.cyan("--all")} Compact every folder-based docs page under the configured contentDir
346
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")}
347
497
  ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
348
498
  ${pc.cyan("--api-key <key>")} Token Company API key (or set ${pc.dim("TOKEN_COMPANY_API_KEY")})
349
499
  ${pc.cyan("--api-key-env <name>")} Custom env var name for the Token Company API key
@@ -358,4 +508,4 @@ ${pc.dim("Options:")}
358
508
  }
359
509
 
360
510
  //#endregion
361
- 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-D6Fa41gs.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-D6Fa41gs.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-BfBpzPir.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