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
@@ -17,6 +17,7 @@ oke --help # command list (also what bare `oke` prints whe
17
17
 
18
18
  oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535 · docs MCP :6536
19
19
  # → always Docker Compose under docker/ + host Bun
20
+ # → regenerates flows/generated.ts when the flows tree changes
20
21
  # → client types + domain schema push on save
21
22
  # → Console UI hot-reloads from source (okengine checkout; no bun run build)
22
23
  oke dev --no-db-push # opt out of auto oke db push on schema input save
@@ -48,7 +49,7 @@ oke schema generate # core + plugin stubs → .oke/schema/oke.ts (-
48
49
  oke db push # domain schema.ts → live DB (dev; drizzle-kit)
49
50
  oke db generate # versioned SQL under drizzle/ (review)
50
51
  oke db migrate # apply migrations (explicit; never auto in prod)
51
- oke db seed # this app's defineSeed (name + essential + env); never at boot
52
+ oke db seed # this app's defineSeed (name + essential + env) standard CLI path
52
53
  oke db seed --env prod --force # CI: skip prod confirmation prompt
53
54
  oke db studio # drizzle-kit Studio (long-running)
54
55
  oke vault set STRIPE_KEY # also: list · import .env · key rotate
@@ -112,24 +113,26 @@ not spawn a second stack. Non-TTY / CI keeps the classic help listing
112
113
  `oke dev` always starts Compose (requires a running Docker daemon). Boot chrome
113
114
  prints immediately (wordmark + Starting + profile), then streams background work
114
115
  (compose up, vault, per-service health, AI probe). Status ● is green ready ·
115
- yellow pending/loading · red error · dim idle. Compose health keeps polling
116
+ yellow pending/loading · red error · dim idle. After bind, Backend · Console ·
117
+ MCP · Docs MCP each get a box (URL + one-line purpose). Compose health keeps polling
116
118
  (`docker compose ps -a`); AI ● tracks model phase while the AI container is up.
117
119
  Boot does not wait for the model to become ready. A successful session writes
118
120
  `.oke/dev.json` (pid · ports · startedAt) and clears it on stop. Durable local
119
- markers live in `.oke/state.json` (`seededAt` + `seed` after the one-shot
120
- prompt for this app's `defineSeed({ name })`) — not in the session lock. Console session signing uses
121
+ markers live in `.oke/state.json` not in the session lock. Console session signing uses
121
122
  `.oke/console.secret` (or `OKE_CONSOLE_SECRET`); that is not a Vault secret.
122
123
 
123
124
  On a TTY, Ink keyboard controls stay active after boot (`useInput` — same
124
125
  shortcuts as before). Press `r` to clear the log pane and reprint the latest
125
126
  board.
126
127
 
127
- | Key | Action |
128
- | --------- | ----------------------------------- |
129
- | `?` | help |
130
- | `r` | refresh — clear logs, show latest ● |
131
- | `q` | quit `oke dev` |
132
- | `u` / `x` | compose up / stop (whole stack) |
128
+ | Key | Action |
129
+ | --- | ----------------------------------- |
130
+ | `?` | help |
131
+ | `r` | refresh — clear logs, show latest ● |
132
+ | `s` | `oke db seed` |
133
+ | `q` | quit `oke dev` |
134
+ | `u` | compose up (whole stack) |
135
+ | `x` | compose stop (whole stack) |
133
136
 
134
137
  Compose stays quiet unless it fails. Process-local boot notices print once under
135
138
  the hero (not once per process).
@@ -5,7 +5,7 @@ icon: "MonitorSmartphone"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- `okengine/client` is how a browser, CLI, or another service calls your app's flows. Adopt the unit, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
8
+ `okengine/client` is how a browser, CLI, or another service calls your app's flows. Import the generated barrel, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
9
9
 
10
10
  <Callout title="The one rule">
11
11
  Treat every call as a result envelope: `{ data, error }`. Flow failures are values you switch on
@@ -21,19 +21,22 @@ source: "docs/spec/unified-theory.md"
21
21
  ### Adopt flows and export `App`
22
22
 
23
23
  ```typescript title="src/app.ts"
24
- import { oke } from "okengine";
25
- import * as main from "./flows/main";
24
+ import "@/core";
25
+ import "@/flows/generated";
26
+ import { oke } from "okengine/http";
26
27
 
27
- export const app = oke({ name: "standard" }).adopt({ main });
28
+ export const app = oke({ name: "notes" });
28
29
  export type App = typeof app;
29
30
  ```
30
31
 
32
+ `.adopt({ notes })` is optional and additive.
33
+
31
34
  </Step>
32
35
 
33
36
  <Step>
34
37
  ### Create the client
35
38
 
36
- Same repo — pass the app value so HTTP triggers hit REST (method + path from adopt):
39
+ Same repo — pass the app value so HTTP triggers hit REST (method + path from `$routes`):
37
40
 
38
41
  ```typescript title="client"
39
42
  import { createClient } from "okengine/client";
@@ -114,7 +117,7 @@ oke client add https://api.example.com --out ./types/oke-client.d.ts
114
117
  | Incomplete proxy path (`api.notes()` with no flow) | Result error: `Incomplete path: api.notes(…)` |
115
118
 
116
119
  ```typescript
117
- // REST — createClient(app, url) saw method/path from on(http.get("/notes/:id"), )
120
+ // REST — method/path from the bound HTTP trigger (file tree or explicit)
118
121
  await api.notes.get({ id: "n_1" }); // GET /notes/n_1
119
122
 
120
123
  // RPC — untriggered flow named notes.stats
@@ -210,16 +213,16 @@ core today: cookie jars or plugin `.client()` decorations. Browser apps: also se
210
213
  outcome by calling a Flow that uses it — or by handling a gate denial on that call.
211
214
  </Callout>
212
215
 
213
- | Element | On the client | How |
214
- | --------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
215
- | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
216
- | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
217
- | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows you adopt |
218
- | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
219
- | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
220
- | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
221
- | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
222
- | [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
216
+ | Element | On the client | How |
217
+ | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------ |
218
+ | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
219
+ | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
220
+ | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows on `$routes` |
221
+ | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
222
+ | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
223
+ | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
224
+ | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
225
+ | [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
223
226
 
224
227
  ### Store resources
225
228
 
@@ -227,7 +230,7 @@ Mount a resource, adopt the returned ops, then call the five Flows like any othe
227
230
 
228
231
  ```typescript
229
232
  const notesR = store.resource(db, notes, {/* in, out, list */});
230
- const mounted = on(http.resource("/notes", notesR.all()).gate.public.live());
233
+ const mounted = on(http.resource("/notes", notesR.all()).public().live());
231
234
  // .adopt({ notes: mounted }) →
232
235
  const page = await api.notes.list({ limit: 20 });
233
236
  const more = await page.next();
@@ -280,9 +283,9 @@ Budget: the `./client` export stays under the measured client-runtime cap (hard
280
283
 
281
284
  <Accordion title="api.main.health is not a function / type error">
282
285
 
283
- Confirm the flow is `export`ed from the module you `.adopt({ main })`, and that `createClient` is
284
- typed with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart
285
- `oke dev` after renaming exports.
286
+ Confirm the Flow is `export`ed from a generated unit (`import "@/flows/generated"` then `oke({ name })`), or from a module you still `.adopt({ main })`.
287
+
288
+ Type `createClient` with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart `oke dev` after renaming exports.
286
289
 
287
290
  </Accordion>
288
291
 
@@ -321,7 +324,8 @@ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no r
321
324
 
322
325
  ## Learn more
323
326
 
324
- - [Basic usage](/docs/get-started/basic-usage) — adopt client test loop
327
+ - [Project structure](/docs/get-started/project-structure) — folders are the URL; `$routes` without `.adopt()`
328
+ - [Basic usage](/docs/get-started/basic-usage) — generated barrel → client → test loop
325
329
  - [Gate](/docs/elements/gate) — policies, `gate.public`, denials
326
330
  - [Store](/docs/elements/store) — `store.resource` and list query language
327
331
  - [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
@@ -33,9 +33,10 @@ See [Store](/docs/elements/store) for the query-builder surface.
33
33
 
34
34
  ## Signals
35
35
 
36
- | Signature | Records | Notes |
37
- | ------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
- | `fx.emit(signal, payload?, { key? })` | `emit` | Commits the signal outbox when the call resolves; optional `key` serializes `once` per key; stamps producer run id as `parentRunId` for trace chains; throws **OKE1042** (orphan) or **OKE1043** (schema) |
36
+ | Signature | Records | Notes |
37
+ | ------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | `fx.emit(signal, payload?, { key? })` | `emit` | Commits the signal outbox when the call resolves; optional `key` serializes `once` per key; stamps producer run id as `parentRunId` for trace chains; throws **OKE1042** (orphan) or **OKE1043** (schema) |
39
+ | `fx.deadLetters(signal)` | `read` `signal:<name>` | Dead-lettered messages for that signal. Payload typed from `SignalDecl<T>`. Page with `fx.json.withQuery`. Cross-signal throws **OKE1001**. |
39
40
 
40
41
  ## Runs (observability read)
41
42
 
@@ -145,12 +146,12 @@ contact a provider. Channel bodies use `{{field}}` catalogs — not ICU (see [Ch
145
146
 
146
147
  ## AI
147
148
 
148
- | Signature | Records | Returns |
149
- | ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
150
- | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
151
- | `fx.run(agent, input?)` | `ask` | Agent result |
152
- | `fx.stream(model, { prompt?, data? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` |
153
- | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
149
+ | Signature | Records | Returns |
150
+ | ----------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
151
+ | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
152
+ | `fx.run(agent, input?)` | `ask` | Agent result |
153
+ | `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
154
+ | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
154
155
 
155
156
  AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
156
157
 
@@ -169,12 +170,15 @@ AI calls are nondeterministic: journaling is forced on and auto-cache disabled a
169
170
 
170
171
  ## Clock
171
172
 
172
- | Signature | Notes |
173
- | --------------------------------- | ----------------------------------------------------- |
174
- | `fx.clock.now()` | Epoch-ms, injectable — the only legal "now" |
175
- | `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
173
+ | Signature | Notes |
174
+ | --------------------------------- | ------------------------------------------------------------------- |
175
+ | `fx.clock.now()` | Epoch-ms, injectable — the only legal "now" |
176
+ | `fx.clock.ago(duration)` | Instant before now (`"30d"` now 30 days) |
177
+ | `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days) |
178
+ | `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`) |
179
+ | `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
176
180
 
177
- Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`.
181
+ Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a calendar day. Unknown strings parse as `0`.
178
182
 
179
183
  ## Cache
180
184
 
@@ -192,15 +196,16 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
192
196
 
193
197
  ## Responses
194
198
 
195
- | Helper | Status | Body |
196
- | ----------------------------------------- | ------ | ----------------------------------------------------------- |
197
- | `fx.json.ok(value, { meta? })` | 200 | `{ data, meta?, error: null }` |
198
- | `fx.json.create(value)` | 201 | `{ data, error: null }` |
199
- | `fx.json.empty()` | 204 | no body |
200
- | `fx.json.with(page)` / `with(data, meta)` | 200 | `{ data, meta, error: null }` — already-built pager |
201
- | `fx.json.withQuery(rows, input, spec?)` | 200 | In-memory list page — zero-config `q` / auto-eq / PostgREST |
199
+ | Helper | Status | Body |
200
+ | ----------------------------------------- | ------ | -------------------------------------------------------------- |
201
+ | `fx.json.ok(value, { meta? })` | 200 | `{ data, meta?, error: null }` |
202
+ | `fx.json.create(value)` | 201 | `{ data, error: null }` |
203
+ | `fx.json.empty()` | 204 | no body |
204
+ | `fx.json.with(page)` / `with(data, meta)` | 200 | `{ data, meta, error: null }` — already-built pager |
205
+ | `fx.json.withQuery(rows, input, spec?)` | 200 | In-memory list page — zero-config `q` / auto-eq / PostgREST |
206
+ | `fx.json.stream(chunks)` | 200 | `text/event-stream` — JSON `data:` frames, then `data: [DONE]` |
202
207
 
203
- Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control.
208
+ Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control. Pass `fx.stream(...)` into `fx.json.stream` to reach the HTTP client token-by-token.
204
209
 
205
210
  ## Logging, i18n, ids
206
211
 
@@ -41,7 +41,7 @@ Our path is concrete: a booking name containing "ignore previous instructions an
41
41
  #### 10.4 Remaining closures
42
42
 
43
43
  - **`invoke-as` is attenuated** exactly like an API key: an operator cannot assume a scope set they could not grant. Impersonating a real user is development-only. Console `POST /console/flows/invoke` runs the target flow on a bound host under the selected identity’s scopes (`trustedInvoke` is in-process only — never from public HTTP); when the host adapter is unbound, invoke fails closed.
44
- - **Store SQL browse** masks classified PII by default. `QUERY /console/store/query` with `revealPii: true` returns cleartext for the current page and writes an audited `console.store.query.reveal` log (operator, ref, child). The SQL query console (`POST /console/store/sql`) stays masked unless `revealPii: true`, which writes `console.store.sql.reveal` (operator, ref). Call API (`POST /console/flows/invoke`) masks the same classified field names on the handler response unless `revealPii: true`, which writes `console.flows.invoke.reveal` and opens the host store session with cleartext so columns the handler reads (for example `ownerEmail`) are not already `[redacted]`. `asGate` (a Manifest policy gate or `public`) sets `oke.gate` and `row_security` on postgres / pglite and writes `console.store.sql.asGate`; Operator (omit `asGate`) still bypasses RLS. Per-cell `POST /console/store/reveal` remains available. The Console auth schema (`sql:oke_console` — `oke_operators`, sessions, roles, keys) is hidden from Store browse unless `OKE_CONSOLE_AUTH_STORE=1`. When listed it is **read-only**; edit / delete / raw SQL are refused so operator-plane rows are never joined to `public`.
44
+ - **Store SQL browse** masks classified PII by default. `QUERY /console/store/query` with `revealPii: true` returns cleartext for the current page and writes an audited `console.store.query.reveal` log (operator, ref, child). The SQL query console (`POST /console/store/sql`) stays masked unless `revealPii: true`, which writes `console.store.sql.reveal` (operator, ref). Call API (`POST /console/flows/invoke`) masks the same classified field names on the handler response unless `revealPii: true`, which writes `console.flows.invoke.reveal` and opens the host store session with cleartext so columns the handler reads (for example `ownerEmail`) are not already `[redacted]`. `{ asGate, asUserId }` on SQL, browse, row edit, and Call API stamps `oke.gate()` / `oke.user()` / `oke.has_scope()` on postgres / pglite (one identity bag; policy-only leaves `oke.user()` empty — never `gate:${name}`); Operator / omit / `bypassGates` still bypasses RLS. The response may include `rls: { gate, userId, applied }`. Catalog DDL stays Operator. Per-cell `POST /console/store/reveal` remains available. The Console auth schema (`sql:oke_console` — `oke_operators`, sessions, roles, keys) is hidden from Store browse unless `OKE_CONSOLE_AUTH_STORE=1`. When listed it is **read-only**; edit / delete / raw SQL are refused so operator-plane rows are never joined to `public`.
45
45
  - **Host WideEvent ingest** (`POST /console/runs/ingest`, `oke dev` only) is secret-gated and never echoes the event body. Operators read runs only through `projectRun` / `maskWideEventForConsole` on `GET /console/runs` and `/console/live` — the same PII boundary as Console-local runs.
46
46
  - **Store SQL performance** (`QUERY /console/store/sql/stats`, `/locks`, `POST /console/store/sql/advise`) is engine-native query text — not Store browse masking. Statements show `pg_stat_statements` fingerprints (`$n`). Live `pg_stat_activity.query` is collapsed until `revealPii: true`, which writes `console.store.sql.stats.reveal` (operator, ref). Named limitation `StoreSqlStatsQueryTextGap`. This surface does not terminate sessions or call `pg_stat_statements_reset`.
47
47
  - **Store KV performance** (`QUERY /console/store/kv/stats`) is Redis-wire `INFO` / `COMMANDSTATS` / `SLOWLOG` / `LATENCY` — not Store browse. INFO is instance-wide (`StoreKvStatsServerWideGap`). SLOWLOG args are keys and values — collapsed until `revealPii: true`, which writes `console.store.kv.stats.reveal` (operator, ref). Named limitation `StoreKvStatsSlowlogArgsGap`. `memory` returns `KvStatsUnsupported`. This surface does not run `MONITOR` and does not invent a hot-key table.
@@ -275,10 +275,13 @@ export function createAuthHttpBindings(
275
275
  } = { refresh, revoke, me };
276
276
  const bindings: Binding[] = [
277
277
  bindAuthHttp(
278
- http.post(`${base}/refresh`).gate.public.gate(...(refreshRate ? [refreshRate] : [])),
278
+ http
279
+ .post(`${base}/refresh`)
280
+ .public()
281
+ .gate(...(refreshRate ? [refreshRate] : [])),
279
282
  refresh,
280
283
  ),
281
- bindAuthHttp(http.post(`${base}/revoke`).gate.public, revoke),
284
+ bindAuthHttp(http.post(`${base}/revoke`).public(), revoke),
282
285
  bindAuthHttp(http.get(`${base}/me`).gate(AUTH_SESSION_GATE), me),
283
286
  ];
284
287
 
@@ -371,11 +374,17 @@ export function createAuthHttpBindings(
371
374
  flows.signUpEmail = signUpEmail;
372
375
  bindings.push(
373
376
  bindAuthHttp(
374
- http.post(`${base}/sign-in/email`).gate.public.gate(...(signInRate ? [signInRate] : [])),
377
+ http
378
+ .post(`${base}/sign-in/email`)
379
+ .public()
380
+ .gate(...(signInRate ? [signInRate] : [])),
375
381
  signInEmail,
376
382
  ),
377
383
  bindAuthHttp(
378
- http.post(`${base}/sign-up/email`).gate.public.gate(...(signUpRate ? [signUpRate] : [])),
384
+ http
385
+ .post(`${base}/sign-up/email`)
386
+ .public()
387
+ .gate(...(signUpRate ? [signUpRate] : [])),
379
388
  signUpEmail,
380
389
  ),
381
390
  );
@@ -406,7 +415,7 @@ export function isSessionFresh(
406
415
  * @param flowDef - Flow
407
416
  */
408
417
  export function bindAuthHttp(trigger: HttpTrigger, flowDef: AnyFlowDef): Binding {
409
- const withPosture = trigger.gates.length > 0 ? trigger : trigger.gate.public;
418
+ const withPosture = trigger.gates.length > 0 ? trigger : trigger.public();
410
419
  const normalized = normalizeTrigger(withPosture);
411
420
  const list = flowDef.triggers as Trigger[];
412
421
  list.push(normalized);
@@ -86,9 +86,16 @@ export function assertCrossPlane(
86
86
 
87
87
  function inferFlowFromPath(path: string): string | undefined {
88
88
  // …/flows/bookings/create.ts → bookings.create
89
- const m = /flows\/([^/]+)\/([^/]+)\.(ts|tsx|js)$/.exec(path.replace(/\\/g, "/"));
90
- if (m) return `${m[1]}.${m[2]}`;
91
- return undefined;
89
+ // …/flows/notes/[id]/get.ts → notes.get (never notes.id.get)
90
+ const posix = path.replace(/\\/g, "/");
91
+ const m = /(?:^|\/)flows\/([^/]+)\/(.+)\.(ts|tsx|js)$/.exec(posix);
92
+ if (!m) return undefined;
93
+ const unit = m[1]!;
94
+ const rest = m[2]!;
95
+ const leaf = rest.split("/").pop();
96
+ if (!leaf || leaf === "index" || leaf.startsWith("_")) return undefined;
97
+ if (leaf === "shapes" || leaf === "signals" || leaf.endsWith(".test")) return undefined;
98
+ return `${unit}.${leaf}`;
92
99
  }
93
100
 
94
101
  function inferPlaneFromSource(source: string): "user" | "operator" | undefined {
@@ -167,6 +167,68 @@ export default defineConfig({
167
167
  rmSync(dir, { recursive: true, force: true });
168
168
  }
169
169
  });
170
+
171
+ test("applyAiSetup prefers an existing src/core/ai.ts over a thin core.ts barrel", () => {
172
+ const dir = mkdtempSync(join(tmpdir(), "oke-ai-setup-split-"));
173
+ try {
174
+ writeFileSync(
175
+ join(dir, "oke.config.ts"),
176
+ `import { defineConfig } from "okengine/config";
177
+ export default defineConfig({
178
+ drivers: {
179
+ channel: {
180
+ email: {
181
+ dev: "smtp",
182
+ test: "console",
183
+ prod: "smtp",
184
+ },
185
+ },
186
+ },
187
+ images: {
188
+ store: {
189
+ sql: "postgres:18-alpine",
190
+ },
191
+ },
192
+ });
193
+ `,
194
+ "utf8",
195
+ );
196
+ mkdirSync(join(dir, "src", "core"), { recursive: true });
197
+ writeFileSync(
198
+ join(dir, "src", "core.ts"),
199
+ `export * from "./core/store.ts";\nexport * from "./core/ai.ts";\n`,
200
+ "utf8",
201
+ );
202
+ writeFileSync(
203
+ join(dir, "src", "core", "store.ts"),
204
+ `import { store } from "okengine";\n\nexport const db = store.sql("app");\n`,
205
+ "utf8",
206
+ );
207
+ writeFileSync(join(dir, "src", "core", "ai.ts"), `import { ai } from "okengine";\n`, "utf8");
208
+ writeFileSync(
209
+ join(dir, "src", "app.ts"),
210
+ `import "@/core";\nexport const app = {};\n`,
211
+ "utf8",
212
+ );
213
+
214
+ applyAiSetup(dir, {
215
+ driver: "ollama",
216
+ baseUrl: "http://127.0.0.1:11434",
217
+ chatModel: "gemma4:e4b",
218
+ visionModel: "qwen3-vl:4b",
219
+ embedModel: "nomic-embed-text",
220
+ });
221
+
222
+ const barrel = readFileSync(join(dir, "src", "core.ts"), "utf8");
223
+ expect(barrel).not.toContain("ai.model");
224
+ expect(barrel).toContain('export * from "./core/ai.ts"');
225
+ const aiTs = readFileSync(join(dir, "src", "core", "ai.ts"), "utf8");
226
+ expect(aiTs).toContain("smart");
227
+ expect(aiTs).toContain('ai.model("vision"');
228
+ } finally {
229
+ rmSync(dir, { recursive: true, force: true });
230
+ }
231
+ });
170
232
  });
171
233
 
172
234
  describe("parseAiSetupArgs", () => {
@@ -244,7 +244,9 @@ export function renderAiTs(input: AiSetupApplyInput): string {
244
244
  }
245
245
 
246
246
  /**
247
- * Write AI model declarations into `src/core.ts` (preferred) or legacy `src/core/ai.ts`.
247
+ * Write AI model declarations into `src/core/ai.ts` when that split exists
248
+ * (so a thin `src/core.ts` barrel stays a re-export), else `src/core.ts`,
249
+ * else legacy `src/core/index.ts` + sidecar.
248
250
  *
249
251
  * @param cwd - Project root
250
252
  * @param input - Setup choices
@@ -252,9 +254,25 @@ export function renderAiTs(input: AiSetupApplyInput): string {
252
254
  */
253
255
  function writeAiModels(cwd: string, input: AiSetupApplyInput): string {
254
256
  const rendered = renderAiTs(input);
257
+ const coreAiPath = join(cwd, "src", "core", "ai.ts");
255
258
  const coreTsPath = join(cwd, "src", "core.ts");
256
259
  const legacyIndex = join(cwd, "src", "core", "index.ts");
257
260
 
261
+ if (existsSync(coreAiPath)) {
262
+ const existing = readFileSync(coreAiPath, "utf8");
263
+ if (hasAiModels(existing)) {
264
+ const withPrompt = ensureSummarizeNotePrompt(existing);
265
+ if (withPrompt !== existing) {
266
+ writeFileSync(coreAiPath, withPrompt, "utf8");
267
+ }
268
+ } else {
269
+ writeFileSync(coreAiPath, mergeAiIntoCore(existing, rendered), "utf8");
270
+ }
271
+ ensureCoreBarrelExportsAi(cwd);
272
+ ensureCoreImported(cwd);
273
+ return coreAiPath;
274
+ }
275
+
258
276
  // Folder layout still in the wild — keep writing a sidecar.
259
277
  if (!existsSync(coreTsPath) && existsSync(legacyIndex)) {
260
278
  const aiTsPath = join(cwd, "src", "core", "ai.ts");
@@ -364,6 +382,19 @@ export function ensureNamedOkengineImport(source: string, name: string): string
364
382
  return source.replace(re, `import { ${sorted.join(", ")} } from "okengine";`);
365
383
  }
366
384
 
385
+ /**
386
+ * Keep a `src/core.ts` barrel re-exporting `./core/ai.ts` after a split write.
387
+ *
388
+ * @param cwd - Project root
389
+ */
390
+ function ensureCoreBarrelExportsAi(cwd: string): void {
391
+ const coreTsPath = join(cwd, "src", "core.ts");
392
+ if (!existsSync(coreTsPath)) return;
393
+ const src = readFileSync(coreTsPath, "utf8");
394
+ if (/from\s+["']\.\/core\/ai/.test(src)) return;
395
+ writeFileSync(coreTsPath, `${src.trimEnd()}\nexport * from "./core/ai.ts";\n`, "utf8");
396
+ }
397
+
367
398
  /**
368
399
  * Ensure `src/app.ts` loads `@/core` / `./core` so merged AI registers.
369
400
  *
@@ -197,4 +197,29 @@ describe("formatModelRow", () => {
197
197
  expect(row).toContain("text · code");
198
198
  expect(row).not.toContain(" · ≈");
199
199
  });
200
+
201
+ test("Arabic and CJK labels keep Caps aligned", () => {
202
+ const header = formatModelTableHeader();
203
+ const capsWidth = Bun.stringWidth(header.slice(0, header.indexOf("Caps")));
204
+ const ar = formatModelRow({
205
+ id: "ar",
206
+ label: "مرحبا بالعالم",
207
+ hint: "",
208
+ role: "chat",
209
+ ramGb: 8,
210
+ tier: "fast",
211
+ modalities: ["text"],
212
+ });
213
+ const cjk = formatModelRow({
214
+ id: "cjk",
215
+ label: "你好世界模型",
216
+ hint: "",
217
+ role: "chat",
218
+ ramGb: 8,
219
+ tier: "fast",
220
+ modalities: ["text"],
221
+ });
222
+ expect(Bun.stringWidth(ar.slice(0, ar.indexOf("text")))).toBe(capsWidth);
223
+ expect(Bun.stringWidth(cjk.slice(0, cjk.indexOf("text")))).toBe(capsWidth);
224
+ });
200
225
  });
@@ -195,9 +195,14 @@ const MODEL_COL_RAM = 7;
195
195
  * @param width - Fixed width
196
196
  */
197
197
  function clipPad(value: string, width: number): string {
198
- if (value.length === width) return value;
199
- if (value.length < width) return value.padEnd(width);
200
- return `${value.slice(0, Math.max(0, width - 1))}…`;
198
+ const bunAnsi = Bun as typeof Bun & {
199
+ stringWidth(text: string): number;
200
+ sliceAnsi(text: string, start: number, end: number, omission?: string): string;
201
+ };
202
+ const display = bunAnsi.stringWidth(value);
203
+ if (display === width) return value;
204
+ if (display < width) return value + " ".repeat(width - display);
205
+ return bunAnsi.sliceAnsi(value, 0, width, "…");
201
206
  }
202
207
 
203
208
  /**
@@ -28,7 +28,7 @@ describe("attachHostToConsole", () => {
28
28
  import { oke, on, flow, http, gate, store } from ${JSON.stringify(OKE)};
29
29
  const db = store.sql("db");
30
30
  export const ping = on(
31
- http.get("/ping").gate.public,
31
+ http.get("/ping").public(),
32
32
  flow("main.ping", { do: () => ({ ok: true }) }),
33
33
  );
34
34
  export const app = oke({
@@ -69,6 +69,7 @@ export const app = oke({
69
69
  expect(ingested.some((row) => row.event?.flow === "main.ping")).toBe(true);
70
70
  await attached!.stop();
71
71
  expect(state.invokeUserFlow).toBeNull();
72
+ expect(state.storeRuntime).toBeNull();
72
73
  });
73
74
 
74
75
  test("binds host aiRuntime so Console projection sees a journaled ask", async () => {
@@ -81,7 +82,7 @@ import { oke, on, flow, http, gate, ai } from ${JSON.stringify(OKE)};
81
82
  const smart = ai.model("smart", { provider: "mock" });
82
83
  smart.prompt("ticket-triage", { version: 2 });
83
84
  export const ping = on(
84
- http.get("/ping").gate.public,
85
+ http.get("/ping").public(),
85
86
  flow("main.ping", {
86
87
  effects: { asks: ["ticket-triage"] },
87
88
  do: (_input, fx) => fx.ask("ticket-triage", { subject: "hello" }),
@@ -73,6 +73,9 @@ export async function attachHostToConsole(
73
73
  return {
74
74
  stop: async () => {
75
75
  options.state.invokeUserFlow = null;
76
+ options.state.storeRuntime = null;
77
+ options.state.vaultRuntime = null;
78
+ options.state.aiRuntime = null;
76
79
  await host.stop();
77
80
  },
78
81
  };
@@ -32,8 +32,10 @@ describe("oke build — .adopt() barrel regeneration", () => {
32
32
  expect(code).toBe(0);
33
33
 
34
34
  const after = await readFile(generatedPath, "utf8");
35
- expect(after).toContain('export * as main from "./main/index.ts";');
36
- expect(after).toContain('export * as notes from "./notes/index.ts";');
35
+ expect(after).toContain('import * as main from "./main/index.ts";');
36
+ expect(after).toContain('import * as notes from "./notes/index.ts";');
37
+ expect(after).toContain("export { main }");
38
+ expect(after).toContain("export { notes }");
37
39
  } finally {
38
40
  await rm(root, { recursive: true, force: true });
39
41
  }