okengine 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Client facts derived from a stored HTTP frame.
3
+ *
4
+ * Port of the sacp user-agent classifier. Nothing is stored: the trace sheet
5
+ * reads the headers it already has. `unknown`, `other`, and `none` stay off
6
+ * the card.
7
+ */
8
+
9
+ /** Device class from the user-agent and client hints. */
10
+ export type DeviceType = "unknown" | "desktop" | "mobile" | "tablet" | "bot";
11
+
12
+ /** Operating system family. */
13
+ export type Platform =
14
+ | "windows"
15
+ | "macos"
16
+ | "linux"
17
+ | "ios"
18
+ | "ipados"
19
+ | "android"
20
+ | "chromeos"
21
+ | "freebsd"
22
+ | "openbsd"
23
+ | "other";
24
+
25
+ /** Browser family. `none` is a non-browser client. */
26
+ export type Browser =
27
+ | "chrome"
28
+ | "firefox"
29
+ | "safari"
30
+ | "edge"
31
+ | "arc"
32
+ | "opera"
33
+ | "brave"
34
+ | "vivaldi"
35
+ | "other"
36
+ | "none";
37
+
38
+ /** Who sent the request. */
39
+ export type ClientType = "browser" | "api_client" | "cli" | "bot" | "other";
40
+
41
+ /** Named HTTP client when the user-agent is not a browser. */
42
+ export type ApiClient =
43
+ | "postmanruntime"
44
+ | "newman"
45
+ | "insomnia"
46
+ | "hoppscotch"
47
+ | "curl"
48
+ | "httpie"
49
+ | "wget"
50
+ | "python-requests"
51
+ | "okhttp"
52
+ | "axios"
53
+ | "node-fetch"
54
+ | "undici"
55
+ | "other"
56
+ | "none";
57
+
58
+ /** Facts the Request card can draw. Hidden kinds are `null`. */
59
+ export type RequestClient = {
60
+ /** Device, or `null` when the guess is `unknown`. */
61
+ readonly deviceType: DeviceType | null;
62
+ /** Platform, or `null` when the guess is `other`. */
63
+ readonly platform: Platform | null;
64
+ /** Browser, or `null` when the guess is `other` or `none`. */
65
+ readonly browser: Browser | null;
66
+ /** Major, or major.minor when the minor is not zero. */
67
+ readonly browserVersion: string | null;
68
+ /** Client kind, or `null` when the guess is `other`. */
69
+ readonly clientType: ClientType | null;
70
+ /** API or CLI client, or `null` when the guess is `other` or `none`. */
71
+ readonly apiClient: ApiClient | null;
72
+ /** Version token for {@link apiClient}. */
73
+ readonly apiClientVersion: string | null;
74
+ /** Call API invoke stamped `x-oke-client: console`. */
75
+ readonly console: boolean;
76
+ /** Client IP from forwarding headers, when one is stored. */
77
+ readonly ip: string | null;
78
+ /** Raw `user-agent`, when the header is present. */
79
+ readonly userAgent: string | null;
80
+ };
81
+
82
+ type Detection = {
83
+ deviceType: DeviceType;
84
+ platform: Platform;
85
+ browser: Browser;
86
+ clientType: ClientType;
87
+ apiClient: ApiClient;
88
+ };
89
+
90
+ const PLATFORM_HINT: Record<string, Platform> = {
91
+ windows: "windows",
92
+ macos: "macos",
93
+ "mac os": "macos",
94
+ linux: "linux",
95
+ android: "android",
96
+ ios: "ios",
97
+ ipados: "ipados",
98
+ "chrome os": "chromeos",
99
+ chromeos: "chromeos",
100
+ cros: "chromeos",
101
+ freebsd: "freebsd",
102
+ openbsd: "openbsd",
103
+ };
104
+
105
+ /**
106
+ * Classify the request headers already stored on a run.
107
+ *
108
+ * @param headers - Request header map. Names may be any case.
109
+ */
110
+ export function requestClientFromHeaders(headers: Readonly<Record<string, string>>): RequestClient {
111
+ const userAgent = headerValue(headers, "user-agent")?.trim() ?? "";
112
+ const detected = detectFromUserAgent(userAgent);
113
+ const hinted = applyClientHints(detected, userAgent.toLowerCase(), headers);
114
+ const version = productVersion(userAgent, hinted.browser, hinted.apiClient);
115
+ const ip = clientIp(headers);
116
+ return {
117
+ deviceType: hinted.deviceType === "unknown" ? null : hinted.deviceType,
118
+ platform: hinted.platform === "other" ? null : hinted.platform,
119
+ browser: hinted.browser === "other" || hinted.browser === "none" ? null : hinted.browser,
120
+ browserVersion: hinted.browser === "other" || hinted.browser === "none" ? null : version,
121
+ clientType: hinted.clientType === "other" ? null : hinted.clientType,
122
+ apiClient:
123
+ hinted.apiClient === "other" || hinted.apiClient === "none" ? null : hinted.apiClient,
124
+ apiClientVersion: hinted.apiClient === "other" || hinted.apiClient === "none" ? null : version,
125
+ console: headerValue(headers, "x-oke-client")?.trim().toLowerCase() === "console",
126
+ ip,
127
+ userAgent: userAgent.length > 0 ? userAgent : null,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Best-effort user-agent classification.
133
+ *
134
+ * Non-browser clients match first so Postman and curl are not read as Chrome.
135
+ * Arc and Brave often still look like Chrome from the user-agent alone.
136
+ *
137
+ * @param userAgent - Raw user-agent, possibly empty
138
+ */
139
+ export function detectFromUserAgent(userAgent?: string): Detection {
140
+ const ua = (userAgent ?? "").trim();
141
+ const ual = ua.toLowerCase();
142
+
143
+ let clientType: ClientType = "other";
144
+ let apiClient: ApiClient = "none";
145
+ let browser: Browser = "other";
146
+ const platform: Platform = "other";
147
+ let deviceType: DeviceType = "unknown";
148
+
149
+ if (/\bpostmanruntime\/\d+/.test(ual)) {
150
+ return finalize({
151
+ ua: ual,
152
+ clientType: "api_client",
153
+ apiClient: "postmanruntime",
154
+ browser: "none",
155
+ platform,
156
+ deviceType,
157
+ });
158
+ }
159
+ if (/\bnewman\/\d+/.test(ual)) {
160
+ return finalize({
161
+ ua: ual,
162
+ clientType: "cli",
163
+ apiClient: "newman",
164
+ browser: "none",
165
+ platform,
166
+ deviceType,
167
+ });
168
+ }
169
+ if (/\binsomnia\/\d+/.test(ual)) {
170
+ return finalize({
171
+ ua: ual,
172
+ clientType: "api_client",
173
+ apiClient: "insomnia",
174
+ browser: "none",
175
+ platform,
176
+ deviceType,
177
+ });
178
+ }
179
+ if (/\bhoppscotch\b/.test(ual)) {
180
+ return finalize({
181
+ ua: ual,
182
+ clientType: "api_client",
183
+ apiClient: "hoppscotch",
184
+ browser: "none",
185
+ platform,
186
+ deviceType,
187
+ });
188
+ }
189
+ if (/\bcurl\/\d+/.test(ual)) {
190
+ return finalize({
191
+ ua: ual,
192
+ clientType: "cli",
193
+ apiClient: "curl",
194
+ browser: "none",
195
+ platform,
196
+ deviceType,
197
+ });
198
+ }
199
+ if (/\bhttpie\/\d+/.test(ual)) {
200
+ return finalize({
201
+ ua: ual,
202
+ clientType: "cli",
203
+ apiClient: "httpie",
204
+ browser: "none",
205
+ platform,
206
+ deviceType,
207
+ });
208
+ }
209
+ if (/\bwget\/\d+/.test(ual)) {
210
+ return finalize({
211
+ ua: ual,
212
+ clientType: "cli",
213
+ apiClient: "wget",
214
+ browser: "none",
215
+ platform,
216
+ deviceType,
217
+ });
218
+ }
219
+ if (/\bpython-requests\/\d+/.test(ual)) {
220
+ return finalize({
221
+ ua: ual,
222
+ clientType: "api_client",
223
+ apiClient: "python-requests",
224
+ browser: "none",
225
+ platform,
226
+ deviceType,
227
+ });
228
+ }
229
+ if (/\bokhttp\/\d+/.test(ual)) {
230
+ return finalize({
231
+ ua: ual,
232
+ clientType: "api_client",
233
+ apiClient: "okhttp",
234
+ browser: "none",
235
+ platform,
236
+ deviceType,
237
+ });
238
+ }
239
+ if (/\baxios\/\d+/.test(ual)) {
240
+ return finalize({
241
+ ua: ual,
242
+ clientType: "api_client",
243
+ apiClient: "axios",
244
+ browser: "none",
245
+ platform,
246
+ deviceType,
247
+ });
248
+ }
249
+ if (/\bnode-fetch\/\d+/.test(ual)) {
250
+ return finalize({
251
+ ua: ual,
252
+ clientType: "api_client",
253
+ apiClient: "node-fetch",
254
+ browser: "none",
255
+ platform,
256
+ deviceType,
257
+ });
258
+ }
259
+ if (/\bundici\b/.test(ual)) {
260
+ return finalize({
261
+ ua: ual,
262
+ clientType: "api_client",
263
+ apiClient: "undici",
264
+ browser: "none",
265
+ platform,
266
+ deviceType,
267
+ });
268
+ }
269
+
270
+ if (/\bbot\b|\bcrawler\b|\bspider\b/.test(ual)) {
271
+ return finalize({
272
+ ua: ual,
273
+ clientType: "bot",
274
+ apiClient: "none",
275
+ browser: "none",
276
+ platform,
277
+ deviceType: "bot",
278
+ });
279
+ }
280
+
281
+ clientType = "browser";
282
+ apiClient = "none";
283
+ if (/\bedg\//.test(ual)) browser = "edge";
284
+ else if (/\barc\//.test(ual)) browser = "arc";
285
+ else if (/\bvivaldi\//.test(ual)) browser = "vivaldi";
286
+ else if (/\bopr\//.test(ual) || /\bopera\b/.test(ual)) browser = "opera";
287
+ else if (/\bfirefox\//.test(ual)) browser = "firefox";
288
+ else if (/\bchrome\//.test(ual) || /\bcrios\//.test(ual)) browser = "chrome";
289
+ else if (/\bsafari\//.test(ual)) browser = "safari";
290
+ else if (/\bbrave\//.test(ual)) browser = "brave";
291
+ else browser = "other";
292
+
293
+ return finalize({ ua: ual, clientType, apiClient, browser, platform, deviceType });
294
+ }
295
+
296
+ function finalize(input: {
297
+ ua: string;
298
+ clientType: ClientType;
299
+ apiClient: ApiClient;
300
+ browser: Browser;
301
+ platform: Platform;
302
+ deviceType: DeviceType;
303
+ }): Detection {
304
+ let platform = input.platform;
305
+ let deviceType = input.deviceType;
306
+ const ual = input.ua;
307
+
308
+ if (/\bwindows nt\b/.test(ual)) platform = "windows";
309
+ else if (/\bipad\b/.test(ual)) platform = "ipados";
310
+ else if (/\biphone\b|\bipod\b|\bcpu iphone os\b/.test(ual)) platform = "ios";
311
+ else if (/\bandroid\b/.test(ual)) platform = "android";
312
+ else if (/\bcros\b|\bchromeos\b/.test(ual)) platform = "chromeos";
313
+ else if (/\bmac os x\b/.test(ual)) platform = "macos";
314
+ else if (/\bfreebsd\b/.test(ual)) platform = "freebsd";
315
+ else if (/\bopenbsd\b/.test(ual)) platform = "openbsd";
316
+ else if (/\blinux\b/.test(ual)) platform = "linux";
317
+
318
+ deviceType = deviceFromPlatform(platform, ual, deviceType);
319
+
320
+ return {
321
+ clientType: input.clientType,
322
+ apiClient: input.apiClient,
323
+ browser: input.browser,
324
+ platform,
325
+ deviceType,
326
+ };
327
+ }
328
+
329
+ function deviceFromPlatform(platform: Platform, ua: string, current: DeviceType): DeviceType {
330
+ if (current === "bot") return "bot";
331
+ if (platform === "ipados" || /\btablet\b/.test(ua)) return "tablet";
332
+ if (platform === "ios" || platform === "android" || /\bmobile\b/.test(ua)) return "mobile";
333
+ if (
334
+ platform === "windows" ||
335
+ platform === "macos" ||
336
+ platform === "linux" ||
337
+ platform === "chromeos" ||
338
+ platform === "freebsd" ||
339
+ platform === "openbsd"
340
+ ) {
341
+ return "desktop";
342
+ }
343
+ return current === "unknown" ? "unknown" : current;
344
+ }
345
+
346
+ function applyClientHints(
347
+ detected: Detection,
348
+ ua: string,
349
+ headers: Readonly<Record<string, string>>,
350
+ ): Detection {
351
+ const platformHint = unquote(headerValue(headers, "sec-ch-ua-platform"));
352
+ const platform = platformHint
353
+ ? (PLATFORM_HINT[platformHint.toLowerCase()] ?? detected.platform)
354
+ : detected.platform;
355
+ let deviceType =
356
+ platform === detected.platform
357
+ ? detected.deviceType
358
+ : deviceFromPlatform(platform, ua, detected.deviceType);
359
+ const mobile = unquote(headerValue(headers, "sec-ch-ua-mobile"));
360
+ if (deviceType !== "bot" && mobile === "?1") deviceType = "mobile";
361
+ if (deviceType !== "bot" && mobile === "?0") {
362
+ deviceType = platform === "ipados" || /\btablet\b/.test(ua) ? "tablet" : "desktop";
363
+ }
364
+ return { ...detected, platform, deviceType };
365
+ }
366
+
367
+ function clientIp(headers: Readonly<Record<string, string>>): string | null {
368
+ const cf = headerValue(headers, "cf-connecting-ip")?.trim();
369
+ if (cf) return cf;
370
+ const forwarded = headerValue(headers, "x-forwarded-for")?.split(",")[0]?.trim();
371
+ if (forwarded) return forwarded;
372
+ const real = headerValue(headers, "x-real-ip")?.trim();
373
+ return real && real.length > 0 ? real : null;
374
+ }
375
+
376
+ function productVersion(userAgent: string, browser: Browser, apiClient: ApiClient): string | null {
377
+ const token =
378
+ browser === "safari"
379
+ ? matchVersion(userAgent, /version\/(\d+(?:\.\d+)*)/i)
380
+ : browser === "edge"
381
+ ? matchVersion(userAgent, /\bedg\/(\d+(?:\.\d+)*)/i)
382
+ : browser === "opera"
383
+ ? matchVersion(userAgent, /\b(?:opr|opera)\/(\d+(?:\.\d+)*)/i)
384
+ : browser === "chrome"
385
+ ? matchVersion(userAgent, /\b(?:chrome|crios)\/(\d+(?:\.\d+)*)/i)
386
+ : browser !== "other" && browser !== "none"
387
+ ? matchVersion(userAgent, new RegExp(`\\b${browser}\\/(\\d+(?:\\.\\d+)*)`, "i"))
388
+ : apiClient !== "other" && apiClient !== "none"
389
+ ? matchVersion(userAgent, new RegExp(`\\b${apiClient}\\/(\\d+(?:\\.\\d+)*)`, "i"))
390
+ : null;
391
+ return token ? displayVersion(token) : null;
392
+ }
393
+
394
+ function matchVersion(userAgent: string, pattern: RegExp): string | null {
395
+ return pattern.exec(userAgent)?.[1] ?? null;
396
+ }
397
+
398
+ /** Major only when the minor is zero (`131.0.0.0` → `131`, `8.7.1` → `8.7`). */
399
+ function displayVersion(raw: string): string {
400
+ const [major, minor] = raw.split(".");
401
+ if (!major) return raw;
402
+ if (!minor || minor === "0") return major;
403
+ return `${major}.${minor}`;
404
+ }
405
+
406
+ function headerValue(headers: Readonly<Record<string, string>>, name: string): string | undefined {
407
+ const direct = headers[name];
408
+ if (direct !== undefined) return direct;
409
+ const lower = name.toLowerCase();
410
+ for (const [key, value] of Object.entries(headers)) {
411
+ if (key.toLowerCase() === lower) return value;
412
+ }
413
+ return undefined;
414
+ }
415
+
416
+ function unquote(value: string | undefined): string | null {
417
+ if (!value) return null;
418
+ const trimmed = value.trim();
419
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
420
+ return trimmed.slice(1, -1);
421
+ }
422
+ return trimmed;
423
+ }
@@ -8,8 +8,26 @@ import {
8
8
  inputByteLabel,
9
9
  inputFieldRows,
10
10
  inputShapeHint,
11
+ payloadHasContent,
11
12
  } from "./request-input-view.ts";
12
13
 
14
+ describe("payloadHasContent", () => {
15
+ test("treats an empty object, array, or missing value as no body", () => {
16
+ expect(payloadHasContent({})).toBe(false);
17
+ expect(payloadHasContent([])).toBe(false);
18
+ expect(payloadHasContent("")).toBe(false);
19
+ expect(payloadHasContent(null)).toBe(false);
20
+ expect(payloadHasContent(undefined)).toBe(false);
21
+ });
22
+
23
+ test("keeps a payload that has fields or a scalar", () => {
24
+ expect(payloadHasContent({ limit: 20 })).toBe(true);
25
+ expect(payloadHasContent([1])).toBe(true);
26
+ expect(payloadHasContent(0)).toBe(true);
27
+ expect(payloadHasContent(false)).toBe(true);
28
+ });
29
+ });
30
+
13
31
  describe("inputFieldRows", () => {
14
32
  test("projects plain objects into typed rows", () => {
15
33
  expect(
@@ -20,22 +38,50 @@ describe("inputFieldRows", () => {
20
38
  meta: { cabin: "economy" },
21
39
  }),
22
40
  ).toEqual([
23
- { key: "bookingId", value: "bk_8f2a", display: "bk_8f2a", kind: "string" },
24
- { key: "seats", value: 2, display: "2", kind: "number" },
25
- { key: "ok", value: true, display: "true", kind: "boolean" },
41
+ { key: "bookingId", value: "bk_8f2a", display: "bk_8f2a", kind: "string", children: null },
42
+ { key: "seats", value: 2, display: "2", kind: "number", children: null },
43
+ { key: "ok", value: true, display: "true", kind: "boolean", children: null },
26
44
  {
27
45
  key: "meta",
28
46
  value: { cabin: "economy" },
29
- display: '{"cabin":"economy"}',
47
+ display: "cabin: economy",
30
48
  kind: "object",
49
+ children: [
50
+ { key: "cabin", value: "economy", display: "economy", kind: "string", children: null },
51
+ ],
31
52
  },
32
53
  ]);
33
54
  });
34
55
 
35
- test("returns null for arrays and scalars", () => {
36
- expect(inputFieldRows(["a"])).toBeNull();
56
+ test("projects arrays as indexed rows and nests objects", () => {
57
+ expect(inputFieldRows([{ id: "ENG-12", tags: ["sso"] }])).toEqual([
58
+ {
59
+ key: "0",
60
+ value: { id: "ENG-12", tags: ["sso"] },
61
+ display: "id: ENG-12",
62
+ kind: "object",
63
+ children: [
64
+ { key: "id", value: "ENG-12", display: "ENG-12", kind: "string", children: null },
65
+ {
66
+ key: "tags",
67
+ value: ["sso"],
68
+ display: "1 item",
69
+ kind: "array",
70
+ children: [{ key: "0", value: "sso", display: "sso", kind: "string", children: null }],
71
+ },
72
+ ],
73
+ },
74
+ ]);
75
+ });
76
+
77
+ test("returns null for scalars, null, and empty arrays", () => {
37
78
  expect(inputFieldRows("x")).toBeNull();
38
79
  expect(inputFieldRows(null)).toBeNull();
80
+ expect(inputFieldRows([])).toBeNull();
81
+ });
82
+
83
+ test("previews nested containers by count when they have no short scalars", () => {
84
+ expect(inputFieldRows({ blob: { nested: { a: 1 } } })?.[0]?.display).toBe("1 field");
39
85
  });
40
86
  });
41
87
 
@@ -2,12 +2,12 @@
2
2
  * Helpers for rendering run.input in the trace Request section.
3
3
  */
4
4
 
5
- /** Scalar / container kind for a top-level input field. */
5
+ /** Scalar / container kind for a payload field. */
6
6
  export type InputFieldKind = "string" | "number" | "boolean" | "null" | "object" | "array";
7
7
 
8
- /** One top-level field row for the Fields view. */
8
+ /** One field row in the Fields view, including nested object fields and array items. */
9
9
  export type InputFieldRow = {
10
- /** Object key. */
10
+ /** Object key, or array index. */
11
11
  readonly key: string;
12
12
  /** Raw value (for copy). */
13
13
  readonly value: unknown;
@@ -15,25 +15,43 @@ export type InputFieldRow = {
15
15
  readonly display: string;
16
16
  /** Value kind chip. */
17
17
  readonly kind: InputFieldKind;
18
+ /**
19
+ * Nested object fields or array items.
20
+ * `null` for scalars and empty containers — those rows do not expand.
21
+ */
22
+ readonly children: readonly InputFieldRow[] | null;
18
23
  };
19
24
 
20
25
  /**
21
- * Flatten a plain object into field rows. Nested objects/arrays stay as one
22
- * row with a compact JSON display. Returns `null` when Fields view is not
23
- * appropriate (arrays, scalars, null).
26
+ * Project an object or array into field rows. Nested objects and arrays
27
+ * become expandable child rows. Returns `null` for scalars, `null`, and
28
+ * empty arrays.
24
29
  *
25
- * @param value - Stored run input
30
+ * @param value - Stored run input or output
26
31
  */
27
32
  export function inputFieldRows(value: unknown): readonly InputFieldRow[] | null {
28
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
29
- return null;
33
+ if (Array.isArray(value)) {
34
+ if (value.length === 0) return null;
35
+ return value.map((item, index) => projectField(String(index), item));
30
36
  }
31
- return Object.entries(value as Record<string, unknown>).map(([key, v]) => ({
32
- key,
33
- value: v,
34
- display: formatFieldDisplay(v),
35
- kind: fieldKind(v),
36
- }));
37
+ if (value === null || typeof value !== "object") return null;
38
+ return Object.entries(value as Record<string, unknown>).map(([key, v]) => projectField(key, v));
39
+ }
40
+
41
+ /**
42
+ * Whether a stored payload is worth a Body panel.
43
+ *
44
+ * `null`, `undefined`, `""`, `{}`, and `[]` are empty. A scalar such as
45
+ * `0` or `false` still counts.
46
+ *
47
+ * @param value - Stored run input or output
48
+ */
49
+ export function payloadHasContent(value: unknown): boolean {
50
+ if (value === null || value === undefined) return false;
51
+ if (typeof value === "string") return value.length > 0;
52
+ if (Array.isArray(value)) return value.length > 0;
53
+ if (typeof value === "object") return Object.keys(value).length > 0;
54
+ return true;
37
55
  }
38
56
 
39
57
  /**
@@ -96,13 +114,61 @@ function fieldKind(value: unknown): InputFieldKind {
96
114
  }
97
115
  }
98
116
 
117
+ const PREVIEW_FIELDS = 3;
118
+ const PREVIEW_SCALAR_MAX = 42;
119
+
120
+ function projectField(key: string, value: unknown): InputFieldRow {
121
+ return {
122
+ key,
123
+ value,
124
+ display: formatFieldDisplay(value),
125
+ kind: fieldKind(value),
126
+ children: childRows(value),
127
+ };
128
+ }
129
+
130
+ function childRows(value: unknown): readonly InputFieldRow[] | null {
131
+ if (Array.isArray(value)) {
132
+ if (value.length === 0) return null;
133
+ return value.map((item, index) => projectField(String(index), item));
134
+ }
135
+ if (value !== null && typeof value === "object") {
136
+ const entries = Object.entries(value as Record<string, unknown>);
137
+ if (entries.length === 0) return null;
138
+ return entries.map(([key, v]) => projectField(key, v));
139
+ }
140
+ return null;
141
+ }
142
+
99
143
  function formatFieldDisplay(value: unknown): string {
100
144
  if (value === null) return "null";
101
145
  if (typeof value === "string") return value;
102
146
  if (typeof value === "number" || typeof value === "boolean") return String(value);
103
- try {
104
- return JSON.stringify(value);
105
- } catch {
106
- return String(value);
147
+ if (Array.isArray(value)) {
148
+ if (value.length === 0) return "[]";
149
+ return `${value.length} ${value.length === 1 ? "item" : "items"}`;
150
+ }
151
+ if (typeof value === "object") return objectPreview(value as Record<string, unknown>);
152
+ if (typeof value === "bigint") return value.toString();
153
+ return typeof value;
154
+ }
155
+
156
+ /**
157
+ * Short collapsed preview: up to three short scalar fields, otherwise a count.
158
+ *
159
+ * @param value - Plain object
160
+ */
161
+ function objectPreview(value: Record<string, unknown>): string {
162
+ const keys = Object.keys(value);
163
+ if (keys.length === 0) return "{}";
164
+ const bits: string[] = [];
165
+ for (const [key, v] of Object.entries(value)) {
166
+ if (bits.length >= PREVIEW_FIELDS) break;
167
+ if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") continue;
168
+ const text = typeof v === "string" ? v : String(v);
169
+ if (text.length === 0 || text.length > PREVIEW_SCALAR_MAX) continue;
170
+ bits.push(`${key}: ${text}`);
107
171
  }
172
+ if (bits.length > 0) return bits.join(" · ");
173
+ return `${keys.length} ${keys.length === 1 ? "field" : "fields"}`;
108
174
  }