okengine 0.13.0 → 0.15.2

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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -6,6 +6,7 @@ import { HugeiconsIcon } from "@hugeicons/react";
6
6
  import type { JSX } from "react";
7
7
  import type { Effects } from "../../../../../../manifest/types.ts";
8
8
  import { SectionHead } from "@/components/explorer/section-head.tsx";
9
+ import { mcpCallChipLabel } from "@/features/flows/traces/effect-summary.ts";
9
10
  import {
10
11
  effectBarColor,
11
12
  effectKindIcon,
@@ -38,17 +39,24 @@ export function EffectsSummary({ effects }: EffectsSummaryProps): JSX.Element |
38
39
  <ul className="flex flex-wrap gap-1.5">
39
40
  {chips.map((c) => {
40
41
  const color = effectBarColor(c.kind);
41
- const Icon = effectKindIcon(c.kind);
42
+ const Icon = effectKindIcon(c.kind, c.ref);
43
+ const mcpLabel = c.kind === "call" ? mcpCallChipLabel(c.ref) : null;
42
44
  return (
43
45
  <li
44
46
  key={`${c.kind}:${c.ref}`}
45
47
  className="inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 font-mono text-[10px]"
46
48
  style={{ borderColor: `${color}55`, color }}
47
- title={`${EFFECT_KIND_LABEL[c.kind]} ${c.ref}`}
49
+ title={mcpLabel ?? `${EFFECT_KIND_LABEL[c.kind]} ${c.ref}`}
48
50
  >
49
51
  <HugeiconsIcon icon={Icon} className="size-3" />
50
- <span className="text-muted-foreground">{EFFECT_KIND_LABEL[c.kind]}</span>
51
- <span className="text-foreground/90">{c.ref}</span>
52
+ {mcpLabel ? (
53
+ <span className="text-foreground/90">{mcpLabel}</span>
54
+ ) : (
55
+ <>
56
+ <span className="text-muted-foreground">{EFFECT_KIND_LABEL[c.kind]}</span>
57
+ <span className="text-foreground/90">{c.ref}</span>
58
+ </>
59
+ )}
52
60
  </li>
53
61
  );
54
62
  })}
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import { splitContractInput } from "./contract-input.ts";
6
+ import { pickSeedFields, splitCallApiInput, splitContractInput } from "./contract-input.ts";
7
7
  import { fieldsFromSchema } from "./fields-from-schema.ts";
8
8
 
9
9
  const LIST_IN = fieldsFromSchema({
@@ -45,3 +45,208 @@ describe("splitContractInput", () => {
45
45
  expect(split.fields.map((f) => f.name)).toEqual(["title"]);
46
46
  });
47
47
  });
48
+
49
+ describe("splitCallApiInput", () => {
50
+ const idOnly = fieldsFromSchema({
51
+ type: "object",
52
+ required: ["id"],
53
+ properties: { id: { type: "string", minLength: 1 } },
54
+ });
55
+ const list = LIST_IN;
56
+ const nestedList = fieldsFromSchema({
57
+ type: "object",
58
+ required: ["id"],
59
+ properties: {
60
+ id: { type: "string", minLength: 1 },
61
+ q: { type: "string" },
62
+ limit: { type: "integer" },
63
+ },
64
+ });
65
+ const create = fieldsFromSchema({
66
+ type: "object",
67
+ required: ["title"],
68
+ properties: { title: { type: "string" }, spaceKey: { type: "string" } },
69
+ });
70
+ const assign = fieldsFromSchema({
71
+ type: "object",
72
+ required: ["id", "assigneeEmail"],
73
+ properties: { id: { type: "string" }, assigneeEmail: { type: "string" } },
74
+ });
75
+ const patch = fieldsFromSchema({
76
+ type: "object",
77
+ required: ["id"],
78
+ properties: {
79
+ id: { type: "string" },
80
+ title: { type: "string" },
81
+ status: { type: "string" },
82
+ },
83
+ });
84
+ const putDraft = fieldsFromSchema({
85
+ type: "object",
86
+ required: ["id", "title"],
87
+ properties: { id: { type: "string" }, title: { type: "string" }, body: { type: "string" } },
88
+ });
89
+ const search = fieldsFromSchema({
90
+ type: "object",
91
+ required: ["q"],
92
+ properties: { q: { type: "string" }, limit: { type: "integer" } },
93
+ });
94
+
95
+ const cases: readonly {
96
+ readonly name: string;
97
+ readonly method: string;
98
+ readonly pathParams: readonly string[];
99
+ readonly fields: ReturnType<typeof fieldsFromSchema>;
100
+ readonly path: readonly string[];
101
+ readonly query: readonly string[];
102
+ readonly body: readonly string[];
103
+ }[] = [
104
+ {
105
+ name: "GET tasks.get",
106
+ method: "GET",
107
+ pathParams: ["id"],
108
+ fields: idOnly,
109
+ path: ["id"],
110
+ query: [],
111
+ body: [],
112
+ },
113
+ {
114
+ name: "GET tasks.list",
115
+ method: "GET",
116
+ pathParams: [],
117
+ fields: list,
118
+ path: [],
119
+ query: ["q", "teamKey", "limit", "offset", "cursor", "orderBy", "order"],
120
+ body: [],
121
+ },
122
+ {
123
+ name: "GET comments.list /tasks/:id/comments",
124
+ method: "GET",
125
+ pathParams: ["id"],
126
+ fields: nestedList,
127
+ path: ["id"],
128
+ query: ["q", "limit"],
129
+ body: [],
130
+ },
131
+ {
132
+ name: "HEAD main.health",
133
+ method: "HEAD",
134
+ pathParams: [],
135
+ fields: [],
136
+ path: [],
137
+ query: [],
138
+ body: [],
139
+ },
140
+ {
141
+ name: "DELETE tasks.delete",
142
+ method: "DELETE",
143
+ pathParams: ["id"],
144
+ fields: idOnly,
145
+ path: ["id"],
146
+ query: [],
147
+ body: [],
148
+ },
149
+ {
150
+ name: "POST tasks.create",
151
+ method: "POST",
152
+ pathParams: [],
153
+ fields: create,
154
+ path: [],
155
+ query: [],
156
+ body: ["title", "spaceKey"],
157
+ },
158
+ {
159
+ name: "POST tasks.assign /tasks/:id/assign",
160
+ method: "POST",
161
+ pathParams: ["id"],
162
+ fields: assign,
163
+ path: ["id"],
164
+ query: [],
165
+ body: ["assigneeEmail"],
166
+ },
167
+ {
168
+ name: "POST tasks.archive path-only",
169
+ method: "POST",
170
+ pathParams: ["id"],
171
+ fields: idOnly,
172
+ path: ["id"],
173
+ query: [],
174
+ body: [],
175
+ },
176
+ {
177
+ name: "PATCH tasks.update",
178
+ method: "PATCH",
179
+ pathParams: ["id"],
180
+ fields: patch,
181
+ path: ["id"],
182
+ query: [],
183
+ body: ["title", "status"],
184
+ },
185
+ {
186
+ name: "PUT drafts.save",
187
+ method: "PUT",
188
+ pathParams: ["id"],
189
+ fields: putDraft,
190
+ path: ["id"],
191
+ query: [],
192
+ body: ["title", "body"],
193
+ },
194
+ {
195
+ name: "QUERY search.query",
196
+ method: "QUERY",
197
+ pathParams: [],
198
+ fields: search,
199
+ path: [],
200
+ query: [],
201
+ body: ["q", "limit"],
202
+ },
203
+ {
204
+ name: "OPTIONS leftover stays body",
205
+ method: "OPTIONS",
206
+ pathParams: [],
207
+ fields: search,
208
+ path: [],
209
+ query: [],
210
+ body: ["q", "limit"],
211
+ },
212
+ ];
213
+
214
+ for (const row of cases) {
215
+ test(row.name, () => {
216
+ const split = splitCallApiInput(row.fields, {
217
+ http: true,
218
+ method: row.method,
219
+ pathParams: row.pathParams,
220
+ });
221
+ expect(split.path).toEqual([...row.path]);
222
+ expect(split.query.map((f) => f.name)).toEqual([...row.query]);
223
+ expect(split.body.map((f) => f.name)).toEqual([...row.body]);
224
+ });
225
+ }
226
+
227
+ test("non-HTTP keeps the full input as body", () => {
228
+ const split = splitCallApiInput(idOnly);
229
+ expect(split.path).toEqual([]);
230
+ expect(split.query).toEqual([]);
231
+ expect(split.body.map((f) => f.name)).toEqual(["id"]);
232
+ });
233
+
234
+ test("Signal / CDC / Call-only never grow Path params from an id field", () => {
235
+ for (const kind of ["signal", "cdc", "internal"] as const) {
236
+ const split = splitCallApiInput(idOnly, { http: false, method: kind });
237
+ expect(split.path).toEqual([]);
238
+ expect(split.query).toEqual([]);
239
+ expect(split.body.map((f) => f.name)).toEqual(["id"]);
240
+ }
241
+ });
242
+ });
243
+
244
+ describe("pickSeedFields", () => {
245
+ test("keeps only named section keys", () => {
246
+ const title = fieldsFromSchema({
247
+ type: "object",
248
+ properties: { title: { type: "string" } },
249
+ });
250
+ expect(pickSeedFields({ id: "x", title: "n" }, title)).toEqual({ title: "n" });
251
+ });
252
+ });
@@ -2,6 +2,7 @@
2
2
  * Split flow input into query/path parameters vs body / filter fields.
3
3
  */
4
4
 
5
+ import { httpMethodHasBody } from "@/features/flows/traces/http-method.ts";
5
6
  import type { FormField } from "./fields-from-schema.ts";
6
7
 
7
8
  /** List / page / sort keys — query parameters, not resource fields. */
@@ -66,3 +67,57 @@ function pathParamField(name: string): FormField {
66
67
  required: true,
67
68
  };
68
69
  }
70
+
71
+ /** Call API request sections — path vs query vs JSON body. */
72
+ export type CallApiInputSplit = {
73
+ readonly path: readonly string[];
74
+ readonly query: readonly FormField[];
75
+ readonly body: readonly FormField[];
76
+ };
77
+
78
+ /**
79
+ * Partition `flow.in` the way the typed client puts fields on the wire.
80
+ *
81
+ * Path tokens never appear in query or body. `GET` / `HEAD` / `DELETE`
82
+ * leftover fields are query. Other HTTP verbs and non-HTTP triggers keep a
83
+ * JSON body (minus path tokens).
84
+ *
85
+ * @param input - `flow.in` fields
86
+ * @param options - Route tokens + HTTP method
87
+ */
88
+ export function splitCallApiInput(
89
+ input: readonly FormField[],
90
+ options: {
91
+ readonly pathParams?: readonly string[];
92
+ readonly method?: string | null;
93
+ readonly http?: boolean;
94
+ } = {},
95
+ ): CallApiInputSplit {
96
+ const pathParams = options.pathParams ?? [];
97
+ const pathSet = new Set(pathParams);
98
+ if (options.http !== true) {
99
+ return { path: [], query: [], body: input };
100
+ }
101
+ const leftover = input.filter((field) => !pathSet.has(field.name));
102
+ if (!httpMethodHasBody(options.method)) {
103
+ return { path: pathParams, query: leftover, body: [] };
104
+ }
105
+ return { path: pathParams, query: [], body: leftover };
106
+ }
107
+
108
+ /**
109
+ * Keep schema seed keys that belong to a Call API section.
110
+ *
111
+ * @param seed - Full `flow.in` seed
112
+ * @param fields - Section fields
113
+ */
114
+ export function pickSeedFields(
115
+ seed: Record<string, unknown>,
116
+ fields: readonly FormField[],
117
+ ): Record<string, unknown> {
118
+ const out: Record<string, unknown> = {};
119
+ for (const field of fields) {
120
+ if (Object.hasOwn(seed, field.name)) out[field.name] = seed[field.name];
121
+ }
122
+ return out;
123
+ }
@@ -110,5 +110,5 @@ export interface VaultRotateMasterResult {
110
110
  readonly kekVersion: number;
111
111
  readonly remaining: number;
112
112
  readonly masterKey: string | null;
113
- readonly at: number;
113
+ readonly at: string;
114
114
  }
@@ -16,6 +16,12 @@ describe("compose-health", () => {
16
16
  expect(composeRowToStatus({ State: "stopped" })).toBe("error");
17
17
  expect(composeRowToStatus({ Status: "Exited (0) 3 seconds ago" })).toBe("error");
18
18
  expect(composeRowToStatus({ State: "running" })).toBe("ready");
19
+ expect(composeRowToStatus({ State: "restarting" })).toBe("pending");
20
+ expect(composeRowToStatus({ State: "running", Status: "Up Less than a second" })).toBe(
21
+ "pending",
22
+ );
23
+ expect(composeRowToStatus({ State: "running", Status: "Up 2 seconds" })).toBe("pending");
24
+ expect(composeRowToStatus({ State: "running", Status: "Up 2 minutes" })).toBe("ready");
19
25
  expect(composeRowToStatus({ Status: "Up 2 seconds (health: starting)" })).toBe("pending");
20
26
  });
21
27
 
@@ -60,6 +66,27 @@ describe("compose-health", () => {
60
66
  expect(updates).toEqual(["store-sql:pending", "store-sql:ready"]);
61
67
  });
62
68
 
69
+ test("watchComposeHealth waits through compose restarting", async () => {
70
+ let calls = 0;
71
+ const map = await watchComposeHealth({
72
+ files: ["compose.yml"],
73
+ cwd: "/tmp",
74
+ env: {},
75
+ intervalMs: 1,
76
+ timeoutMs: 5_000,
77
+ sleep: async () => {},
78
+ run: async () => {
79
+ calls += 1;
80
+ if (calls === 1) {
81
+ return JSON.stringify([{ Service: "store-sql", State: "restarting" }]);
82
+ }
83
+ return JSON.stringify([{ Service: "store-sql", Health: "healthy", State: "running" }]);
84
+ },
85
+ });
86
+ expect(calls).toBe(2);
87
+ expect(map.get("store-sql")).toBe("ready");
88
+ });
89
+
63
90
  test("readComposeHealth / watch use ps -a", async () => {
64
91
  let seen: readonly string[] = [];
65
92
  await watchComposeHealth({
@@ -20,22 +20,27 @@ export type ComposePsRow = {
20
20
  export function composeRowToStatus(row: ComposePsRow): DevStatus {
21
21
  const health = (row.Health ?? "").trim().toLowerCase();
22
22
  const state = (row.State ?? "").trim().toLowerCase();
23
+ const statusText = (row.Status ?? "").toLowerCase();
23
24
  if (health === "healthy") return "ready";
24
25
  if (health === "unhealthy") return "error";
25
26
  if (health === "starting") return "pending";
26
- if (state === "running" || state === "up") return "ready";
27
+ if (state === "restarting") return "pending";
28
+ if (state === "running" || state === "up") {
29
+ // Right after `compose restart`, State is "running" before Health is
30
+ // "starting". Treat a fresh Up without (healthy) as still coming up.
31
+ if (isFreshComposeUp(statusText) && !statusText.includes("(healthy)")) return "pending";
32
+ return "ready";
33
+ }
27
34
  if (
28
35
  state === "exited" ||
29
36
  state === "dead" ||
30
37
  state === "stopped" ||
31
- state === "restarting" ||
32
38
  state === "removing" ||
33
39
  state === "oomkilled"
34
40
  ) {
35
41
  return "error";
36
42
  }
37
43
  // Status text for stopped containers (`Exited (0) 2 seconds ago`).
38
- const statusText = (row.Status ?? "").toLowerCase();
39
44
  if (statusText.startsWith("exited") || statusText.startsWith("dead")) return "error";
40
45
  if (state === "created" || state === "paused" || state.length === 0) return "pending";
41
46
  // Status text like "Up 3 seconds (healthy)"
@@ -48,6 +53,17 @@ export function composeRowToStatus(row: ComposePsRow): DevStatus {
48
53
  return "pending";
49
54
  }
50
55
 
56
+ /**
57
+ * True when compose `Status` is a just-started container (health not reported yet).
58
+ *
59
+ * @param statusText - Lowercased `Status` from `docker compose ps`
60
+ */
61
+ function isFreshComposeUp(statusText: string): boolean {
62
+ if (statusText.includes("less than a second")) return true;
63
+ const seconds = /up\s+(\d+)\s+seconds?/i.exec(statusText);
64
+ return seconds !== null && Number(seconds[1]) < 8;
65
+ }
66
+
51
67
  /**
52
68
  * Parse NDJSON or a JSON array from `docker compose ps --format json`.
53
69
  *
@@ -392,11 +392,13 @@ describe("image recipes", () => {
392
392
  expect(pgdogToml).toContain('host = "0.0.0.0"');
393
393
  expect(pgdogToml).toContain("port = 6432");
394
394
  expect(pgdogToml).toContain('pooler_mode = "transaction"');
395
+ expect(pgdogToml).toContain("default_pool_size = 20");
395
396
  expect(pgdogToml).toContain("[[databases]]");
396
397
  expect(pgdogToml).toContain('name = "oke"');
397
398
  expect(pgdogToml).toContain('host = "store-sql"');
398
399
  expect(pgdogToml).toContain("port = 5432");
399
400
  expect(pgdogToml).toContain('database_name = "oke"');
401
+ expect(pgdogToml).toContain("pool_size = 20");
400
402
 
401
403
  const usersToml = buildPgDogUsersToml({
402
404
  user: "oke",
@@ -936,6 +938,7 @@ describe("deriveInfrastructure", () => {
936
938
 
937
939
  const pgdogToml = result.files.find((f) => f.path === "pgdog/pgdog.toml")!.content;
938
940
  expect(pgdogToml).toContain('pooler_mode = "transaction"');
941
+ expect(pgdogToml).toContain("pool_size = 20");
939
942
  expect(pgdogToml).toContain('host = "store-sql"');
940
943
  expect(pgdogToml).not.toContain(fixedCreds["store.sql"].password);
941
944
 
@@ -957,7 +960,7 @@ describe("deriveInfrastructure", () => {
957
960
  test("Dockerfile CMD is oke start", () => {
958
961
  const df = emitDockerfile();
959
962
  expect(df).toContain('CMD ["oke", "start"]');
960
- expect(df).toContain("oven/bun:1.3");
963
+ expect(df).toContain("oven/bun:1.4");
961
964
  });
962
965
 
963
966
  test("writeDerivedFiles never writes compose.override.yml", async () => {
@@ -19,7 +19,7 @@ export interface DockerfileOptions {
19
19
  */
20
20
  export function emitDockerfile(options: DockerfileOptions = {}): string {
21
21
  const port = options.appPort ?? APP_PORT;
22
- const base = options.baseImage ?? "oven/bun:1.3";
22
+ const base = options.baseImage ?? "oven/bun:1.4";
23
23
  return `# generated by oke docker — do not edit
24
24
  FROM ${base} AS deps
25
25
  WORKDIR /app
@@ -35,12 +35,14 @@ export function buildPgDogToml(opts: {
35
35
  'host = "0.0.0.0"',
36
36
  "port = 6432",
37
37
  'pooler_mode = "transaction"',
38
+ "default_pool_size = 20",
38
39
  "",
39
40
  "[[databases]]",
40
41
  `name = ${tomlString(db)}`,
41
42
  `host = ${tomlString(host)}`,
42
43
  `port = ${port}`,
43
44
  `database_name = ${tomlString(db)}`,
45
+ "pool_size = 20",
44
46
  "",
45
47
  ].join("\n");
46
48
  }
@@ -29,6 +29,10 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
29
29
  const model = options.model ?? "claude-sonnet-4-20250514";
30
30
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
31
31
  const fetchFn = options.fetch ?? globalThis.fetch;
32
+ const preconnect = (fetchFn as { preconnect?: (href: string) => void }).preconnect;
33
+ if (typeof preconnect === "function") {
34
+ preconnect(baseUrl);
35
+ }
32
36
 
33
37
  return {
34
38
  driverId: "anthropic",
@@ -50,6 +54,7 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
50
54
  ...(system !== undefined ? { system } : {}),
51
55
  messages,
52
56
  }),
57
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
53
58
  });
54
59
  const raw = (await res.json().catch(() => ({}))) as AnthropicMessagesResponse;
55
60
  if (!res.ok) {
@@ -156,10 +156,7 @@ export async function openOllama(options: AiOpenOptions = {}): Promise<AiModelCl
156
156
  const msg = raw.error ?? `ollama HTTP ${res.status}`;
157
157
  throw new OllamaUnavailableError(`ollama: ${msg}`);
158
158
  }
159
- if (!res.body) {
160
- throw new OllamaUnavailableError("ollama: stream response has no body");
161
- }
162
- yield* readOllamaNdjson(res.body, opts.signal);
159
+ yield* readOllamaNdjson(res, opts.signal);
163
160
  },
164
161
  async embed(opts: AiEmbedOptions): Promise<AiEmbedResult> {
165
162
  const resolvedModel = resolveOllamaModel(options, opts.model);
@@ -265,45 +262,45 @@ function parseOllamaToolCalls(
265
262
  }
266
263
 
267
264
  /**
268
- * Parse Ollama NDJSON stream lines.
265
+ * Parse Ollama NDJSON stream lines via {@link Bun.JSONL.parseChunk}.
269
266
  *
270
- * @param body - Response body
267
+ * @param res - Streaming response
271
268
  * @param signal - Optional abort
272
269
  */
273
270
  async function* readOllamaNdjson(
274
- body: ReadableStream<Uint8Array>,
271
+ res: Response,
275
272
  signal?: AbortSignal,
276
273
  ): AsyncGenerator<AiStreamChunk> {
277
- const reader = body.getReader();
274
+ const reader = res.body?.getReader();
275
+ if (!reader) {
276
+ throw new OllamaUnavailableError("ollama: stream response has no body");
277
+ }
278
278
  const decoder = new TextDecoder();
279
- let buffer = "";
279
+ let pending = "";
280
280
  try {
281
281
  while (true) {
282
- if (signal?.aborted) {
283
- throw abortAsError(signal.reason);
284
- }
282
+ if (signal?.aborted) throw abortAsError(signal.reason);
285
283
  const { done, value } = await reader.read();
286
284
  if (done) break;
287
- buffer += decoder.decode(value, { stream: true });
288
- const lines = buffer.split("\n");
289
- buffer = lines.pop() ?? "";
290
- for (const line of lines) {
291
- const trimmed = line.trim();
292
- if (!trimmed) continue;
293
- try {
294
- const chunk = JSON.parse(trimmed) as OllamaChatResponse;
295
- const delta = chunk.message?.content;
296
- if (typeof delta === "string" && delta.length > 0) {
297
- yield { text: delta };
298
- }
299
- if (chunk.done) {
300
- yield { text: "", done: true };
301
- return;
302
- }
303
- } catch {
304
- // ignore malformed lines
285
+ pending += decoder.decode(value, { stream: true });
286
+ const parsed = bunJsonl().parseChunk(pending);
287
+ pending = pending.slice(parsed.read);
288
+ for (const row of parsed.values) {
289
+ const chunk = row as OllamaChatResponse;
290
+ const delta = chunk.message?.content;
291
+ if (typeof delta === "string" && delta.length > 0) {
292
+ yield { text: delta };
293
+ }
294
+ if (chunk.done) {
295
+ yield { text: "", done: true };
296
+ return;
305
297
  }
306
298
  }
299
+ if (parsed.error) {
300
+ const nl = pending.indexOf("\n");
301
+ if (nl === -1) continue;
302
+ pending = pending.slice(nl + 1);
303
+ }
307
304
  }
308
305
  yield { text: "", done: true };
309
306
  } finally {
@@ -311,6 +308,28 @@ async function* readOllamaNdjson(
311
308
  }
312
309
  }
313
310
 
311
+ interface BunJsonlParse {
312
+ readonly values: readonly unknown[];
313
+ readonly read: number;
314
+ readonly done: boolean;
315
+ readonly error: unknown;
316
+ }
317
+
318
+ function bunJsonl(): {
319
+ parse(text: string): unknown[];
320
+ parseChunk(text: string): BunJsonlParse;
321
+ } {
322
+ const jsonl = (
323
+ Bun as typeof Bun & {
324
+ JSONL?: { parse: (t: string) => unknown[]; parseChunk: (t: string) => BunJsonlParse };
325
+ }
326
+ ).JSONL;
327
+ if (!jsonl) {
328
+ throw new Error("ollama: Bun.JSONL is required (Bun >= 1.4.0)");
329
+ }
330
+ return jsonl;
331
+ }
332
+
314
333
  /**
315
334
  * Probe Ollama — fail loud before the first completion.
316
335
  *