@agent-native/core 0.44.0 → 0.44.3

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 (75) hide show
  1. package/dist/agent/production-agent.d.ts.map +1 -1
  2. package/dist/agent/production-agent.js +28 -13
  3. package/dist/agent/production-agent.js.map +1 -1
  4. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  5. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  6. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  7. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  8. package/dist/cli/recap.d.ts +23 -0
  9. package/dist/cli/recap.d.ts.map +1 -1
  10. package/dist/cli/recap.js +175 -0
  11. package/dist/cli/recap.js.map +1 -1
  12. package/dist/cli/skills.d.ts +3 -3
  13. package/dist/cli/skills.d.ts.map +1 -1
  14. package/dist/cli/skills.js +54 -7
  15. package/dist/cli/skills.js.map +1 -1
  16. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  17. package/dist/client/blocks/library/AnnotatedCodeBlock.js +21 -8
  18. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  19. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
  20. package/dist/client/blocks/library/ApiEndpointBlock.js +112 -12
  21. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
  22. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  23. package/dist/client/blocks/library/DiffBlock.js +59 -75
  24. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  25. package/dist/client/blocks/library/JsonExplorerBlock.d.ts.map +1 -1
  26. package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
  27. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
  28. package/dist/client/blocks/library/MermaidBlock.d.ts.map +1 -1
  29. package/dist/client/blocks/library/MermaidBlock.js +22 -3
  30. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  31. package/dist/client/blocks/library/annotation-rail.d.ts +85 -0
  32. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  33. package/dist/client/blocks/library/annotation-rail.js +149 -8
  34. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  35. package/dist/client/blocks/library/diagram.d.ts +17 -0
  36. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  37. package/dist/client/blocks/library/diagram.js +47 -2
  38. package/dist/client/blocks/library/diagram.js.map +1 -1
  39. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  40. package/dist/client/composer/TiptapComposer.js +13 -8
  41. package/dist/client/composer/TiptapComposer.js.map +1 -1
  42. package/dist/client/composer/pasted-text.d.ts +25 -0
  43. package/dist/client/composer/pasted-text.d.ts.map +1 -1
  44. package/dist/client/composer/pasted-text.js +86 -4
  45. package/dist/client/composer/pasted-text.js.map +1 -1
  46. package/dist/client/context-xray/ContextMeter.d.ts.map +1 -1
  47. package/dist/client/context-xray/ContextMeter.js +5 -3
  48. package/dist/client/context-xray/ContextMeter.js.map +1 -1
  49. package/dist/db/migrations.d.ts +10 -0
  50. package/dist/db/migrations.d.ts.map +1 -1
  51. package/dist/db/migrations.js +32 -0
  52. package/dist/db/migrations.js.map +1 -1
  53. package/dist/file-upload/builder.d.ts.map +1 -1
  54. package/dist/file-upload/builder.js +23 -8
  55. package/dist/file-upload/builder.js.map +1 -1
  56. package/dist/server/og-fonts-data.d.ts +3 -0
  57. package/dist/server/og-fonts-data.d.ts.map +1 -0
  58. package/dist/server/og-fonts-data.js +9 -0
  59. package/dist/server/og-fonts-data.js.map +1 -0
  60. package/dist/server/og-fonts.d.ts +10 -0
  61. package/dist/server/og-fonts.d.ts.map +1 -0
  62. package/dist/server/og-fonts.js +58 -0
  63. package/dist/server/og-fonts.js.map +1 -0
  64. package/dist/server/social-og-image.d.ts.map +1 -1
  65. package/dist/server/social-og-image.js +16 -5
  66. package/dist/server/social-og-image.js.map +1 -1
  67. package/dist/styles/blocks.css +111 -0
  68. package/dist/usage/store.d.ts +12 -0
  69. package/dist/usage/store.d.ts.map +1 -1
  70. package/dist/usage/store.js +35 -5
  71. package/dist/usage/store.js.map +1 -1
  72. package/dist/vite/client.d.ts.map +1 -1
  73. package/dist/vite/client.js +26 -3
  74. package/dist/vite/client.js.map +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ export declare const OG_FONT_FAMILY = "Liberation Sans";
2
+ /**
3
+ * Materialize the embedded OG fonts to disk and return their paths for resvg's
4
+ * `fontFiles` option. resvg 2.x only accepts file paths (no in-memory buffers),
5
+ * so the bytes are written once to a content-hashed tmp directory and cached
6
+ * for the lifetime of the process. Returns `undefined` if the fonts can't be
7
+ * written, letting the caller fall back to system fonts.
8
+ */
9
+ export declare function resolveOgFontFiles(): string[] | undefined;
10
+ //# sourceMappingURL=og-fonts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-fonts.d.ts","sourceRoot":"","sources":["../../src/server/og-fonts.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAIhD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,GAAG,SAAS,CA+BzD"}
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { LIBERATION_SANS_BOLD_BASE64, LIBERATION_SANS_REGULAR_BASE64, } from "./og-fonts-data.js";
6
+ /**
7
+ * Liberation Sans is the metric-compatible libre replacement for
8
+ * Arial/Helvetica that the OG image SVG asks for. It ships embedded as base64
9
+ * (see {@link ./og-fonts-data.ts}) so the renderer never depends on the host's
10
+ * system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial
11
+ * nor Inter, which previously left every `<text>` element rendering nothing.
12
+ */
13
+ const OG_FONT_FILES = [
14
+ {
15
+ filename: "LiberationSans-Regular.ttf",
16
+ base64: LIBERATION_SANS_REGULAR_BASE64,
17
+ },
18
+ { filename: "LiberationSans-Bold.ttf", base64: LIBERATION_SANS_BOLD_BASE64 },
19
+ ];
20
+ export const OG_FONT_FAMILY = "Liberation Sans";
21
+ let cachedFontFiles;
22
+ /**
23
+ * Materialize the embedded OG fonts to disk and return their paths for resvg's
24
+ * `fontFiles` option. resvg 2.x only accepts file paths (no in-memory buffers),
25
+ * so the bytes are written once to a content-hashed tmp directory and cached
26
+ * for the lifetime of the process. Returns `undefined` if the fonts can't be
27
+ * written, letting the caller fall back to system fonts.
28
+ */
29
+ export function resolveOgFontFiles() {
30
+ if (cachedFontFiles !== undefined)
31
+ return cachedFontFiles ?? undefined;
32
+ try {
33
+ const hash = createHash("sha256");
34
+ const decoded = OG_FONT_FILES.map((font) => {
35
+ const bytes = Buffer.from(font.base64, "base64");
36
+ if (!bytes.byteLength)
37
+ throw new Error(`empty font: ${font.filename}`);
38
+ hash.update(font.filename);
39
+ hash.update(bytes);
40
+ return { filename: font.filename, bytes };
41
+ });
42
+ const fontDir = path.join(tmpdir(), `agent-native-og-fonts-${hash.digest("hex").slice(0, 16)}`);
43
+ mkdirSync(fontDir, { recursive: true });
44
+ const fontFiles = decoded.map(({ filename, bytes }) => {
45
+ const target = path.join(fontDir, filename);
46
+ if (!existsSync(target))
47
+ writeFileSync(target, bytes);
48
+ return target;
49
+ });
50
+ cachedFontFiles = fontFiles;
51
+ return fontFiles;
52
+ }
53
+ catch {
54
+ cachedFontFiles = null;
55
+ return undefined;
56
+ }
57
+ }
58
+ //# sourceMappingURL=og-fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-fonts.js","sourceRoot":"","sources":["../../src/server/og-fonts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,aAAa,GAAG;IACpB;QACE,QAAQ,EAAE,4BAA4B;QACtC,MAAM,EAAE,8BAA8B;KACvC;IACD,EAAE,QAAQ,EAAE,yBAAyB,EAAE,MAAM,EAAE,2BAA2B,EAAE;CACpE,CAAC;AAEX,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEhD,IAAI,eAA4C,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,eAAe,KAAK,SAAS;QAAE,OAAO,eAAe,IAAI,SAAS,CAAC;IAEvE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YACjD,IAAI,CAAC,KAAK,CAAC,UAAU;gBAAE,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YACvE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CACvB,MAAM,EAAE,EACR,yBAAyB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAC3D,CAAC;QACF,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAExC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE;YACpD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC5C,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;gBAAE,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACtD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;QAEH,eAAe,GAAG,SAAS,CAAC;QAC5B,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,eAAe,GAAG,IAAI,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport {\n LIBERATION_SANS_BOLD_BASE64,\n LIBERATION_SANS_REGULAR_BASE64,\n} from \"./og-fonts-data.js\";\n\n/**\n * Liberation Sans is the metric-compatible libre replacement for\n * Arial/Helvetica that the OG image SVG asks for. It ships embedded as base64\n * (see {@link ./og-fonts-data.ts}) so the renderer never depends on the host's\n * system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial\n * nor Inter, which previously left every `<text>` element rendering nothing.\n */\nconst OG_FONT_FILES = [\n {\n filename: \"LiberationSans-Regular.ttf\",\n base64: LIBERATION_SANS_REGULAR_BASE64,\n },\n { filename: \"LiberationSans-Bold.ttf\", base64: LIBERATION_SANS_BOLD_BASE64 },\n] as const;\n\nexport const OG_FONT_FAMILY = \"Liberation Sans\";\n\nlet cachedFontFiles: string[] | null | undefined;\n\n/**\n * Materialize the embedded OG fonts to disk and return their paths for resvg's\n * `fontFiles` option. resvg 2.x only accepts file paths (no in-memory buffers),\n * so the bytes are written once to a content-hashed tmp directory and cached\n * for the lifetime of the process. Returns `undefined` if the fonts can't be\n * written, letting the caller fall back to system fonts.\n */\nexport function resolveOgFontFiles(): string[] | undefined {\n if (cachedFontFiles !== undefined) return cachedFontFiles ?? undefined;\n\n try {\n const hash = createHash(\"sha256\");\n const decoded = OG_FONT_FILES.map((font) => {\n const bytes = Buffer.from(font.base64, \"base64\");\n if (!bytes.byteLength) throw new Error(`empty font: ${font.filename}`);\n hash.update(font.filename);\n hash.update(bytes);\n return { filename: font.filename, bytes };\n });\n\n const fontDir = path.join(\n tmpdir(),\n `agent-native-og-fonts-${hash.digest(\"hex\").slice(0, 16)}`,\n );\n mkdirSync(fontDir, { recursive: true });\n\n const fontFiles = decoded.map(({ filename, bytes }) => {\n const target = path.join(fontDir, filename);\n if (!existsSync(target)) writeFileSync(target, bytes);\n return target;\n });\n\n cachedFontFiles = fontFiles;\n return fontFiles;\n } catch {\n cachedFontFiles = null;\n return undefined;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"social-og-image.d.ts","sourceRoot":"","sources":["../../src/server/social-og-image.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAChD,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAChD,eAAO,MAAM,mCAAmC,2EAC0B,CAAC;AAC3E,eAAO,MAAM,2CAA2C,oFAC2B,CAAC;AAuOpF,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQtE;AAED,wBAAgB,2BAA2B,CACzC,KAAK,GAAE,uBAA4B,GAClC,MAAM,CAsCR;AAED,wBAAsB,2BAA2B,CAC/C,KAAK,GAAE,uBAA4B,GAClC,OAAO,CAAC,UAAU,CAAC,CAWrB;AAED,wBAAgB,iCAAiC,CAC/C,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,SAAc,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYxB;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,uBAA4B,2FAqCtC"}
1
+ {"version":3,"file":"social-og-image.d.ts","sourceRoot":"","sources":["../../src/server/social-og-image.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAChD,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAChD,eAAO,MAAM,mCAAmC,2EAC0B,CAAC;AAC3E,eAAO,MAAM,2CAA2C,oFAC2B,CAAC;AAsOpF,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQtE;AAED,wBAAgB,2BAA2B,CACzC,KAAK,GAAE,uBAA4B,GAClC,MAAM,CAyCR;AAED,wBAAsB,2BAA2B,CAC/C,KAAK,GAAE,uBAA4B,GAClC,OAAO,CAAC,UAAU,CAAC,CAkBrB;AAED,wBAAgB,iCAAiC,CAC/C,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,SAAc,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYxB;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,uBAA4B,2FAqCtC"}
@@ -1,6 +1,7 @@
1
1
  import { defineEventHandler, getHeader, getMethod, getQuery, getRequestURL, } from "h3";
2
2
  import { resolveBuiltInAuthMarketing } from "./auth-marketing.js";
3
3
  import { getAppName } from "./app-name.js";
4
+ import { OG_FONT_FAMILY, resolveOgFontFiles } from "./og-fonts.js";
4
5
  export const AGENT_NATIVE_OG_IMAGE_WIDTH = 1200;
5
6
  export const AGENT_NATIVE_OG_IMAGE_HEIGHT = 630;
6
7
  export const AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL = "public, max-age=60, stale-while-revalidate=604800, stale-if-error=3600";
@@ -11,7 +12,7 @@ const BRAND_BLUE = "#00B5FF";
11
12
  const BRAND_MINT = "#48FFE4";
12
13
  const BG = "#000000";
13
14
  const FG = "#f5f5f5";
14
- const FONT_FAMILY = "Inter, Liberation Sans, Arial, Helvetica, system-ui, sans-serif";
15
+ const FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
15
16
  const DEFAULT_ACCENT_TEXT = "100% free and open source";
16
17
  const LOGO_MARK = `
17
18
  <path d="M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z" fill="white"/>
@@ -204,7 +205,10 @@ export function renderAgentNativeOgImageSvg(input = {}) {
204
205
  y: titleY,
205
206
  fontSize: titleLayout.fontSize,
206
207
  lineHeight: titleLayout.lineHeight,
207
- weight: 850,
208
+ // resvg's fontdb maps font-weight 850 to the Regular face (only 400/700
209
+ // exist for Liberation Sans); 800 resolves to Bold, the heaviest face we
210
+ // bundle, which is the intended look for the display title.
211
+ weight: 800,
208
212
  fill: FG,
209
213
  })}
210
214
  <text x="84" y="${accentY}" font-family="${FONT_FAMILY}" font-size="34" font-weight="800" fill="${BRAND_BLUE}">${escapeSvg(accentText)}</text>
@@ -213,12 +217,19 @@ export function renderAgentNativeOgImageSvg(input = {}) {
213
217
  }
214
218
  export async function renderAgentNativeOgImagePng(input = {}) {
215
219
  const { Resvg } = await import(/* @vite-ignore */ "@resvg/resvg-js");
220
+ // Feed resvg the embedded Liberation Sans font explicitly. System fonts can't
221
+ // be relied on: Linux serverless runtimes (Netlify/Lambda) ship neither Arial
222
+ // nor Inter, so without a bundled font every `<text>` rendered blank.
223
+ const fontFiles = resolveOgFontFiles();
224
+ const hasBundledFonts = Boolean(fontFiles?.length);
216
225
  const image = new Resvg(renderAgentNativeOgImageSvg(input), {
217
226
  fitTo: { mode: "width", value: WIDTH },
218
227
  font: {
219
- loadSystemFonts: true,
220
- defaultFontFamily: "Arial",
221
- sansSerifFamily: "Arial",
228
+ loadSystemFonts: !hasBundledFonts,
229
+ ...(hasBundledFonts ? { fontFiles } : {}),
230
+ defaultFontFamily: OG_FONT_FAMILY,
231
+ serifFamily: OG_FONT_FAMILY,
232
+ sansSerifFamily: OG_FONT_FAMILY,
222
233
  },
223
234
  }).render();
224
235
  return image.asPng();
@@ -1 +1 @@
1
- {"version":3,"file":"social-og-image.js","sourceRoot":"","sources":["../../src/server/social-og-image.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n defineEventHandler,\n getHeader,\n getMethod,\n getQuery,\n getRequestURL,\n type H3Event,\n} from \"h3\";\nimport { resolveBuiltInAuthMarketing } from \"./auth-marketing.js\";\nimport { getAppName } from \"./app-name.js\";\n\nexport interface AgentNativeOgImageInput {\n appName?: string | null;\n title?: string | null;\n accentText?: string | null;\n}\n\nexport const AGENT_NATIVE_OG_IMAGE_WIDTH = 1200;\nexport const AGENT_NATIVE_OG_IMAGE_HEIGHT = 630;\nexport const AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL =\n \"public, max-age=60, stale-while-revalidate=604800, stale-if-error=3600\";\nexport const AGENT_NATIVE_OG_IMAGE_NETLIFY_CACHE_CONTROL =\n \"public, durable, max-age=60, stale-while-revalidate=604800, stale-if-error=3600\";\n\nconst WIDTH = AGENT_NATIVE_OG_IMAGE_WIDTH;\nconst HEIGHT = AGENT_NATIVE_OG_IMAGE_HEIGHT;\nconst BRAND_BLUE = \"#00B5FF\";\nconst BRAND_MINT = \"#48FFE4\";\nconst BG = \"#000000\";\nconst FG = \"#f5f5f5\";\nconst FONT_FAMILY =\n \"Inter, Liberation Sans, Arial, Helvetica, system-ui, sans-serif\";\nconst DEFAULT_ACCENT_TEXT = \"100% free and open source\";\n\nconst LOGO_MARK = `\n <path d=\"M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z\" fill=\"white\"/>\n <path d=\"M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z\" fill=\"url(#brand)\"/>\n`;\n\nfunction escapeSvg(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nfunction cleanText(value: string | null | undefined): string {\n return String(value ?? \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction titleCase(value: string): string {\n return value\n .split(/[\\s._-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join(\" \");\n}\n\nfunction titleFromAppName(appName: string): string {\n if (appName) return appName;\n const basePath =\n process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || \"\";\n const slug = basePath.split(\"/\").filter(Boolean)[0] || \"\";\n return titleCase(slug) || \"Agent-Native\";\n}\n\ninterface WrappedText {\n lines: string[];\n truncated: boolean;\n}\n\ninterface TitleLayout {\n lines: string[];\n fontSize: number;\n lineHeight: number;\n}\n\nfunction estimateTextWidth(value: string, fontSize: number): number {\n let units = 0;\n for (const char of value) {\n if (char === \" \") {\n units += 0.28;\n } else if (/[MW@#%&]/.test(char)) {\n units += 0.86;\n } else if (/[A-Z]/.test(char)) {\n units += 0.64;\n } else if (/[ilI.,:;|!']/u.test(char)) {\n units += 0.26;\n } else if (/[0-9]/.test(char)) {\n units += 0.56;\n } else {\n units += 0.54;\n }\n }\n return units * fontSize;\n}\n\nfunction trimTextToWidth(\n value: string,\n fontSize: number,\n maxWidth: number,\n): string {\n const ellipsis = \"...\";\n let trimmed = value.trim();\n while (\n trimmed.length > 0 &&\n estimateTextWidth(`${trimmed}${ellipsis}`, fontSize) > maxWidth\n ) {\n trimmed = trimmed.slice(0, -1).trimEnd();\n }\n return trimmed ? `${trimmed}${ellipsis}` : ellipsis;\n}\n\nfunction wrapTextToWidth(\n value: string,\n fontSize: number,\n maxWidth: number,\n maxLines: number,\n): WrappedText {\n const words = value.split(/\\s+/).filter(Boolean);\n const lines: string[] = [];\n let current = \"\";\n let truncated = false;\n\n for (const word of words) {\n const next = current ? `${current} ${word}` : word;\n if (estimateTextWidth(next, fontSize) <= maxWidth) {\n current = next;\n continue;\n }\n if (!current) {\n lines.push(trimTextToWidth(word, fontSize, maxWidth));\n truncated = true;\n current = \"\";\n } else {\n lines.push(current);\n current = word;\n }\n if (lines.length === maxLines) {\n truncated = true;\n break;\n }\n }\n if (current && lines.length < maxLines) lines.push(current);\n\n const usedWordCount = lines.join(\" \").split(/\\s+/).filter(Boolean).length;\n if (usedWordCount < words.length && lines.length > 0) {\n lines[lines.length - 1] = trimTextToWidth(\n lines[lines.length - 1],\n fontSize,\n maxWidth,\n );\n truncated = true;\n }\n\n return {\n lines: lines.length ? lines : [trimTextToWidth(value, fontSize, maxWidth)],\n truncated,\n };\n}\n\nfunction getTitleLayout(title: string): TitleLayout {\n const maxTitleWidth = 900;\n if (estimateTextWidth(title, 88) <= maxTitleWidth) {\n return {\n lines: [title],\n fontSize: 88,\n lineHeight: 96,\n };\n }\n\n for (const fontSize of [76, 70, 64, 58, 52]) {\n const wrapped = wrapTextToWidth(title, fontSize, maxTitleWidth, 2);\n if (!wrapped.truncated) {\n const lineHeight = Math.round(fontSize * 1.1);\n return {\n lines: wrapped.lines,\n fontSize,\n lineHeight,\n };\n }\n }\n\n const fallbackFontSize = 52;\n const wrapped = wrapTextToWidth(title, fallbackFontSize, maxTitleWidth, 2);\n return {\n lines: wrapped.lines,\n fontSize: fallbackFontSize,\n lineHeight: 60,\n };\n}\n\nfunction textBlock({\n lines,\n x,\n y,\n fontSize,\n lineHeight,\n weight,\n fill,\n anchor = \"start\",\n}: {\n lines: string[];\n x: number;\n y: number;\n fontSize: number;\n lineHeight: number;\n weight: number;\n fill: string;\n anchor?: \"start\" | \"middle\";\n}): string {\n return `<text x=\"${x}\" y=\"${y}\" text-anchor=\"${anchor}\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" font-weight=\"${weight}\" fill=\"${fill}\">${lines\n .map(\n (line, index) =>\n `<tspan x=\"${x}\" dy=\"${index === 0 ? 0 : lineHeight}\">${escapeSvg(line)}</tspan>`,\n )\n .join(\"\")}</text>`;\n}\n\nfunction resolveDefaultAppName(event?: H3Event): string {\n const requestHost = event\n ? (getHeader(event, \"x-forwarded-host\") ?? getHeader(event, \"host\"))\n : undefined;\n const requestPath = event ? getRequestURL(event).pathname : undefined;\n return (\n getAppName() ??\n resolveBuiltInAuthMarketing({ requestHost, requestPath })?.appName ??\n \"Agent-Native\"\n );\n}\n\nfunction queryStringValue(\n value: unknown,\n maxLength: number,\n): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const clean = cleanText(value).slice(0, maxLength);\n return clean || undefined;\n}\n\nfunction pngBody(bytes: Uint8Array): ArrayBuffer {\n const body = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(body).set(bytes);\n return body;\n}\n\nfunction textByteLength(value: string): number {\n return new TextEncoder().encode(value).byteLength;\n}\n\nexport function isResvgRuntimeUnavailableError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error ?? \"\");\n return (\n /@resvg\\/resvg-js|resvgjs\\.[\\w-]+\\.node|native binding/i.test(message) &&\n /cannot find|err_module_not_found|dlopen|invalid elf|wrong architecture|not a valid win32|native binding/i.test(\n message,\n )\n );\n}\n\nexport function renderAgentNativeOgImageSvg(\n input: AgentNativeOgImageInput = {},\n): string {\n const appName = cleanText(input.appName) || resolveDefaultAppName();\n const title = cleanText(input.title) || titleFromAppName(appName);\n const accentText = cleanText(input.accentText) || DEFAULT_ACCENT_TEXT;\n const titleLayout = getTitleLayout(title);\n const titleY = titleLayout.lines.length > 1 ? 288 : 330;\n const accentY =\n titleY + titleLayout.lineHeight * (titleLayout.lines.length - 1) + 70;\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${WIDTH}\" height=\"${HEIGHT}\" viewBox=\"0 0 ${WIDTH} ${HEIGHT}\">\n <title>${escapeSvg(title)} - Agent-Native preview</title>\n <defs>\n <linearGradient id=\"brand\" x1=\"101.702\" y1=\"67.4791\" x2=\"113.672\" y2=\"-37.4275\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"${BRAND_BLUE}\"/>\n <stop offset=\"1\" stop-color=\"${BRAND_MINT}\"/>\n </linearGradient>\n <pattern id=\"grid\" width=\"48\" height=\"48\" patternUnits=\"userSpaceOnUse\">\n <path d=\"M 48 0 L 0 0 0 48\" fill=\"none\" stroke=\"#ffffff\" stroke-opacity=\"0.07\" stroke-width=\"1\"/>\n </pattern>\n </defs>\n <rect width=\"${WIDTH}\" height=\"${HEIGHT}\" fill=\"${BG}\"/>\n <rect width=\"${WIDTH}\" height=\"${HEIGHT}\" fill=\"url(#grid)\"/>\n <g transform=\"translate(80 116) scale(0.94)\">\n ${LOGO_MARK}\n </g>\n <g>\n ${textBlock({\n lines: titleLayout.lines,\n x: 80,\n y: titleY,\n fontSize: titleLayout.fontSize,\n lineHeight: titleLayout.lineHeight,\n weight: 850,\n fill: FG,\n })}\n <text x=\"84\" y=\"${accentY}\" font-family=\"${FONT_FAMILY}\" font-size=\"34\" font-weight=\"800\" fill=\"${BRAND_BLUE}\">${escapeSvg(accentText)}</text>\n </g>\n</svg>`;\n}\n\nexport async function renderAgentNativeOgImagePng(\n input: AgentNativeOgImageInput = {},\n): Promise<Uint8Array> {\n const { Resvg } = await import(/* @vite-ignore */ \"@resvg/resvg-js\");\n const image = new Resvg(renderAgentNativeOgImageSvg(input), {\n fitTo: { mode: \"width\", value: WIDTH },\n font: {\n loadSystemFonts: true,\n defaultFontFamily: \"Arial\",\n sansSerifFamily: \"Arial\",\n },\n }).render();\n return image.asPng();\n}\n\nexport function agentNativeOgImageResponseHeaders(\n byteLength?: number,\n contentType = \"image/png\",\n): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": contentType,\n \"Cache-Control\": AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL,\n \"CDN-Cache-Control\": AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL,\n \"Netlify-CDN-Cache-Control\": AGENT_NATIVE_OG_IMAGE_NETLIFY_CACHE_CONTROL,\n \"Cross-Origin-Resource-Policy\": \"cross-origin\",\n };\n if (typeof byteLength === \"number\") {\n headers[\"Content-Length\"] = String(byteLength);\n }\n return headers;\n}\n\nexport function createAgentNativeOgImageHandler(\n options: AgentNativeOgImageInput = {},\n) {\n return defineEventHandler(async (event) => {\n if (getMethod(event) === \"HEAD\") {\n return new Response(null, {\n headers: agentNativeOgImageResponseHeaders(),\n });\n }\n\n const query = getQuery(event);\n const appName = cleanText(options.appName) || resolveDefaultAppName(event);\n const input = {\n ...options,\n appName,\n title: cleanText(options.title) || queryStringValue(query.title, 140),\n accentText:\n cleanText(options.accentText) || queryStringValue(query.accentText, 80),\n };\n\n let png: Uint8Array;\n try {\n png = await renderAgentNativeOgImagePng(input);\n } catch (error) {\n if (!isResvgRuntimeUnavailableError(error)) throw error;\n const svg = renderAgentNativeOgImageSvg(input);\n return new Response(svg, {\n headers: agentNativeOgImageResponseHeaders(\n textByteLength(svg),\n \"image/svg+xml; charset=utf-8\",\n ),\n });\n }\n\n return new Response(pngBody(png), {\n headers: agentNativeOgImageResponseHeaders(png.byteLength),\n });\n });\n}\n"]}
1
+ {"version":3,"file":"social-og-image.js","sourceRoot":"","sources":["../../src/server/social-og-image.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n defineEventHandler,\n getHeader,\n getMethod,\n getQuery,\n getRequestURL,\n type H3Event,\n} from \"h3\";\nimport { resolveBuiltInAuthMarketing } from \"./auth-marketing.js\";\nimport { getAppName } from \"./app-name.js\";\nimport { OG_FONT_FAMILY, resolveOgFontFiles } from \"./og-fonts.js\";\n\nexport interface AgentNativeOgImageInput {\n appName?: string | null;\n title?: string | null;\n accentText?: string | null;\n}\n\nexport const AGENT_NATIVE_OG_IMAGE_WIDTH = 1200;\nexport const AGENT_NATIVE_OG_IMAGE_HEIGHT = 630;\nexport const AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL =\n \"public, max-age=60, stale-while-revalidate=604800, stale-if-error=3600\";\nexport const AGENT_NATIVE_OG_IMAGE_NETLIFY_CACHE_CONTROL =\n \"public, durable, max-age=60, stale-while-revalidate=604800, stale-if-error=3600\";\n\nconst WIDTH = AGENT_NATIVE_OG_IMAGE_WIDTH;\nconst HEIGHT = AGENT_NATIVE_OG_IMAGE_HEIGHT;\nconst BRAND_BLUE = \"#00B5FF\";\nconst BRAND_MINT = \"#48FFE4\";\nconst BG = \"#000000\";\nconst FG = \"#f5f5f5\";\nconst FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;\nconst DEFAULT_ACCENT_TEXT = \"100% free and open source\";\n\nconst LOGO_MARK = `\n <path d=\"M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z\" fill=\"white\"/>\n <path d=\"M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z\" fill=\"url(#brand)\"/>\n`;\n\nfunction escapeSvg(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nfunction cleanText(value: string | null | undefined): string {\n return String(value ?? \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction titleCase(value: string): string {\n return value\n .split(/[\\s._-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join(\" \");\n}\n\nfunction titleFromAppName(appName: string): string {\n if (appName) return appName;\n const basePath =\n process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || \"\";\n const slug = basePath.split(\"/\").filter(Boolean)[0] || \"\";\n return titleCase(slug) || \"Agent-Native\";\n}\n\ninterface WrappedText {\n lines: string[];\n truncated: boolean;\n}\n\ninterface TitleLayout {\n lines: string[];\n fontSize: number;\n lineHeight: number;\n}\n\nfunction estimateTextWidth(value: string, fontSize: number): number {\n let units = 0;\n for (const char of value) {\n if (char === \" \") {\n units += 0.28;\n } else if (/[MW@#%&]/.test(char)) {\n units += 0.86;\n } else if (/[A-Z]/.test(char)) {\n units += 0.64;\n } else if (/[ilI.,:;|!']/u.test(char)) {\n units += 0.26;\n } else if (/[0-9]/.test(char)) {\n units += 0.56;\n } else {\n units += 0.54;\n }\n }\n return units * fontSize;\n}\n\nfunction trimTextToWidth(\n value: string,\n fontSize: number,\n maxWidth: number,\n): string {\n const ellipsis = \"...\";\n let trimmed = value.trim();\n while (\n trimmed.length > 0 &&\n estimateTextWidth(`${trimmed}${ellipsis}`, fontSize) > maxWidth\n ) {\n trimmed = trimmed.slice(0, -1).trimEnd();\n }\n return trimmed ? `${trimmed}${ellipsis}` : ellipsis;\n}\n\nfunction wrapTextToWidth(\n value: string,\n fontSize: number,\n maxWidth: number,\n maxLines: number,\n): WrappedText {\n const words = value.split(/\\s+/).filter(Boolean);\n const lines: string[] = [];\n let current = \"\";\n let truncated = false;\n\n for (const word of words) {\n const next = current ? `${current} ${word}` : word;\n if (estimateTextWidth(next, fontSize) <= maxWidth) {\n current = next;\n continue;\n }\n if (!current) {\n lines.push(trimTextToWidth(word, fontSize, maxWidth));\n truncated = true;\n current = \"\";\n } else {\n lines.push(current);\n current = word;\n }\n if (lines.length === maxLines) {\n truncated = true;\n break;\n }\n }\n if (current && lines.length < maxLines) lines.push(current);\n\n const usedWordCount = lines.join(\" \").split(/\\s+/).filter(Boolean).length;\n if (usedWordCount < words.length && lines.length > 0) {\n lines[lines.length - 1] = trimTextToWidth(\n lines[lines.length - 1],\n fontSize,\n maxWidth,\n );\n truncated = true;\n }\n\n return {\n lines: lines.length ? lines : [trimTextToWidth(value, fontSize, maxWidth)],\n truncated,\n };\n}\n\nfunction getTitleLayout(title: string): TitleLayout {\n const maxTitleWidth = 900;\n if (estimateTextWidth(title, 88) <= maxTitleWidth) {\n return {\n lines: [title],\n fontSize: 88,\n lineHeight: 96,\n };\n }\n\n for (const fontSize of [76, 70, 64, 58, 52]) {\n const wrapped = wrapTextToWidth(title, fontSize, maxTitleWidth, 2);\n if (!wrapped.truncated) {\n const lineHeight = Math.round(fontSize * 1.1);\n return {\n lines: wrapped.lines,\n fontSize,\n lineHeight,\n };\n }\n }\n\n const fallbackFontSize = 52;\n const wrapped = wrapTextToWidth(title, fallbackFontSize, maxTitleWidth, 2);\n return {\n lines: wrapped.lines,\n fontSize: fallbackFontSize,\n lineHeight: 60,\n };\n}\n\nfunction textBlock({\n lines,\n x,\n y,\n fontSize,\n lineHeight,\n weight,\n fill,\n anchor = \"start\",\n}: {\n lines: string[];\n x: number;\n y: number;\n fontSize: number;\n lineHeight: number;\n weight: number;\n fill: string;\n anchor?: \"start\" | \"middle\";\n}): string {\n return `<text x=\"${x}\" y=\"${y}\" text-anchor=\"${anchor}\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" font-weight=\"${weight}\" fill=\"${fill}\">${lines\n .map(\n (line, index) =>\n `<tspan x=\"${x}\" dy=\"${index === 0 ? 0 : lineHeight}\">${escapeSvg(line)}</tspan>`,\n )\n .join(\"\")}</text>`;\n}\n\nfunction resolveDefaultAppName(event?: H3Event): string {\n const requestHost = event\n ? (getHeader(event, \"x-forwarded-host\") ?? getHeader(event, \"host\"))\n : undefined;\n const requestPath = event ? getRequestURL(event).pathname : undefined;\n return (\n getAppName() ??\n resolveBuiltInAuthMarketing({ requestHost, requestPath })?.appName ??\n \"Agent-Native\"\n );\n}\n\nfunction queryStringValue(\n value: unknown,\n maxLength: number,\n): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const clean = cleanText(value).slice(0, maxLength);\n return clean || undefined;\n}\n\nfunction pngBody(bytes: Uint8Array): ArrayBuffer {\n const body = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(body).set(bytes);\n return body;\n}\n\nfunction textByteLength(value: string): number {\n return new TextEncoder().encode(value).byteLength;\n}\n\nexport function isResvgRuntimeUnavailableError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error ?? \"\");\n return (\n /@resvg\\/resvg-js|resvgjs\\.[\\w-]+\\.node|native binding/i.test(message) &&\n /cannot find|err_module_not_found|dlopen|invalid elf|wrong architecture|not a valid win32|native binding/i.test(\n message,\n )\n );\n}\n\nexport function renderAgentNativeOgImageSvg(\n input: AgentNativeOgImageInput = {},\n): string {\n const appName = cleanText(input.appName) || resolveDefaultAppName();\n const title = cleanText(input.title) || titleFromAppName(appName);\n const accentText = cleanText(input.accentText) || DEFAULT_ACCENT_TEXT;\n const titleLayout = getTitleLayout(title);\n const titleY = titleLayout.lines.length > 1 ? 288 : 330;\n const accentY =\n titleY + titleLayout.lineHeight * (titleLayout.lines.length - 1) + 70;\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${WIDTH}\" height=\"${HEIGHT}\" viewBox=\"0 0 ${WIDTH} ${HEIGHT}\">\n <title>${escapeSvg(title)} - Agent-Native preview</title>\n <defs>\n <linearGradient id=\"brand\" x1=\"101.702\" y1=\"67.4791\" x2=\"113.672\" y2=\"-37.4275\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"${BRAND_BLUE}\"/>\n <stop offset=\"1\" stop-color=\"${BRAND_MINT}\"/>\n </linearGradient>\n <pattern id=\"grid\" width=\"48\" height=\"48\" patternUnits=\"userSpaceOnUse\">\n <path d=\"M 48 0 L 0 0 0 48\" fill=\"none\" stroke=\"#ffffff\" stroke-opacity=\"0.07\" stroke-width=\"1\"/>\n </pattern>\n </defs>\n <rect width=\"${WIDTH}\" height=\"${HEIGHT}\" fill=\"${BG}\"/>\n <rect width=\"${WIDTH}\" height=\"${HEIGHT}\" fill=\"url(#grid)\"/>\n <g transform=\"translate(80 116) scale(0.94)\">\n ${LOGO_MARK}\n </g>\n <g>\n ${textBlock({\n lines: titleLayout.lines,\n x: 80,\n y: titleY,\n fontSize: titleLayout.fontSize,\n lineHeight: titleLayout.lineHeight,\n // resvg's fontdb maps font-weight 850 to the Regular face (only 400/700\n // exist for Liberation Sans); 800 resolves to Bold, the heaviest face we\n // bundle, which is the intended look for the display title.\n weight: 800,\n fill: FG,\n })}\n <text x=\"84\" y=\"${accentY}\" font-family=\"${FONT_FAMILY}\" font-size=\"34\" font-weight=\"800\" fill=\"${BRAND_BLUE}\">${escapeSvg(accentText)}</text>\n </g>\n</svg>`;\n}\n\nexport async function renderAgentNativeOgImagePng(\n input: AgentNativeOgImageInput = {},\n): Promise<Uint8Array> {\n const { Resvg } = await import(/* @vite-ignore */ \"@resvg/resvg-js\");\n // Feed resvg the embedded Liberation Sans font explicitly. System fonts can't\n // be relied on: Linux serverless runtimes (Netlify/Lambda) ship neither Arial\n // nor Inter, so without a bundled font every `<text>` rendered blank.\n const fontFiles = resolveOgFontFiles();\n const hasBundledFonts = Boolean(fontFiles?.length);\n const image = new Resvg(renderAgentNativeOgImageSvg(input), {\n fitTo: { mode: \"width\", value: WIDTH },\n font: {\n loadSystemFonts: !hasBundledFonts,\n ...(hasBundledFonts ? { fontFiles } : {}),\n defaultFontFamily: OG_FONT_FAMILY,\n serifFamily: OG_FONT_FAMILY,\n sansSerifFamily: OG_FONT_FAMILY,\n },\n }).render();\n return image.asPng();\n}\n\nexport function agentNativeOgImageResponseHeaders(\n byteLength?: number,\n contentType = \"image/png\",\n): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": contentType,\n \"Cache-Control\": AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL,\n \"CDN-Cache-Control\": AGENT_NATIVE_OG_IMAGE_CACHE_CONTROL,\n \"Netlify-CDN-Cache-Control\": AGENT_NATIVE_OG_IMAGE_NETLIFY_CACHE_CONTROL,\n \"Cross-Origin-Resource-Policy\": \"cross-origin\",\n };\n if (typeof byteLength === \"number\") {\n headers[\"Content-Length\"] = String(byteLength);\n }\n return headers;\n}\n\nexport function createAgentNativeOgImageHandler(\n options: AgentNativeOgImageInput = {},\n) {\n return defineEventHandler(async (event) => {\n if (getMethod(event) === \"HEAD\") {\n return new Response(null, {\n headers: agentNativeOgImageResponseHeaders(),\n });\n }\n\n const query = getQuery(event);\n const appName = cleanText(options.appName) || resolveDefaultAppName(event);\n const input = {\n ...options,\n appName,\n title: cleanText(options.title) || queryStringValue(query.title, 140),\n accentText:\n cleanText(options.accentText) || queryStringValue(query.accentText, 80),\n };\n\n let png: Uint8Array;\n try {\n png = await renderAgentNativeOgImagePng(input);\n } catch (error) {\n if (!isResvgRuntimeUnavailableError(error)) throw error;\n const svg = renderAgentNativeOgImageSvg(input);\n return new Response(svg, {\n headers: agentNativeOgImageResponseHeaders(\n textByteLength(svg),\n \"image/svg+xml; charset=utf-8\",\n ),\n });\n }\n\n return new Response(pngBody(png), {\n headers: agentNativeOgImageResponseHeaders(png.byteLength),\n });\n });\n}\n"]}
@@ -31,6 +31,22 @@
31
31
  large prose size. The plan template overrides this var in its own
32
32
  `global.css`; this is the cross-app default. */
33
33
  --plan-code-size: 0.75rem;
34
+
35
+ /* AUTHORITATIVE code-body size for DENSE in-document code surfaces (diff rows,
36
+ annotated-code lines, API JSON examples, code-tabs, the `code` primitive
37
+ read surface). The catch-all below forces every such surface to this size
38
+ with `!important`, so dense document code is sized from ONE place no matter
39
+ which component renders it or what inline/utility/template size it would
40
+ otherwise pick up.
41
+
42
+ This is deliberately DECOUPLED from `--plan-code-size`: the plan template
43
+ raises `--plan-code-size` to 1rem for its free-standing prose-scale code,
44
+ which made dense surfaces (annotated diffs, API JSON) read far too large —
45
+ the user's recurring complaint. By pinning dense code to its own small,
46
+ comfortable size here, those surfaces stay compact and scannable in every
47
+ app while a free-standing snippet can still follow `--plan-code-size`.
48
+ Resize ALL dense document code at once by overriding this var. */
49
+ --plan-doc-code-size: 0.8125rem;
34
50
  }
35
51
 
36
52
  .dark {
@@ -41,6 +57,101 @@
41
57
  --an-callout-success: 142 60% 52%;
42
58
  }
43
59
 
60
+ /* ════════════════════════════════════════════════════════════════════════
61
+ AUTHORITATIVE code-surface font-size catch-all.
62
+
63
+ THE single source of truth for how big monospace code renders inside plan
64
+ document content. Historically the size was set per-surface — a Tailwind
65
+ arbitrary `[font-size:var(--plan-code-size)]` on diff/annotated-code rows
66
+ (specificity 0,1,0), the plan template's `.plan-code-surface pre` rule
67
+ (0,2,0), the editor node-view `.an-rich-md-prose .an-code-block pre` rule
68
+ (0,3,0), etc. Because those live in different files at different specificities
69
+ (and the plan template raises `--plan-code-size` to 1rem), one surface could
70
+ render large while another stayed small: the recurring "code is too big in
71
+ the annotated diff / API JSON" whack-a-mole.
72
+
73
+ This rule ends it. It scopes to plan document content via a ZERO-specificity
74
+ `:where()` wrapper, then targets EVERY code surface — shiki/lowlight `<pre>`
75
+ and `<code>`, the `.plan-shiki` wrapper, the shared `.plan-code-surface`
76
+ scroll body, the editor `.an-code-block`, and an explicit
77
+ `[data-code-surface]` hook the dense block components stamp on their code
78
+ containers — and forces `font-size: var(--plan-doc-code-size)` with
79
+ `!important`. The `!important` + the `[data-code-surface]` attribute leaf
80
+ (specificity contribution 0,1,0 inside `:is()`, but `!important` is what
81
+ decides the cascade) guarantees it WINS over every per-surface size above,
82
+ including the plan template's later-loaded `.plan-code-surface pre` and the
83
+ inline Tailwind arbitrary class — no surface can escape it. Free-standing
84
+ prose code (inline `code` in body copy) is intentionally NOT in scope: it is
85
+ matched only when it sits inside one of the code-surface containers.
86
+
87
+ Tune `--plan-doc-code-size` (here or per-app) to resize ALL dense document
88
+ code at once. ════════════════════════════════════════════════════════════ */
89
+ :where(.plan-document-flow, .plan-block, .plan-block-node)
90
+ :is(
91
+ [data-code-surface],
92
+ [data-code-surface] pre,
93
+ [data-code-surface] code,
94
+ [data-code-surface] .plan-shiki,
95
+ .plan-code-surface,
96
+ .plan-code-surface pre,
97
+ .plan-code-surface code,
98
+ .plan-code-surface .plan-shiki,
99
+ .plan-code-surface .plan-code-surface-scroll,
100
+ .an-code-block,
101
+ .an-code-block pre,
102
+ .an-code-block code
103
+ ) {
104
+ font-size: var(--plan-doc-code-size) !important;
105
+ }
106
+
107
+ /* Syntax-token spans nested INSIDE a highlighted `<pre>`/`<code>` (shiki +
108
+ lowlight wrap every token in a `<span>`) inherit the forced size, so a stray
109
+ per-token size can never reintroduce a large glyph. Scoped to spans under
110
+ pre/code only — NOT every span in the surface — so the in-code annotation
111
+ marker pips (their own tiny `text-[9px]`) and other chrome keep their size. */
112
+ :where(.plan-document-flow, .plan-block, .plan-block-node)
113
+ :is([data-code-surface], .plan-code-surface, .an-code-block)
114
+ :is(pre, code)
115
+ span {
116
+ font-size: inherit !important;
117
+ }
118
+
119
+ /* Universal guarantee (NOT gated on a plan-document ancestor): the dense code
120
+ block components — annotated-code, diff, api-endpoint — stamp
121
+ `data-code-surface` / `.an-code-block` on their code containers. Force the
122
+ dense size on those WHEREVER they render, so the standard annotated/diff
123
+ component can never show oversized code on any surface (embedded panels,
124
+ non-plan-document hosts, etc.), not just inside `.plan-document-flow`. The
125
+ `var(--plan-doc-code-size, 0.8125rem)` fallback covers hosts that don't define
126
+ the token. */
127
+ :is(
128
+ [data-code-surface],
129
+ [data-code-surface] pre,
130
+ [data-code-surface] code,
131
+ [data-code-surface] .plan-shiki,
132
+ .an-code-block,
133
+ .an-code-block pre,
134
+ .an-code-block code
135
+ ) {
136
+ font-size: var(--plan-doc-code-size, 0.8125rem) !important;
137
+ }
138
+ :is([data-code-surface], .an-code-block) :is(pre, code) span {
139
+ font-size: inherit !important;
140
+ }
141
+
142
+ /* COMPREHENSIVE NET — the final backstop so "code is too big" can't reappear in
143
+ a NEW place. Every code/data/JSON component opts into monospace via the
144
+ `.font-mono` utility (12+ block components, incl. the JSON-explorer tree which
145
+ renders as div/span — NOT pre/code — and so escaped every earlier rule). Force
146
+ the dense doc-code size on any `.font-mono` inside plan content, and on the
147
+ code-block component hooks wherever they render. Annotation marker pips are NOT
148
+ font-mono, so they keep their tiny size; inline prose `code` is styled by the
149
+ prose plugin (no `.font-mono` class), so body copy is unaffected. */
150
+ :where(.plan-document-flow, .plan-block, .plan-block-node) .font-mono,
151
+ :is([data-code-surface], .an-code-block) .font-mono {
152
+ font-size: var(--plan-doc-code-size, 0.8125rem) !important;
153
+ }
154
+
44
155
  /* Small, muted eyebrow label above a block (block title). */
45
156
  .an-block-label {
46
157
  margin: 0 0 1rem;
@@ -24,6 +24,18 @@ export interface UsageRecord {
24
24
  label?: string;
25
25
  /** Optional template/app name (e.g. "mail"). Falls back to AGENT_APP / APP_NAME env. */
26
26
  app?: string;
27
+ /**
28
+ * Stable id of the thing this usage belongs to (e.g. a recap plan id). When
29
+ * set, any prior row(s) with the same (label, refId) are deleted before
30
+ * insert, so re-recording the same run overwrites instead of double-counting.
31
+ */
32
+ refId?: string;
33
+ /**
34
+ * Precomputed cost in centicents (1/100¢). When provided, it is stored
35
+ * verbatim instead of being derived from tokens — e.g. to mirror a
36
+ * provider-reported dollar cost so two surfaces agree exactly.
37
+ */
38
+ costCentsX100?: number;
27
39
  }
28
40
  /**
29
41
  * Calculate cost in centicents (1/100th of a cent).
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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"}
@@ -47,6 +47,20 @@ const PRICING = [
47
47
  match: /haiku/i,
48
48
  pricing: { input: 100, output: 500, cacheRead: 10, cacheWrite: 125 },
49
49
  },
50
+ // OpenAI / Codex models (cents per 1M tokens). Without these, a Codex recap
51
+ // model like "gpt-5.5" falls through to the default (Sonnet) row and is
52
+ // mispriced. Published rates as of 2026-06; OpenAI bills cached input at a
53
+ // discount and has no separate cache-write token charge, so cacheWrite is 0
54
+ // (recap usage passes 0 cache-write anyway). The /gpt-5\.5/ row must precede
55
+ // /gpt-5/ since the latter also matches "gpt-5.5".
56
+ {
57
+ match: /gpt-5\.5/i,
58
+ pricing: { input: 500, output: 3000, cacheRead: 50, cacheWrite: 0 },
59
+ },
60
+ {
61
+ match: /gpt-5/i,
62
+ pricing: { input: 125, output: 1000, cacheRead: 12.5, cacheWrite: 0 },
63
+ },
50
64
  // default → sonnet pricing
51
65
  {
52
66
  match: /.*/,
@@ -77,6 +91,7 @@ async function ensureUsageTable() {
77
91
  model TEXT NOT NULL DEFAULT '',
78
92
  label TEXT NOT NULL DEFAULT 'chat',
79
93
  app TEXT NOT NULL DEFAULT '',
94
+ ref_id TEXT NOT NULL DEFAULT '',
80
95
  created_at ${intType()} NOT NULL
81
96
  )
82
97
  `);
@@ -88,6 +103,7 @@ async function ensureUsageTable() {
88
103
  ["cache_write_tokens", `${intType()} NOT NULL DEFAULT 0`],
89
104
  ["label", `TEXT NOT NULL DEFAULT 'chat'`],
90
105
  ["app", `TEXT NOT NULL DEFAULT ''`],
106
+ ["ref_id", `TEXT NOT NULL DEFAULT ''`],
91
107
  ];
92
108
  for (const [col, def] of additions) {
93
109
  try {
@@ -136,20 +152,33 @@ export async function recordUsage(recordOrOwner, inputTokens, outputTokens, mode
136
152
  model: model ?? "",
137
153
  }
138
154
  : recordOrOwner;
139
- const { ownerEmail, inputTokens: inTok, outputTokens: outTok, cacheReadTokens = 0, cacheWriteTokens = 0, model: modelName, label, app, } = record;
155
+ const { ownerEmail, inputTokens: inTok, outputTokens: outTok, cacheReadTokens = 0, cacheWriteTokens = 0, model: modelName, label, app, refId, costCentsX100, } = record;
140
156
  // Skip no-op writes (e.g. a stream aborted before any tokens flowed)
141
157
  if (!inTok && !outTok && !cacheReadTokens && !cacheWriteTokens)
142
158
  return;
143
159
  await ensureUsageTable();
144
160
  const client = getDbExec();
145
- const costX100 = calculateCost(inTok, outTok, modelName, cacheReadTokens, cacheWriteTokens);
146
- const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);
147
161
  const resolvedApp = app ?? process.env.AGENT_APP ?? process.env.APP_NAME ?? "";
148
162
  const resolvedLabel = label ?? "chat";
163
+ const resolvedRef = refId ?? "";
164
+ // Replace any prior usage for this (label, refId) so re-recording the same
165
+ // run — e.g. a recap regenerated on a PR re-push — overwrites instead of
166
+ // double-counting. No-op when refId is unset (the common per-call path).
167
+ if (resolvedRef) {
168
+ await client.execute({
169
+ sql: `DELETE FROM token_usage WHERE label = ? AND ref_id = ?`,
170
+ args: [resolvedLabel, resolvedRef],
171
+ });
172
+ }
173
+ // Prefer an explicit precomputed cost (e.g. a provider-reported dollar cost);
174
+ // otherwise derive it from tokens via the pricing table.
175
+ const costX100 = costCentsX100 ??
176
+ calculateCost(inTok, outTok, modelName, cacheReadTokens, cacheWriteTokens);
177
+ const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);
149
178
  await client.execute({
150
179
  sql: `INSERT INTO token_usage
151
- (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, created_at)
152
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
180
+ (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, ref_id, created_at)
181
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
153
182
  args: [
154
183
  id,
155
184
  ownerEmail,
@@ -161,6 +190,7 @@ export async function recordUsage(recordOrOwner, inputTokens, outputTokens, mode
161
190
  modelName,
162
191
  resolvedLabel,
163
192
  resolvedApp,
193
+ resolvedRef,
164
194
  Date.now(),
165
195
  ],
166
196
  });
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Token usage tracking and cost monitoring.\n *\n * Every LLM call made by the framework records a row here so users can\n * see where their spend is going — chat vs automations vs background jobs\n * vs whatever else a template labels its prompts as.\n *\n * Cost is stored as \"centicents\" (1/100th of a cent) for integer precision.\n */\nimport { getDbExec, intType, isPostgres } from \"../db/client.js\";\n\n/**\n * Per-million-token pricing in cents. Cache read is typically ~10% of\n * input; cache write (5m TTL) is ~125%. Pricing is best-effort — keep\n * this table in sync with Anthropic's published prices.\n */\ninterface ModelPricing {\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n}\n\nexport const BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER = 1.25;\nexport const BUILDER_AGENT_CREDITS_PER_USD = 20;\n\nexport type UsageBillingUnit = \"usd\" | \"builder-credits\";\n\nexport interface UsageBillingMode {\n unit: UsageBillingUnit;\n label: string;\n shortLabel: string;\n source: \"estimated-provider-cost\" | \"builder-agent-credits\";\n hardCostMarginMultiplier?: number;\n creditsPerUsd?: number;\n}\n\nexport const USD_USAGE_BILLING: UsageBillingMode = {\n unit: \"usd\",\n label: \"Estimated spend\",\n shortLabel: \"Cost\",\n source: \"estimated-provider-cost\",\n};\n\nexport const BUILDER_CREDIT_USAGE_BILLING: UsageBillingMode = {\n unit: \"builder-credits\",\n label: \"Builder.io credit spend\",\n shortLabel: \"Credits\",\n source: \"builder-agent-credits\",\n hardCostMarginMultiplier: BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER,\n creditsPerUsd: BUILDER_AGENT_CREDITS_PER_USD,\n};\n\nexport function usageBillingForEngine(\n engineName: string | null | undefined,\n): UsageBillingMode {\n return engineName === \"builder\"\n ? BUILDER_CREDIT_USAGE_BILLING\n : USD_USAGE_BILLING;\n}\n\nexport function builderCreditsFromCostCents(cents: number): number {\n if (!Number.isFinite(cents) || cents <= 0) return 0;\n const dollars = cents / 100;\n const credits =\n dollars *\n BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER *\n BUILDER_AGENT_CREDITS_PER_USD;\n return Math.ceil(credits * 1000) / 1000;\n}\n\nconst PRICING: Array<{ match: RegExp; pricing: ModelPricing }> = [\n {\n match: /opus/i,\n pricing: { input: 1500, output: 7500, cacheRead: 150, cacheWrite: 1875 },\n },\n {\n match: /haiku/i,\n pricing: { input: 100, output: 500, cacheRead: 10, cacheWrite: 125 },\n },\n // default → sonnet pricing\n {\n match: /.*/,\n pricing: { input: 300, output: 1500, cacheRead: 30, cacheWrite: 375 },\n },\n];\n\nfunction pricingFor(model: string): ModelPricing {\n for (const entry of PRICING) {\n if (entry.match.test(model)) return entry.pricing;\n }\n return PRICING[PRICING.length - 1]!.pricing;\n}\n\nexport interface UsageRecord {\n ownerEmail: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n model: string;\n /** Category for this call — e.g. \"chat\", \"automation\", \"job\", \"custom-agent\". */\n label?: string;\n /** Optional template/app name (e.g. \"mail\"). Falls back to AGENT_APP / APP_NAME env. */\n app?: string;\n}\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureUsageTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS token_usage (\n id ${intType()} PRIMARY KEY,\n owner_email TEXT NOT NULL,\n input_tokens ${intType()} NOT NULL DEFAULT 0,\n output_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_read_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_write_tokens ${intType()} NOT NULL DEFAULT 0,\n cost_cents_x100 ${intType()} NOT NULL DEFAULT 0,\n model TEXT NOT NULL DEFAULT '',\n label TEXT NOT NULL DEFAULT 'chat',\n app TEXT NOT NULL DEFAULT '',\n created_at ${intType()} NOT NULL\n )\n `);\n\n // Add columns on older deployments that pre-date the label/cache\n // fields. Each ALTER is wrapped so a dialect without IF NOT EXISTS\n // (SQLite) still makes progress if only some columns are missing.\n const additions: Array<[string, string]> = [\n [\"cache_read_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"cache_write_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"label\", `TEXT NOT NULL DEFAULT 'chat'`],\n [\"app\", `TEXT NOT NULL DEFAULT ''`],\n ];\n for (const [col, def] of additions) {\n try {\n if (isPostgres()) {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN IF NOT EXISTS ${col} ${def}`,\n );\n } else {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN ${col} ${def}`,\n );\n }\n } catch {\n // Column already exists — ignore\n }\n }\n\n try {\n await client.execute(\n `CREATE INDEX IF NOT EXISTS idx_token_usage_owner_created ON token_usage (owner_email, created_at)`,\n );\n } catch {}\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n/**\n * Calculate cost in centicents (1/100th of a cent).\n * Accepts cache tokens so callers that use prompt caching are priced\n * correctly. Non-cache-aware callers can pass 0 for the cache fields.\n */\nexport function calculateCost(\n inputTokens: number,\n outputTokens: number,\n model: string,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n): number {\n const p = pricingFor(model);\n const rawCenticents =\n (inputTokens / 1_000_000) * p.input * 100 +\n (outputTokens / 1_000_000) * p.output * 100 +\n (cacheReadTokens / 1_000_000) * p.cacheRead * 100 +\n (cacheWriteTokens / 1_000_000) * p.cacheWrite * 100;\n return rawCenticents > 0 ? Math.max(1, Math.round(rawCenticents)) : 0;\n}\n\n/**\n * Record token usage from an LLM call.\n *\n * Accepts an object with the full set of fields. A positional overload\n * remains for backward compatibility with the older 4-arg signature.\n */\nexport async function recordUsage(record: UsageRecord): Promise<void>;\nexport async function recordUsage(\n ownerEmail: string,\n inputTokens: number,\n outputTokens: number,\n model: string,\n): Promise<void>;\nexport async function recordUsage(\n recordOrOwner: UsageRecord | string,\n inputTokens?: number,\n outputTokens?: number,\n model?: string,\n): Promise<void> {\n const record: UsageRecord =\n typeof recordOrOwner === \"string\"\n ? {\n ownerEmail: recordOrOwner,\n inputTokens: inputTokens ?? 0,\n outputTokens: outputTokens ?? 0,\n model: model ?? \"\",\n }\n : recordOrOwner;\n\n const {\n ownerEmail,\n inputTokens: inTok,\n outputTokens: outTok,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n model: modelName,\n label,\n app,\n } = record;\n\n // Skip no-op writes (e.g. a stream aborted before any tokens flowed)\n if (!inTok && !outTok && !cacheReadTokens && !cacheWriteTokens) return;\n\n await ensureUsageTable();\n const client = getDbExec();\n const costX100 = calculateCost(\n inTok,\n outTok,\n modelName,\n cacheReadTokens,\n cacheWriteTokens,\n );\n const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);\n const resolvedApp =\n app ?? process.env.AGENT_APP ?? process.env.APP_NAME ?? \"\";\n const resolvedLabel = label ?? \"chat\";\n await client.execute({\n sql: `INSERT INTO token_usage\n (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n ownerEmail,\n inTok,\n outTok,\n cacheReadTokens,\n cacheWriteTokens,\n costX100,\n modelName,\n resolvedLabel,\n resolvedApp,\n Date.now(),\n ],\n });\n}\n\n/** Total cost (in cents) charged against a user, across all time. */\nexport async function getUserUsageCents(ownerEmail: string): Promise<number> {\n await ensureUsageTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT COALESCE(SUM(cost_cents_x100), 0) as total FROM token_usage WHERE owner_email = ?`,\n args: [ownerEmail],\n });\n const total = Number((rows[0] as { total?: number })?.total ?? 0);\n return total / 100;\n}\n\n// ─── Admin / UI queries ─────────────────────────────────────────────────\n\nexport interface UsageSummaryOptions {\n ownerEmail: string;\n /** Inclusive lower bound (ms since epoch). Defaults to 30 days ago. */\n sinceMs?: number;\n}\n\nexport interface UsageBucket {\n key: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n calls: number;\n}\n\nexport interface DailyBucket {\n /** YYYY-MM-DD (UTC) */\n date: string;\n cents: number;\n calls: number;\n}\n\nexport interface UsageRecentEntry {\n id: number;\n createdAt: number;\n label: string;\n app: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n}\n\nexport interface UsageSummary {\n billing?: UsageBillingMode;\n totalCents: number;\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalCacheWriteTokens: number;\n sinceMs: number;\n byLabel: UsageBucket[];\n byModel: UsageBucket[];\n byApp: UsageBucket[];\n byDay: DailyBucket[];\n recent: UsageRecentEntry[];\n}\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Produce an aggregated spend view for the Usage admin panel.\n * Scoped to the passed owner email; the UI always passes the session user.\n */\nexport async function getUsageSummary(\n options: UsageSummaryOptions,\n): Promise<UsageSummary> {\n await ensureUsageTable();\n const client = getDbExec();\n const sinceMs = options.sinceMs ?? Date.now() - 30 * DAY_MS;\n\n const totalRow = await client.execute({\n sql: `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const t = (totalRow.rows[0] ?? {}) as Record<string, number | null>;\n\n const bucketSql = (col: string) => ({\n sql: `SELECT ${col} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage\n WHERE owner_email = ? AND created_at >= ?\n GROUP BY ${col}\n ORDER BY cents DESC`,\n args: [options.ownerEmail, sinceMs],\n });\n\n const mapBuckets = (rows: unknown[]): UsageBucket[] =>\n rows.map((r) => {\n const row = r as Record<string, number | string | null>;\n return {\n key: String(row.k ?? \"\"),\n cents: Number(row.cents ?? 0) / 100,\n calls: Number(row.calls ?? 0),\n inputTokens: Number(row.in_tok ?? 0),\n outputTokens: Number(row.out_tok ?? 0),\n cacheReadTokens: Number(row.cr_tok ?? 0),\n cacheWriteTokens: Number(row.cw_tok ?? 0),\n };\n });\n\n const [byLabelR, byModelR, byAppR] = await Promise.all([\n client.execute(bucketSql(\"label\")),\n client.execute(bucketSql(\"model\")),\n client.execute(bucketSql(\"app\")),\n ]);\n\n // By-day aggregation — done in JS so we don't depend on dialect-specific\n // date functions (SQLite `strftime`, Postgres `to_char`). Cheap enough\n // for a 30-day window; if this grows, swap for a dialect-aware query.\n const dayRows = await client.execute({\n sql: `SELECT created_at, cost_cents_x100 FROM token_usage\n WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const dayMap = new Map<string, { cents: number; calls: number }>();\n for (const row of dayRows.rows as Array<Record<string, number>>) {\n const date = new Date(Number(row.created_at)).toISOString().slice(0, 10);\n const prev = dayMap.get(date) ?? { cents: 0, calls: 0 };\n prev.cents += Number(row.cost_cents_x100 ?? 0);\n prev.calls += 1;\n dayMap.set(date, prev);\n }\n const byDay: DailyBucket[] = [...dayMap.entries()]\n .map(([date, v]) => ({\n date,\n cents: v.cents / 100,\n calls: v.calls,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await client.execute({\n sql: `SELECT id, created_at, label, app, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE owner_email = ?\n ORDER BY created_at DESC\n LIMIT 50`,\n args: [options.ownerEmail],\n });\n const recent: UsageRecentEntry[] = (\n recentRows.rows as Array<Record<string, number | string | null>>\n ).map((row) => ({\n id: Number(row.id),\n createdAt: Number(row.created_at),\n label: String(row.label ?? \"chat\"),\n app: String(row.app ?? \"\"),\n model: String(row.model ?? \"\"),\n inputTokens: Number(row.input_tokens ?? 0),\n outputTokens: Number(row.output_tokens ?? 0),\n cacheReadTokens: Number(row.cache_read_tokens ?? 0),\n cacheWriteTokens: Number(row.cache_write_tokens ?? 0),\n cents: Number(row.cost_cents_x100 ?? 0) / 100,\n }));\n\n return {\n billing: USD_USAGE_BILLING,\n totalCents: Number(t.cents ?? 0) / 100,\n totalCalls: Number(t.calls ?? 0),\n totalInputTokens: Number(t.in_tok ?? 0),\n totalOutputTokens: Number(t.out_tok ?? 0),\n totalCacheReadTokens: Number(t.cr_tok ?? 0),\n totalCacheWriteTokens: Number(t.cw_tok ?? 0),\n sinceMs,\n byLabel: mapBuckets(byLabelR.rows),\n byModel: mapBuckets(byModelR.rows),\n byApp: mapBuckets(byAppR.rows),\n byDay,\n recent,\n };\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Token usage tracking and cost monitoring.\n *\n * Every LLM call made by the framework records a row here so users can\n * see where their spend is going — chat vs automations vs background jobs\n * vs whatever else a template labels its prompts as.\n *\n * Cost is stored as \"centicents\" (1/100th of a cent) for integer precision.\n */\nimport { getDbExec, intType, isPostgres } from \"../db/client.js\";\n\n/**\n * Per-million-token pricing in cents. Cache read is typically ~10% of\n * input; cache write (5m TTL) is ~125%. Pricing is best-effort — keep\n * this table in sync with Anthropic's published prices.\n */\ninterface ModelPricing {\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n}\n\nexport const BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER = 1.25;\nexport const BUILDER_AGENT_CREDITS_PER_USD = 20;\n\nexport type UsageBillingUnit = \"usd\" | \"builder-credits\";\n\nexport interface UsageBillingMode {\n unit: UsageBillingUnit;\n label: string;\n shortLabel: string;\n source: \"estimated-provider-cost\" | \"builder-agent-credits\";\n hardCostMarginMultiplier?: number;\n creditsPerUsd?: number;\n}\n\nexport const USD_USAGE_BILLING: UsageBillingMode = {\n unit: \"usd\",\n label: \"Estimated spend\",\n shortLabel: \"Cost\",\n source: \"estimated-provider-cost\",\n};\n\nexport const BUILDER_CREDIT_USAGE_BILLING: UsageBillingMode = {\n unit: \"builder-credits\",\n label: \"Builder.io credit spend\",\n shortLabel: \"Credits\",\n source: \"builder-agent-credits\",\n hardCostMarginMultiplier: BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER,\n creditsPerUsd: BUILDER_AGENT_CREDITS_PER_USD,\n};\n\nexport function usageBillingForEngine(\n engineName: string | null | undefined,\n): UsageBillingMode {\n return engineName === \"builder\"\n ? BUILDER_CREDIT_USAGE_BILLING\n : USD_USAGE_BILLING;\n}\n\nexport function builderCreditsFromCostCents(cents: number): number {\n if (!Number.isFinite(cents) || cents <= 0) return 0;\n const dollars = cents / 100;\n const credits =\n dollars *\n BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER *\n BUILDER_AGENT_CREDITS_PER_USD;\n return Math.ceil(credits * 1000) / 1000;\n}\n\nconst PRICING: Array<{ match: RegExp; pricing: ModelPricing }> = [\n {\n match: /opus/i,\n pricing: { input: 1500, output: 7500, cacheRead: 150, cacheWrite: 1875 },\n },\n {\n match: /haiku/i,\n pricing: { input: 100, output: 500, cacheRead: 10, cacheWrite: 125 },\n },\n // OpenAI / Codex models (cents per 1M tokens). Without these, a Codex recap\n // model like \"gpt-5.5\" falls through to the default (Sonnet) row and is\n // mispriced. Published rates as of 2026-06; OpenAI bills cached input at a\n // discount and has no separate cache-write token charge, so cacheWrite is 0\n // (recap usage passes 0 cache-write anyway). The /gpt-5\\.5/ row must precede\n // /gpt-5/ since the latter also matches \"gpt-5.5\".\n {\n match: /gpt-5\\.5/i,\n pricing: { input: 500, output: 3000, cacheRead: 50, cacheWrite: 0 },\n },\n {\n match: /gpt-5/i,\n pricing: { input: 125, output: 1000, cacheRead: 12.5, cacheWrite: 0 },\n },\n // default → sonnet pricing\n {\n match: /.*/,\n pricing: { input: 300, output: 1500, cacheRead: 30, cacheWrite: 375 },\n },\n];\n\nfunction pricingFor(model: string): ModelPricing {\n for (const entry of PRICING) {\n if (entry.match.test(model)) return entry.pricing;\n }\n return PRICING[PRICING.length - 1]!.pricing;\n}\n\nexport interface UsageRecord {\n ownerEmail: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n model: string;\n /** Category for this call — e.g. \"chat\", \"automation\", \"job\", \"custom-agent\". */\n label?: string;\n /** Optional template/app name (e.g. \"mail\"). Falls back to AGENT_APP / APP_NAME env. */\n app?: string;\n /**\n * Stable id of the thing this usage belongs to (e.g. a recap plan id). When\n * set, any prior row(s) with the same (label, refId) are deleted before\n * insert, so re-recording the same run overwrites instead of double-counting.\n */\n refId?: string;\n /**\n * Precomputed cost in centicents (1/100¢). When provided, it is stored\n * verbatim instead of being derived from tokens — e.g. to mirror a\n * provider-reported dollar cost so two surfaces agree exactly.\n */\n costCentsX100?: number;\n}\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureUsageTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS token_usage (\n id ${intType()} PRIMARY KEY,\n owner_email TEXT NOT NULL,\n input_tokens ${intType()} NOT NULL DEFAULT 0,\n output_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_read_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_write_tokens ${intType()} NOT NULL DEFAULT 0,\n cost_cents_x100 ${intType()} NOT NULL DEFAULT 0,\n model TEXT NOT NULL DEFAULT '',\n label TEXT NOT NULL DEFAULT 'chat',\n app TEXT NOT NULL DEFAULT '',\n ref_id TEXT NOT NULL DEFAULT '',\n created_at ${intType()} NOT NULL\n )\n `);\n\n // Add columns on older deployments that pre-date the label/cache\n // fields. Each ALTER is wrapped so a dialect without IF NOT EXISTS\n // (SQLite) still makes progress if only some columns are missing.\n const additions: Array<[string, string]> = [\n [\"cache_read_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"cache_write_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"label\", `TEXT NOT NULL DEFAULT 'chat'`],\n [\"app\", `TEXT NOT NULL DEFAULT ''`],\n [\"ref_id\", `TEXT NOT NULL DEFAULT ''`],\n ];\n for (const [col, def] of additions) {\n try {\n if (isPostgres()) {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN IF NOT EXISTS ${col} ${def}`,\n );\n } else {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN ${col} ${def}`,\n );\n }\n } catch {\n // Column already exists — ignore\n }\n }\n\n try {\n await client.execute(\n `CREATE INDEX IF NOT EXISTS idx_token_usage_owner_created ON token_usage (owner_email, created_at)`,\n );\n } catch {}\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n/**\n * Calculate cost in centicents (1/100th of a cent).\n * Accepts cache tokens so callers that use prompt caching are priced\n * correctly. Non-cache-aware callers can pass 0 for the cache fields.\n */\nexport function calculateCost(\n inputTokens: number,\n outputTokens: number,\n model: string,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n): number {\n const p = pricingFor(model);\n const rawCenticents =\n (inputTokens / 1_000_000) * p.input * 100 +\n (outputTokens / 1_000_000) * p.output * 100 +\n (cacheReadTokens / 1_000_000) * p.cacheRead * 100 +\n (cacheWriteTokens / 1_000_000) * p.cacheWrite * 100;\n return rawCenticents > 0 ? Math.max(1, Math.round(rawCenticents)) : 0;\n}\n\n/**\n * Record token usage from an LLM call.\n *\n * Accepts an object with the full set of fields. A positional overload\n * remains for backward compatibility with the older 4-arg signature.\n */\nexport async function recordUsage(record: UsageRecord): Promise<void>;\nexport async function recordUsage(\n ownerEmail: string,\n inputTokens: number,\n outputTokens: number,\n model: string,\n): Promise<void>;\nexport async function recordUsage(\n recordOrOwner: UsageRecord | string,\n inputTokens?: number,\n outputTokens?: number,\n model?: string,\n): Promise<void> {\n const record: UsageRecord =\n typeof recordOrOwner === \"string\"\n ? {\n ownerEmail: recordOrOwner,\n inputTokens: inputTokens ?? 0,\n outputTokens: outputTokens ?? 0,\n model: model ?? \"\",\n }\n : recordOrOwner;\n\n const {\n ownerEmail,\n inputTokens: inTok,\n outputTokens: outTok,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n model: modelName,\n label,\n app,\n refId,\n costCentsX100,\n } = record;\n\n // Skip no-op writes (e.g. a stream aborted before any tokens flowed)\n if (!inTok && !outTok && !cacheReadTokens && !cacheWriteTokens) return;\n\n await ensureUsageTable();\n const client = getDbExec();\n const resolvedApp =\n app ?? process.env.AGENT_APP ?? process.env.APP_NAME ?? \"\";\n const resolvedLabel = label ?? \"chat\";\n const resolvedRef = refId ?? \"\";\n\n // Replace any prior usage for this (label, refId) so re-recording the same\n // run — e.g. a recap regenerated on a PR re-push — overwrites instead of\n // double-counting. No-op when refId is unset (the common per-call path).\n if (resolvedRef) {\n await client.execute({\n sql: `DELETE FROM token_usage WHERE label = ? AND ref_id = ?`,\n args: [resolvedLabel, resolvedRef],\n });\n }\n\n // Prefer an explicit precomputed cost (e.g. a provider-reported dollar cost);\n // otherwise derive it from tokens via the pricing table.\n const costX100 =\n costCentsX100 ??\n calculateCost(inTok, outTok, modelName, cacheReadTokens, cacheWriteTokens);\n const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);\n await client.execute({\n sql: `INSERT INTO token_usage\n (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, ref_id, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n ownerEmail,\n inTok,\n outTok,\n cacheReadTokens,\n cacheWriteTokens,\n costX100,\n modelName,\n resolvedLabel,\n resolvedApp,\n resolvedRef,\n Date.now(),\n ],\n });\n}\n\n/** Total cost (in cents) charged against a user, across all time. */\nexport async function getUserUsageCents(ownerEmail: string): Promise<number> {\n await ensureUsageTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT COALESCE(SUM(cost_cents_x100), 0) as total FROM token_usage WHERE owner_email = ?`,\n args: [ownerEmail],\n });\n const total = Number((rows[0] as { total?: number })?.total ?? 0);\n return total / 100;\n}\n\n// ─── Admin / UI queries ─────────────────────────────────────────────────\n\nexport interface UsageSummaryOptions {\n ownerEmail: string;\n /** Inclusive lower bound (ms since epoch). Defaults to 30 days ago. */\n sinceMs?: number;\n}\n\nexport interface UsageBucket {\n key: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n calls: number;\n}\n\nexport interface DailyBucket {\n /** YYYY-MM-DD (UTC) */\n date: string;\n cents: number;\n calls: number;\n}\n\nexport interface UsageRecentEntry {\n id: number;\n createdAt: number;\n label: string;\n app: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n}\n\nexport interface UsageSummary {\n billing?: UsageBillingMode;\n totalCents: number;\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalCacheWriteTokens: number;\n sinceMs: number;\n byLabel: UsageBucket[];\n byModel: UsageBucket[];\n byApp: UsageBucket[];\n byDay: DailyBucket[];\n recent: UsageRecentEntry[];\n}\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Produce an aggregated spend view for the Usage admin panel.\n * Scoped to the passed owner email; the UI always passes the session user.\n */\nexport async function getUsageSummary(\n options: UsageSummaryOptions,\n): Promise<UsageSummary> {\n await ensureUsageTable();\n const client = getDbExec();\n const sinceMs = options.sinceMs ?? Date.now() - 30 * DAY_MS;\n\n const totalRow = await client.execute({\n sql: `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const t = (totalRow.rows[0] ?? {}) as Record<string, number | null>;\n\n const bucketSql = (col: string) => ({\n sql: `SELECT ${col} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage\n WHERE owner_email = ? AND created_at >= ?\n GROUP BY ${col}\n ORDER BY cents DESC`,\n args: [options.ownerEmail, sinceMs],\n });\n\n const mapBuckets = (rows: unknown[]): UsageBucket[] =>\n rows.map((r) => {\n const row = r as Record<string, number | string | null>;\n return {\n key: String(row.k ?? \"\"),\n cents: Number(row.cents ?? 0) / 100,\n calls: Number(row.calls ?? 0),\n inputTokens: Number(row.in_tok ?? 0),\n outputTokens: Number(row.out_tok ?? 0),\n cacheReadTokens: Number(row.cr_tok ?? 0),\n cacheWriteTokens: Number(row.cw_tok ?? 0),\n };\n });\n\n const [byLabelR, byModelR, byAppR] = await Promise.all([\n client.execute(bucketSql(\"label\")),\n client.execute(bucketSql(\"model\")),\n client.execute(bucketSql(\"app\")),\n ]);\n\n // By-day aggregation — done in JS so we don't depend on dialect-specific\n // date functions (SQLite `strftime`, Postgres `to_char`). Cheap enough\n // for a 30-day window; if this grows, swap for a dialect-aware query.\n const dayRows = await client.execute({\n sql: `SELECT created_at, cost_cents_x100 FROM token_usage\n WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const dayMap = new Map<string, { cents: number; calls: number }>();\n for (const row of dayRows.rows as Array<Record<string, number>>) {\n const date = new Date(Number(row.created_at)).toISOString().slice(0, 10);\n const prev = dayMap.get(date) ?? { cents: 0, calls: 0 };\n prev.cents += Number(row.cost_cents_x100 ?? 0);\n prev.calls += 1;\n dayMap.set(date, prev);\n }\n const byDay: DailyBucket[] = [...dayMap.entries()]\n .map(([date, v]) => ({\n date,\n cents: v.cents / 100,\n calls: v.calls,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await client.execute({\n sql: `SELECT id, created_at, label, app, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE owner_email = ?\n ORDER BY created_at DESC\n LIMIT 50`,\n args: [options.ownerEmail],\n });\n const recent: UsageRecentEntry[] = (\n recentRows.rows as Array<Record<string, number | string | null>>\n ).map((row) => ({\n id: Number(row.id),\n createdAt: Number(row.created_at),\n label: String(row.label ?? \"chat\"),\n app: String(row.app ?? \"\"),\n model: String(row.model ?? \"\"),\n inputTokens: Number(row.input_tokens ?? 0),\n outputTokens: Number(row.output_tokens ?? 0),\n cacheReadTokens: Number(row.cache_read_tokens ?? 0),\n cacheWriteTokens: Number(row.cache_write_tokens ?? 0),\n cents: Number(row.cost_cents_x100 ?? 0) / 100,\n }));\n\n return {\n billing: USD_USAGE_BILLING,\n totalCents: Number(t.cents ?? 0) / 100,\n totalCalls: Number(t.calls ?? 0),\n totalInputTokens: Number(t.in_tok ?? 0),\n totalOutputTokens: Number(t.out_tok ?? 0),\n totalCacheReadTokens: Number(t.cr_tok ?? 0),\n totalCacheWriteTokens: Number(t.cw_tok ?? 0),\n sinceMs,\n byLabel: mapBuckets(byLabelR.rows),\n byModel: mapBuckets(byModelR.rows),\n byApp: mapBuckets(byAppR.rows),\n byDay,\n recent,\n };\n}\n"]}