@farming-labs/theme 0.1.70 → 0.1.72

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,4 +1,4 @@
1
- import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
1
+ import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsObservabilityConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
2
2
 
3
3
  //#region src/docs-api.d.ts
4
4
  interface AIProviderConfig {
@@ -43,6 +43,8 @@ interface DocsAPIOptions {
43
43
  search?: boolean | DocsSearchConfig;
44
44
  /** Analytics configuration */
45
45
  analytics?: boolean | DocsAnalyticsConfig;
46
+ /** Observability configuration for logs, traces, and metrics callbacks. */
47
+ observability?: boolean | DocsObservabilityConfig;
46
48
  /** Feedback configuration */
47
49
  feedback?: boolean | FeedbackConfig;
48
50
  /** MCP configuration used for the agent discovery spec. */
@@ -59,6 +61,7 @@ interface DocsMCPAPIOptions {
59
61
  mcp?: boolean | DocsMcpConfig;
60
62
  search?: boolean | DocsSearchConfig;
61
63
  analytics?: boolean | DocsAnalyticsConfig;
64
+ observability?: boolean | DocsObservabilityConfig;
62
65
  }
63
66
  /**
64
67
  * Create a unified docs API route handler.
package/dist/docs-api.mjs CHANGED
@@ -3,7 +3,7 @@ import { getNextAppDir } from "./get-app-dir.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import matter from "gray-matter";
6
- import { emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolveSearchRequestConfig } from "@farming-labs/docs";
6
+ import { createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig } from "@farming-labs/docs";
7
7
  import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource, resolveDocsMcpConfig } from "@farming-labs/docs/server";
8
8
 
9
9
  //#region src/docs-api.ts
@@ -660,6 +660,45 @@ function resolveAgentMdxContent(content, audience) {
660
660
  function stripMdx(content) {
661
661
  return content.replace(/^(import|export)\s.*$/gm, "").replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
662
662
  }
663
+ function resolveDocsSearchPageSource(dir) {
664
+ return ["page.mdx", "page.md"].map((fileName) => path.join(dir, fileName)).find((candidate) => fs.existsSync(candidate));
665
+ }
666
+ function hasVisibleDescendantDocsSearchPage(dir) {
667
+ let entries;
668
+ try {
669
+ entries = fs.readdirSync(dir);
670
+ } catch {
671
+ return false;
672
+ }
673
+ for (const name of entries.sort()) {
674
+ const full = path.join(dir, name);
675
+ try {
676
+ if (!fs.statSync(full).isDirectory()) continue;
677
+ } catch {
678
+ continue;
679
+ }
680
+ const pageSource = resolveDocsSearchPageSource(full);
681
+ if (pageSource) try {
682
+ const data = matter(fs.readFileSync(pageSource, "utf-8")).data;
683
+ const hiddenFolderIndex = resolvePageSidebarFolderIndexBehavior(data.sidebar) === "hidden";
684
+ if (data.hidden !== true && !hiddenFolderIndex) return true;
685
+ } catch {
686
+ return true;
687
+ }
688
+ if (hasVisibleDescendantDocsSearchPage(full)) return true;
689
+ }
690
+ return false;
691
+ }
692
+ function isHiddenFolderIndexPageDir(dir) {
693
+ const pageSource = resolveDocsSearchPageSource(dir);
694
+ if (!pageSource) return false;
695
+ try {
696
+ const data = matter(fs.readFileSync(pageSource, "utf-8")).data;
697
+ return resolvePageSidebarFolderIndexBehavior(data.sidebar) === "hidden" && hasVisibleDescendantDocsSearchPage(dir);
698
+ } catch {
699
+ return false;
700
+ }
701
+ }
663
702
  function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
664
703
  const indexes = [];
665
704
  function isExcluded(dir) {
@@ -672,27 +711,29 @@ function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
672
711
  function scan(dir, slugParts) {
673
712
  if (!fs.existsSync(dir)) return;
674
713
  if (isExcluded(dir)) return;
675
- const pagePath = path.join(dir, "page.mdx");
676
- if (fs.existsSync(pagePath)) try {
677
- const raw = fs.readFileSync(pagePath, "utf-8");
714
+ const pageSource = resolveDocsSearchPageSource(dir);
715
+ if (pageSource) try {
716
+ const raw = fs.readFileSync(pageSource, "utf-8");
678
717
  const { data } = matter(raw);
679
- const title = data.title || slugParts[slugParts.length - 1]?.replace(/-/g, " ") || "Documentation";
680
- const description = data.description;
681
- const { content: fileContent } = matter(raw);
682
- const rawContent = resolveAgentMdxContent(fileContent, "human");
683
- const agentRawContent = resolveAgentMdxContent(fileContent, "agent");
684
- const content = stripMdx(rawContent);
685
- const url = withLangInUrl(slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`, locale);
686
- indexes.push({
687
- title,
688
- description,
689
- relatedInput: data.related,
690
- content,
691
- rawContent,
692
- agentFallbackRawContent: agentRawContent !== rawContent ? agentRawContent : void 0,
693
- url,
694
- locale
695
- });
718
+ if (resolvePageSidebarFolderIndexBehavior(data.sidebar) === "hidden" && hasVisibleDescendantDocsSearchPage(dir)) {} else {
719
+ const title = data.title || slugParts[slugParts.length - 1]?.replace(/-/g, " ") || "Documentation";
720
+ const description = data.description;
721
+ const { content: fileContent } = matter(raw);
722
+ const rawContent = resolveAgentMdxContent(fileContent, "human");
723
+ const agentRawContent = resolveAgentMdxContent(fileContent, "agent");
724
+ const content = stripMdx(rawContent);
725
+ const url = withLangInUrl(slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`, locale);
726
+ indexes.push({
727
+ title,
728
+ description,
729
+ relatedInput: data.related,
730
+ content,
731
+ rawContent,
732
+ agentFallbackRawContent: agentRawContent !== rawContent ? agentRawContent : void 0,
733
+ url,
734
+ locale
735
+ });
736
+ }
696
737
  } catch {}
697
738
  let entries;
698
739
  try {
@@ -915,9 +956,74 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
915
956
  apiKey
916
957
  };
917
958
  }
918
- async function handleAskAI(request, indexes, aiConfig, analytics, analyticsContext = {}) {
959
+ function safeUrlOrigin(value) {
960
+ try {
961
+ return new URL(value).origin;
962
+ } catch {
963
+ return value;
964
+ }
965
+ }
966
+ async function handleAskAI(request, indexes, aiConfig, analytics, observability, analyticsContext = {}) {
919
967
  const url = new URL(request.url);
920
968
  const requestStartedAt = Date.now();
969
+ const trace = createDocsAgentTraceContext("ask-ai");
970
+ const runSpanId = createDocsAgentTraceId("span");
971
+ const traceBase = {
972
+ source: "server",
973
+ traceId: trace.traceId,
974
+ url: request.url,
975
+ path: url.pathname,
976
+ locale: analyticsContext.locale
977
+ };
978
+ async function emitTrace(event) {
979
+ await emitDocsAgentTraceEvent(observability, {
980
+ ...traceBase,
981
+ ...event
982
+ });
983
+ }
984
+ async function emitRunError(reason, outputPreview = {}) {
985
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
986
+ const elapsed = Math.max(0, Date.now() - requestStartedAt);
987
+ const common = {
988
+ name: "ask-ai",
989
+ startedAt: trace.startedAt,
990
+ endedAt,
991
+ durationMs: elapsed,
992
+ status: "error",
993
+ outputPreview: {
994
+ reason,
995
+ ...outputPreview
996
+ },
997
+ metadata: { reason }
998
+ };
999
+ await emitTrace({
1000
+ ...common,
1001
+ type: "error",
1002
+ parentSpanId: runSpanId
1003
+ });
1004
+ await emitTrace({
1005
+ ...common,
1006
+ type: "run.error",
1007
+ spanId: runSpanId
1008
+ });
1009
+ await emitTrace({
1010
+ ...common,
1011
+ type: "run.end",
1012
+ spanId: runSpanId
1013
+ });
1014
+ }
1015
+ await emitTrace({
1016
+ type: "run.start",
1017
+ name: "ask-ai",
1018
+ spanId: runSpanId,
1019
+ startedAt: trace.startedAt,
1020
+ durationMs: 0,
1021
+ status: "started",
1022
+ inputPreview: {
1023
+ method: request.method,
1024
+ path: url.pathname
1025
+ }
1026
+ });
921
1027
  let body;
922
1028
  try {
923
1029
  body = await request.json();
@@ -933,6 +1039,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
933
1039
  durationMs: Math.max(0, Date.now() - requestStartedAt)
934
1040
  }
935
1041
  });
1042
+ await emitRunError("invalid_json", { status: 400 });
936
1043
  return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
937
1044
  }
938
1045
  const messages = body.messages;
@@ -948,6 +1055,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
948
1055
  durationMs: Math.max(0, Date.now() - requestStartedAt)
949
1056
  }
950
1057
  });
1058
+ await emitRunError("missing_messages", { status: 400 });
951
1059
  return Response.json({ error: "messages array is required and must not be empty." }, { status: 400 });
952
1060
  }
953
1061
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
@@ -964,10 +1072,44 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
964
1072
  durationMs: Math.max(0, Date.now() - requestStartedAt)
965
1073
  }
966
1074
  });
1075
+ await emitRunError("missing_user_message", {
1076
+ status: 400,
1077
+ messageCount: messages.length
1078
+ });
967
1079
  return Response.json({ error: "At least one user message is required." }, { status: 400 });
968
1080
  }
969
1081
  const maxResults = aiConfig.maxResults ?? 5;
970
1082
  const query = lastUserMessage.content;
1083
+ await emitTrace({
1084
+ type: "user.input",
1085
+ name: "ask-ai",
1086
+ parentSpanId: runSpanId,
1087
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1088
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1089
+ durationMs: 0,
1090
+ status: "success",
1091
+ inputPreview: {
1092
+ messageCount: messages.length,
1093
+ questionLength: query.length,
1094
+ requestedModel: typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0
1095
+ }
1096
+ });
1097
+ const retrievalStartedAt = Date.now();
1098
+ const retrievalStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
1099
+ const retrievalSpanId = createDocsAgentTraceId("span");
1100
+ await emitTrace({
1101
+ type: "retrieval.query",
1102
+ name: "docs-index",
1103
+ spanId: retrievalSpanId,
1104
+ parentSpanId: runSpanId,
1105
+ startedAt: retrievalStartedAtIso,
1106
+ status: "started",
1107
+ inputPreview: {
1108
+ queryLength: query.length,
1109
+ maxResults,
1110
+ indexSize: indexes.length
1111
+ }
1112
+ });
971
1113
  const scored = indexes.map((doc) => {
972
1114
  const q = query.toLowerCase();
973
1115
  const titleMatch = doc.title.toLowerCase().includes(q) ? 10 : 0;
@@ -979,12 +1121,52 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
979
1121
  score: titleMatch + contentMatch
980
1122
  };
981
1123
  }).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults);
1124
+ await emitTrace({
1125
+ type: "retrieval.result",
1126
+ name: "docs-index",
1127
+ parentSpanId: retrievalSpanId,
1128
+ startedAt: retrievalStartedAtIso,
1129
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1130
+ durationMs: Math.max(0, Date.now() - retrievalStartedAt),
1131
+ status: "success",
1132
+ outputPreview: {
1133
+ resultCount: scored.length,
1134
+ urls: scored.slice(0, 5).map((doc) => doc.url)
1135
+ },
1136
+ metadata: {
1137
+ maxResults,
1138
+ indexSize: indexes.length
1139
+ }
1140
+ });
1141
+ const promptStartedAt = Date.now();
1142
+ const promptStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
1143
+ const promptSpanId = createDocsAgentTraceId("span");
982
1144
  const context = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`).join("\n\n---\n\n");
983
1145
  const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
984
- const llmMessages = [{
1146
+ const systemMessage = {
985
1147
  role: "system",
986
1148
  content: context ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : systemPrompt
987
- }, ...messages.filter((m) => m.role !== "system")];
1149
+ };
1150
+ const llmMessages = [systemMessage, ...messages.filter((m) => m.role !== "system")];
1151
+ await emitTrace({
1152
+ type: "prompt.build",
1153
+ name: "ask-ai.prompt",
1154
+ spanId: promptSpanId,
1155
+ parentSpanId: runSpanId,
1156
+ startedAt: promptStartedAtIso,
1157
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1158
+ durationMs: Math.max(0, Date.now() - promptStartedAt),
1159
+ status: "success",
1160
+ inputPreview: {
1161
+ messageCount: messages.length,
1162
+ retrievedCount: scored.length
1163
+ },
1164
+ outputPreview: {
1165
+ llmMessageCount: llmMessages.length,
1166
+ contextChars: context.length,
1167
+ systemMessageChars: systemMessage.content.length
1168
+ }
1169
+ });
988
1170
  const resolved = resolveModelAndProvider(aiConfig, typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0);
989
1171
  if (!resolved.apiKey) {
990
1172
  await emitDocsAnalyticsEvent(analytics, {
@@ -1003,6 +1185,13 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1003
1185
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1004
1186
  }
1005
1187
  });
1188
+ await emitRunError("missing_api_key", {
1189
+ status: 500,
1190
+ messageCount: messages.length,
1191
+ questionLength: query.length,
1192
+ retrievedCount: scored.length,
1193
+ model: resolved.model
1194
+ });
1006
1195
  return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config ai section, configure a provider, or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
1007
1196
  }
1008
1197
  await emitDocsAnalyticsEvent(analytics, {
@@ -1019,20 +1208,100 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1019
1208
  model: resolved.model
1020
1209
  }
1021
1210
  });
1022
- const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
1023
- method: "POST",
1024
- headers: {
1025
- "Content-Type": "application/json",
1026
- Authorization: `Bearer ${resolved.apiKey}`
1027
- },
1028
- body: JSON.stringify({
1029
- model: resolved.model,
1211
+ const modelStartedAt = Date.now();
1212
+ const modelStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
1213
+ const modelSpanId = createDocsAgentTraceId("span");
1214
+ const providerOrigin = safeUrlOrigin(resolved.baseUrl);
1215
+ await emitTrace({
1216
+ type: "model.call",
1217
+ name: resolved.model,
1218
+ spanId: modelSpanId,
1219
+ parentSpanId: runSpanId,
1220
+ startedAt: modelStartedAtIso,
1221
+ status: "started",
1222
+ inputPreview: {
1223
+ messageCount: llmMessages.length,
1030
1224
  stream: true,
1031
- messages: llmMessages
1032
- })
1225
+ providerOrigin
1226
+ },
1227
+ metadata: { model: resolved.model }
1033
1228
  });
1229
+ let llmResponse;
1230
+ try {
1231
+ llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
1232
+ method: "POST",
1233
+ headers: {
1234
+ "Content-Type": "application/json",
1235
+ Authorization: `Bearer ${resolved.apiKey}`
1236
+ },
1237
+ body: JSON.stringify({
1238
+ model: resolved.model,
1239
+ stream: true,
1240
+ messages: llmMessages
1241
+ })
1242
+ });
1243
+ } catch (error) {
1244
+ const elapsed = Math.max(0, Date.now() - modelStartedAt);
1245
+ const message = error instanceof Error ? error.message : "Unknown error";
1246
+ await emitTrace({
1247
+ type: "model.error",
1248
+ name: resolved.model,
1249
+ parentSpanId: modelSpanId,
1250
+ startedAt: modelStartedAtIso,
1251
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1252
+ durationMs: elapsed,
1253
+ status: "error",
1254
+ outputPreview: { message },
1255
+ metadata: {
1256
+ model: resolved.model,
1257
+ providerOrigin
1258
+ }
1259
+ });
1260
+ await emitDocsAnalyticsEvent(analytics, {
1261
+ type: "api_ai_error",
1262
+ source: "server",
1263
+ url: request.url,
1264
+ path: url.pathname,
1265
+ locale: analyticsContext.locale,
1266
+ input: { question: query },
1267
+ properties: {
1268
+ reason: "llm_fetch_error",
1269
+ messageCount: messages.length,
1270
+ questionLength: query.length,
1271
+ retrievedCount: scored.length,
1272
+ model: resolved.model,
1273
+ durationMs: Math.max(0, Date.now() - requestStartedAt)
1274
+ }
1275
+ });
1276
+ await emitRunError("llm_fetch_error", {
1277
+ status: 502,
1278
+ messageCount: messages.length,
1279
+ questionLength: query.length,
1280
+ retrievedCount: scored.length,
1281
+ model: resolved.model
1282
+ });
1283
+ return Response.json({ error: "LLM API request failed." }, { status: 502 });
1284
+ }
1034
1285
  if (!llmResponse.ok) {
1035
1286
  const errText = await llmResponse.text().catch(() => "Unknown error");
1287
+ const elapsed = Math.max(0, Date.now() - modelStartedAt);
1288
+ await emitTrace({
1289
+ type: "model.error",
1290
+ name: resolved.model,
1291
+ parentSpanId: modelSpanId,
1292
+ startedAt: modelStartedAtIso,
1293
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1294
+ durationMs: elapsed,
1295
+ status: "error",
1296
+ outputPreview: {
1297
+ status: llmResponse.status,
1298
+ errorChars: errText.length
1299
+ },
1300
+ metadata: {
1301
+ model: resolved.model,
1302
+ providerOrigin
1303
+ }
1304
+ });
1036
1305
  await emitDocsAnalyticsEvent(analytics, {
1037
1306
  type: "api_ai_error",
1038
1307
  source: "server",
@@ -1050,6 +1319,14 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1050
1319
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1051
1320
  }
1052
1321
  });
1322
+ await emitRunError("llm_error", {
1323
+ status: 502,
1324
+ modelStatus: llmResponse.status,
1325
+ messageCount: messages.length,
1326
+ questionLength: query.length,
1327
+ retrievedCount: scored.length,
1328
+ model: resolved.model
1329
+ });
1053
1330
  return Response.json({ error: `LLM API error (${llmResponse.status}): ${errText}` }, { status: 502 });
1054
1331
  }
1055
1332
  await emitDocsAnalyticsEvent(analytics, {
@@ -1067,6 +1344,66 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1067
1344
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1068
1345
  }
1069
1346
  });
1347
+ const responseEndedAt = (/* @__PURE__ */ new Date()).toISOString();
1348
+ const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
1349
+ await emitTrace({
1350
+ type: "model.response",
1351
+ name: resolved.model,
1352
+ parentSpanId: modelSpanId,
1353
+ startedAt: modelStartedAtIso,
1354
+ endedAt: responseEndedAt,
1355
+ durationMs: modelDurationMs,
1356
+ status: "success",
1357
+ outputPreview: {
1358
+ status: llmResponse.status,
1359
+ stream: true,
1360
+ contentType: llmResponse.headers.get("content-type") ?? void 0
1361
+ },
1362
+ metadata: {
1363
+ model: resolved.model,
1364
+ providerOrigin
1365
+ }
1366
+ });
1367
+ await emitTrace({
1368
+ type: "model.stream",
1369
+ name: resolved.model,
1370
+ parentSpanId: modelSpanId,
1371
+ startedAt: modelStartedAtIso,
1372
+ endedAt: responseEndedAt,
1373
+ durationMs: modelDurationMs,
1374
+ status: "success",
1375
+ outputPreview: { stream: true },
1376
+ metadata: { model: resolved.model }
1377
+ });
1378
+ const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
1379
+ await emitTrace({
1380
+ type: "agent.final",
1381
+ name: "ask-ai",
1382
+ parentSpanId: runSpanId,
1383
+ startedAt: trace.startedAt,
1384
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1385
+ durationMs: runDurationMs,
1386
+ status: "success",
1387
+ outputPreview: {
1388
+ stream: true,
1389
+ retrievedCount: scored.length
1390
+ },
1391
+ metadata: { model: resolved.model }
1392
+ });
1393
+ await emitTrace({
1394
+ type: "run.end",
1395
+ name: "ask-ai",
1396
+ spanId: runSpanId,
1397
+ startedAt: trace.startedAt,
1398
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1399
+ durationMs: runDurationMs,
1400
+ status: "success",
1401
+ outputPreview: {
1402
+ stream: true,
1403
+ retrievedCount: scored.length
1404
+ },
1405
+ metadata: { model: resolved.model }
1406
+ });
1070
1407
  return new Response(llmResponse.body, { headers: {
1071
1408
  "Content-Type": "text/event-stream",
1072
1409
  "Cache-Control": "no-cache",
@@ -1147,6 +1484,7 @@ function createDocsAPI(options) {
1147
1484
  const root = options?.rootDir ?? process.cwd();
1148
1485
  const entry = options?.entry ?? readEntry(root);
1149
1486
  const analytics = options?.analytics;
1487
+ const observability = options?.observability;
1150
1488
  const appDir = getNextAppDir(root);
1151
1489
  const contentDir = options?.contentDir ?? path.join(appDir, entry);
1152
1490
  const changelogConfig = resolveChangelogConfig(options?.changelog);
@@ -1249,11 +1587,14 @@ function createDocsAPI(options) {
1249
1587
  return sources;
1250
1588
  }
1251
1589
  async function getMarkdownDocument(ctx, requestedPath) {
1590
+ const normalizedRequest = normalizeRequestedMarkdownPath(ctx.entryPath, requestedPath);
1591
+ const normalizedEntry = `/${normalizePathSegment(ctx.entryPath)}`;
1592
+ const relativeSlug = normalizedRequest === normalizedEntry ? "" : normalizedRequest.slice(normalizedEntry.length).replace(/^\/+/, "");
1593
+ for (const docsDir of ctx.docsDirs) if (isHiddenFolderIndexPageDir(relativeSlug ? path.join(docsDir, ...relativeSlug.split("/")) : docsDir)) return null;
1252
1594
  for (const source of getMarkdownSources(ctx)) {
1253
1595
  const page = findDocsMcpPage(ctx.entryPath, await source.getPages(), requestedPath);
1254
1596
  if (page) return renderMarkdownDocument(page);
1255
1597
  }
1256
- const normalizedRequest = normalizeRequestedMarkdownPath(ctx.entryPath, requestedPath);
1257
1598
  const fallbackPage = getIndexes(ctx).find((page) => normalizeUrlPath(page.url) === normalizedRequest);
1258
1599
  if (fallbackPage) return renderMarkdownDocument(fallbackPage);
1259
1600
  for (const page of getIndexes(ctx)) if (normalizePathSegment(page.url.replace(/^\/+/, "").replace(`${ctx.entryPath}/`, "")) === normalizePathSegment(requestedPath.replace(/^\/+/, "").replace(/\.md$/i, ""))) return renderMarkdownDocument(page);
@@ -1542,7 +1883,7 @@ function createDocsAPI(options) {
1542
1883
  return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
1543
1884
  }
1544
1885
  const ctx = resolveContextFromRequest(request);
1545
- return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, { locale: ctx.locale });
1886
+ return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, observability, { locale: ctx.locale });
1546
1887
  }
1547
1888
  };
1548
1889
  }
@@ -1572,6 +1913,7 @@ function createDocsMCPAPI(options = {}) {
1572
1913
  mcp: options.mcp ?? readMcpConfig(rootDir),
1573
1914
  search: options.search,
1574
1915
  analytics: options.analytics,
1916
+ observability: options.observability,
1575
1917
  defaultName: navTitle
1576
1918
  });
1577
1919
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -139,7 +139,7 @@
139
139
  "tsdown": "^0.20.3",
140
140
  "typescript": "^5.9.3",
141
141
  "vitest": "^3.2.4",
142
- "@farming-labs/docs": "0.1.70"
142
+ "@farming-labs/docs": "0.1.72"
143
143
  },
144
144
  "peerDependencies": {
145
145
  "@farming-labs/docs": ">=0.0.1",