okengine 0.9.1 → 0.10.1

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 (175) hide show
  1. package/AGENTS.md +1 -1
  2. package/manifest.v1.schema.json +9 -1
  3. package/package.json +3 -3
  4. package/site/content/docs/console/runs.mdx +10 -0
  5. package/site/content/docs/console/traces.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +54 -32
  7. package/site/content/docs/elements/channel.mdx +97 -9
  8. package/site/content/docs/elements/clock.mdx +1 -0
  9. package/site/content/docs/elements/flow.mdx +42 -8
  10. package/site/content/docs/get-started/basic-usage.mdx +5 -1
  11. package/site/content/docs/get-started/installation.mdx +9 -5
  12. package/site/content/docs/meta.json +2 -0
  13. package/site/content/docs/providers/azure-redis.mdx +85 -0
  14. package/site/content/docs/providers/cockroachdb.mdx +86 -0
  15. package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
  16. package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
  17. package/site/content/docs/providers/elasticache.mdx +92 -0
  18. package/site/content/docs/providers/index.mdx +101 -0
  19. package/site/content/docs/providers/memorystore.mdx +89 -0
  20. package/site/content/docs/providers/meta.json +20 -0
  21. package/site/content/docs/providers/neon.mdx +115 -0
  22. package/site/content/docs/providers/redis-cloud.mdx +93 -0
  23. package/site/content/docs/providers/supabase.mdx +117 -0
  24. package/site/content/docs/providers/upstash.mdx +91 -0
  25. package/site/content/docs/providers/yugabytedb.mdx +95 -0
  26. package/site/content/docs/recipes/caddy.mdx +128 -0
  27. package/site/content/docs/recipes/cockroachdb.mdx +127 -0
  28. package/site/content/docs/recipes/dragonfly.mdx +125 -0
  29. package/site/content/docs/recipes/index.mdx +146 -0
  30. package/site/content/docs/recipes/llama-cpp.mdx +153 -0
  31. package/site/content/docs/recipes/mailpit.mdx +136 -0
  32. package/site/content/docs/recipes/meilisearch.mdx +139 -0
  33. package/site/content/docs/recipes/meta.json +31 -0
  34. package/site/content/docs/recipes/ollama.mdx +142 -0
  35. package/site/content/docs/recipes/openbao.mdx +141 -0
  36. package/site/content/docs/recipes/pgdog.mdx +137 -0
  37. package/site/content/docs/recipes/postgres.mdx +136 -0
  38. package/site/content/docs/recipes/redis.mdx +130 -0
  39. package/site/content/docs/recipes/rustfs.mdx +148 -0
  40. package/site/content/docs/recipes/sglang.mdx +105 -0
  41. package/site/content/docs/recipes/supabase-docker.mdx +145 -0
  42. package/site/content/docs/recipes/timescale.mdx +129 -0
  43. package/site/content/docs/recipes/traefik.mdx +136 -0
  44. package/site/content/docs/recipes/valkey.mdx +121 -0
  45. package/site/content/docs/recipes/vllm.mdx +106 -0
  46. package/site/content/docs/recipes/yugabytedb.mdx +135 -0
  47. package/site/content/docs/reference/cli.md +42 -6
  48. package/site/content/docs/reference/configuration.mdx +5 -3
  49. package/site/content/docs/reference/environment-variables.mdx +12 -12
  50. package/site/content/docs/reference/fx.mdx +38 -4
  51. package/src/cli/ai-setup/ai-setup.test.ts +32 -12
  52. package/src/cli/ai-setup/apply.ts +65 -23
  53. package/src/cli/ai-setup/catalog.ts +1316 -35
  54. package/src/cli/ai-setup/detect-ollama.ts +47 -0
  55. package/src/cli/ai-setup/index.ts +44 -7
  56. package/src/cli/ai-setup/prompts.ts +430 -486
  57. package/src/cli/ai-setup/recommend.test.ts +150 -50
  58. package/src/cli/ai-setup/recommend.ts +118 -101
  59. package/src/cli/ask-seed.test.ts +68 -0
  60. package/src/cli/ask-seed.ts +79 -0
  61. package/src/cli/db-seed-redact.test.ts +25 -0
  62. package/src/cli/db-seed.ts +36 -7
  63. package/src/cli/db.test.ts +1 -1
  64. package/src/cli/db.ts +2 -2
  65. package/src/cli/dev-app-runner.ts +6 -0
  66. package/src/cli/dev-controls.test.ts +107 -0
  67. package/src/cli/dev-controls.ts +354 -0
  68. package/src/cli/dev-schema-sync.ts +12 -4
  69. package/src/cli/dev.test.ts +50 -30
  70. package/src/cli/dev.ts +633 -66
  71. package/src/cli/docker-cli.test.ts +15 -0
  72. package/src/cli/drizzle-env.test.ts +40 -6
  73. package/src/cli/drizzle-env.ts +22 -1
  74. package/src/cli/ensure-drizzle-config.ts +6 -4
  75. package/src/cli/hero-meta.test.ts +13 -1
  76. package/src/cli/hero-meta.ts +28 -6
  77. package/src/cli/index.ts +5 -0
  78. package/src/cli/registry.ts +24 -0
  79. package/src/cli/replay.test.ts +105 -0
  80. package/src/cli/replay.ts +206 -0
  81. package/src/cli/start.ts +12 -1
  82. package/src/compiler/effects-infer.ts +37 -8
  83. package/src/compiler/extract.test.ts +125 -0
  84. package/src/compiler/extract.ts +31 -6
  85. package/src/config/index.ts +3 -3
  86. package/src/console/server/claim.ts +3 -1
  87. package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
  88. package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
  89. package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
  90. package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
  91. package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
  92. package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
  93. package/src/console/ui/dist/index.html +1 -1
  94. package/src/console/ui/overview/slo.ts +31 -3
  95. package/src/console/ui/overview/types.ts +6 -0
  96. package/src/console/ui/runs/errors.test.ts +53 -0
  97. package/src/console/ui/runs/errors.ts +88 -0
  98. package/src/console/ui/runs/index.ts +8 -0
  99. package/src/console/ui/runs/search.ts +16 -0
  100. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
  101. package/src/docker/ai-model-status.test.ts +101 -0
  102. package/src/docker/ai-model-status.ts +276 -0
  103. package/src/docker/compose-health.test.ts +115 -0
  104. package/src/docker/compose-health.ts +234 -0
  105. package/src/docker/compose.ts +24 -8
  106. package/src/docker/derive.ts +35 -9
  107. package/src/docker/docker.test.ts +304 -12
  108. package/src/docker/helpers.ts +56 -0
  109. package/src/docker/index.ts +52 -0
  110. package/src/docker/ollama-pull.ts +232 -0
  111. package/src/docker/recipes/cockroach.ts +24 -0
  112. package/src/docker/recipes/index.ts +32 -0
  113. package/src/docker/recipes/llama-cpp.ts +273 -0
  114. package/src/docker/recipes/ollama.ts +19 -23
  115. package/src/docker/recipes/pgdog.ts +4 -1
  116. package/src/docker/recipes/postgres.ts +5 -18
  117. package/src/docker/recipes/sglang.ts +55 -0
  118. package/src/docker/recipes/supabase.ts +21 -0
  119. package/src/docker/recipes/timescale.ts +18 -0
  120. package/src/docker/recipes/vllm.ts +44 -0
  121. package/src/docker/recipes/yugabyte.ts +23 -0
  122. package/src/docker/stack-id.test.ts +22 -0
  123. package/src/docker/stack-id.ts +38 -4
  124. package/src/docker/types.ts +15 -0
  125. package/src/drivers/ai-ollama.integration.test.ts +7 -4
  126. package/src/drivers/signal-engine.ts +10 -0
  127. package/src/drivers/signal-postgres.ts +24 -2
  128. package/src/drivers/signal-types.ts +10 -0
  129. package/src/elements/channel/fallback.test.ts +91 -0
  130. package/src/elements/channel/hard-bounce.test.ts +82 -0
  131. package/src/elements/channel/injection.test.ts +153 -0
  132. package/src/elements/channel/locale-ar.test.ts +102 -0
  133. package/src/elements/channel/otp-delivery.ts +5 -2
  134. package/src/elements/channel/receipts-ledger.test.ts +100 -0
  135. package/src/elements/channel/runtime.ts +24 -1
  136. package/src/elements/channel/test-helpers.ts +60 -0
  137. package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
  138. package/src/elements/store/cache.test.ts +28 -0
  139. package/src/elements/store/cache.ts +1 -1
  140. package/src/elements/store/emit-drizzle.ts +43 -9
  141. package/src/elements/store/runtime.ts +1 -1
  142. package/src/elements/store/schema-decl.test.ts +2 -0
  143. package/src/elements/store/schema-decl.ts +15 -0
  144. package/src/elements/store/seed.ts +1 -1
  145. package/src/index.ts +1 -0
  146. package/src/kernel/app.ts +131 -10
  147. package/src/kernel/boot-bind/signal.ts +6 -2
  148. package/src/kernel/boot-bind/store.ts +2 -1
  149. package/src/kernel/boot.test.ts +6 -5
  150. package/src/kernel/boot.ts +113 -10
  151. package/src/kernel/compensate.test.ts +131 -0
  152. package/src/kernel/correlation.test.ts +108 -0
  153. package/src/kernel/effect-timing.test.ts +82 -0
  154. package/src/kernel/effects-stamping.test.ts +207 -0
  155. package/src/kernel/errors.ts +11 -0
  156. package/src/kernel/flow.ts +38 -0
  157. package/src/kernel/fx-runs.test.ts +49 -0
  158. package/src/kernel/fx.test.ts +14 -0
  159. package/src/kernel/fx.ts +153 -14
  160. package/src/manifest/sql-resource.ts +41 -0
  161. package/src/manifest/types.ts +8 -2
  162. package/src/runs/collect.ts +3 -0
  163. package/src/runs/export-otlp.ts +64 -0
  164. package/src/runs/index.ts +16 -0
  165. package/src/runs/parquet.ts +9 -0
  166. package/src/runs/types.ts +5 -0
  167. package/src/runs/window.test.ts +68 -0
  168. package/src/runs/window.ts +148 -0
  169. package/src/runtime/boot-warn.test.ts +45 -0
  170. package/src/runtime/boot-warn.ts +17 -0
  171. package/src/runtime/dev-request-log.ts +5 -5
  172. package/src/term.test.ts +95 -10
  173. package/src/term.ts +369 -14
  174. package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
  175. package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
@@ -45,14 +45,44 @@ oke build --target edge # < 15 kB kernel profile
45
45
  oke eval # run prompt eval sets; fails CI on regression
46
46
  oke ai setup # configure AI driver + models (TTY wizard or flags)
47
47
  oke branch prod --at "yesterday" # fork journaled state into a sandbox
48
+ oke replay --request-id <id> # re-invoke a past Flow from a Runs WideEvent (dry-run if send/ask)
48
49
  oke privacy erase --subject <id> # crypto-shredding: deletes the key, not the terabytes
49
50
  oke upgrade # run codemods for a breaking change, print the diff
50
51
  ```
51
52
 
53
+ `oke replay` loads a Runs WideEvent by id and re-enters the Flow with stored
54
+ input. Defaults to dry-run when the ledger has `send` / `ask` (pass `--live`
55
+ to override). Needs a build that persisted `WideEvent.input`.
56
+
52
57
  In development, app, Console, MCP, and docs MCP prefer the canonical ports and move upward
53
58
  when occupied. Docker infrastructure uses stable per-project offsets in disjoint ranges for
54
59
  each built-in service, preventing one service's offset port from overlapping another's.
55
60
 
61
+ `oke dev` boot chrome prints immediately (wordmark + Starting + profile), then
62
+ streams background work (compose up, vault, per-service health, AI probe) in a
63
+ live progress pane — no blank multi-second wait. That pane clears before the
64
+ elements + Docker board. Status ● is green ready · yellow pending/loading ·
65
+ red error · dim idle. Compose health keeps polling (`docker compose ps -a`)
66
+ so ● updates live if you stop a container; AI ● tracks model phase while the
67
+ AI container is up. Boot does not wait for the model to become ready.
68
+
69
+ On a TTY with `-d`, keyboard controls stay active under Logs. Status ● for
70
+ compose/AI live on the board **above** Logs (not as log lines). Press `c` to
71
+ clear the log pane and reprint the latest board. `l` / `?` also refresh first
72
+ so the services/help panel is not mixed into request logs.
73
+
74
+ | Key | Action |
75
+ | ---------------------------- | ----------------------------------- |
76
+ | `?` | help |
77
+ | `c` | refresh — clear logs, show latest ● |
78
+ | `q` | quit `oke dev` |
79
+ | `l` | list docker services |
80
+ | `u` / `x` | compose up / stop (whole stack) |
81
+ | `1`–`9` then `u` / `x` / `r` | start / stop / restart one service |
82
+
83
+ Compose stays quiet unless it fails. Process-local boot notices print once under
84
+ the hero (not once per process).
85
+
56
86
  ### create-oke
57
87
 
58
88
  ```bash
@@ -66,9 +96,11 @@ bunx create-oke@latest my-app --no-ai
66
96
 
67
97
  On a TTY: pick **standard** or **advanced**, then recommended defaults or
68
98
  customize. Customize asks **local** or **docker** first, walks facets for that
69
- side only, then whether to customize the other (defaults if no). Writes
70
- user-global `~/.oke/create-defaults.json` (reuse only when `template` matches).
71
- Scaffold writes `.oke/mode` from the primary side. Non-TTY / `--yes` / explicit
99
+ side only (including `store.index` with a `none` opt-out no separate enable
100
+ prompt), then whether to customize the other (defaults if no). **AI setup**
101
+ offers Recommended (llama.cpp) · Customize · Off. Writes user-global
102
+ `~/.oke/create-defaults.json` (reuse only when `template` matches). Scaffold
103
+ writes `.oke/mode` from the primary side. Non-TTY / `--yes` / explicit
72
104
  `--template` never prompt.
73
105
 
74
106
  ### Additional commands
@@ -79,8 +111,9 @@ oke doctor --json # -j JSON on stdout; hints on stderr (agents /
79
111
  oke stack --json # -j
80
112
  oke images list # recipe · image · tag · digest · size (--json|-j)
81
113
  oke gates list # Module:Action catalogue (--json|-j)
82
- oke ai setup --provider ollama --yes # non-interactive AI config
83
- # TTY Ollama: needs quiz RAM-tier panel → recommend / installed / manual (+ ← Back)
114
+ oke ai setup --provider llama-cpp --yes # default local AI
115
+ oke ai setup --provider ollama --yes # fully supported alternative
116
+ # TTY: llama.cpp / Ollama → banner · tiers · manual; vLLM / SGLang / cloud → model
84
117
  ```
85
118
 
86
119
  ### Shell completion
@@ -115,7 +148,10 @@ Long form is canonical in docs; short form is convenience only. Shared letters f
115
148
  | `--diff` | | `doctor` |
116
149
  | `--json` | `-j` | `doctor`, `stack`, `images list`, `gates list` |
117
150
  | `--manifest` | `-m` | most Manifest readers |
118
- | `--entry` | `-e` | `dev`, `start`, `build` |
151
+ | `--entry` | `-e` | `dev`, `start`, `build`, `replay` |
152
+ | `--request-id` | `-r` | `replay` |
153
+ | `--dry-run` | | `replay` |
154
+ | `--live` | | `replay` |
119
155
  | `--out` / `--outdir` | `-o` | writers |
120
156
  | `--subject` | `-s` | `privacy erase` |
121
157
  | `--before` | `-b` | `doctor --diff` |
@@ -82,7 +82,7 @@ images: {
82
82
  "store.files": "rustfs/rustfs:1.0.0-beta.11",
83
83
  "channel.email": "axllent/mailpit:v1.22.3",
84
84
  vault: "openbao/openbao:2.6.1",
85
- ai: "ollama/ollama:latest",
85
+ ai: "ghcr.io/ggml-org/llama.cpp:server-b10290", // default; or ollama/ollama:0.32.6
86
86
  // opt-in TLS edge — omit for the default (app publishes 6530):
87
87
  // proxy: "caddy:2-alpine", // simplest automatic HTTPS
88
88
  // proxy: "traefik:v3.3", // label auto-discovery for --scale app=N
@@ -96,7 +96,9 @@ When both `store.sql` and `pgdog` are pinned, `DATABASE_URL` points at PgDog
96
96
  For `store.kv`, pin Redis (default), Valkey, or Dragonfly — driver id stays
97
97
  `redis`, same `REDIS_URL`. Image table + licenses: [Store · KV](/docs/elements/store#kv).
98
98
 
99
- For `ai`, the Ollama recipe pulls `OKE_AI_MODEL` (default `qwen3.5:9b` — a balanced local-dev starting point; override freely; on Apple Silicon prefer `qwen3.5:9b-mlx`) and writes `OKE_AI_URL`.
99
+ For `ai`, default is [llama.cpp](/docs/recipes/llama-cpp) (`OKE_AI_URL` →
100
+ `/v1`, curated Docker Hub `ai/` model). Ollama / vLLM / SGLang are explicit
101
+ alternatives — see [Recipes · AI](/docs/recipes#ai-local--self-hosted).
100
102
 
101
103
  For `proxy`, pin **either** Caddy or Traefik — never both. Opt-in only; see
102
104
  [Reverse proxy](/docs/deployment/reverse-proxy).
@@ -165,7 +167,7 @@ Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated t
165
167
 
166
168
  | Option | Default | Surface |
167
169
  | --------- | ------- | ------- |
168
- | `app` | `6530` | App |
170
+ | `app` | `6530` | Backend |
169
171
  | `console` | `6533` | Console |
170
172
  | `mcp` | `6535` | MCP |
171
173
 
@@ -109,18 +109,18 @@ provider's real credentials — credentials alone never send.
109
109
 
110
110
  ## AI providers
111
111
 
112
- | Variable | Used for | Default when unset |
113
- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
114
- | `ANTHROPIC_API_KEY` | `anthropic` driver credential | — |
115
- | `ANTHROPIC_MODEL` | Model override for the anthropic driver | — |
116
- | `OPENAI_API_KEY` | `openai-compatible` driver credential | — |
117
- | `OPENAI_BASE_URL` | `openai-compatible` base URL | OpenAI cloud |
118
- | `OKE_AI_DRIVER` | Force the AI driver id (honoured in docker mode) | config map |
119
- | `OKE_AI_URL` | Ollama (or openai-compatible) base URL | `http://127.0.0.1:11434` (ollama) |
120
- | `OKE_AI_MODEL` | Default model for `ollama` (any pulled model; `qwen3.5:9b` is a balanced local-dev starting point; on Apple Silicon try `qwen3.5:9b-mlx`) | `qwen3.5:9b` |
121
- | `OKE_AI_VISION_MODEL` | Vision model id written by `oke ai setup` (logical `ai.model("vision")`) | — |
122
- | `OKE_AI_EMBED_MODEL` | Embedding model id written by `oke ai setup` | — |
123
- | `OLLAMA_HOST` | Alternate Ollama host (`host:port` or URL) when `OKE_AI_URL` is unset | — |
112
+ | Variable | Used for | Default when unset |
113
+ | --------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
114
+ | `ANTHROPIC_API_KEY` | `anthropic` driver credential | — |
115
+ | `ANTHROPIC_MODEL` | Model override for the anthropic driver | — |
116
+ | `OPENAI_API_KEY` | `openai-compatible` driver credential | — |
117
+ | `OPENAI_BASE_URL` | `openai-compatible` base URL | OpenAI cloud |
118
+ | `OKE_AI_DRIVER` | Force the AI driver id (honoured in docker mode) | config map |
119
+ | `OKE_AI_URL` | Local / openai-compatible base URL | `http://127.0.0.1:8080/v1` (llama.cpp) · `http://127.0.0.1:11434` (ollama) |
120
+ | `OKE_AI_MODEL` | Default model id Docker Hub `ai/` for llama.cpp; Ollama library tag for ollama; HF id for vLLM/SGLang | `smollm2` (llama.cpp) / `qwen3.5:9b` (ollama) |
121
+ | `OKE_AI_VISION_MODEL` | Vision model id written by `oke ai setup` (logical `ai.model("vision")`) | — |
122
+ | `OKE_AI_EMBED_MODEL` | Embedding model id written by `oke ai setup` | — |
123
+ | `OLLAMA_HOST` | Alternate Ollama host (`host:port` or URL) when `OKE_AI_URL` is unset | — |
124
124
 
125
125
  ## Framework behavior
126
126
 
@@ -33,9 +33,42 @@ 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; 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
+
40
+ ## Runs (observability read)
41
+
42
+ Declare `effects: { reads: ["runs"] }`. Powers native SLO checkers (Clock + Channel) without a parallel metrics API.
43
+
44
+ | Signature | Records | Returns |
45
+ | ---------------------------------------- | ------------- | --------------------------------------------- |
46
+ | `fx.runs.query(sql)` | `read` `runs` | SQL rows (`FROM runs` on files/memory) |
47
+ | `fx.runs.all()` | `read` `runs` | All wide events |
48
+ | `fx.runs.window(flow, windowMs?)` | `read` `runs` | Rolling P50/P95/P99 + error rate (default 5m) |
49
+ | `fx.runs.checkSlo(flow, slo, windowMs?)` | `read` `runs` | Availability / latency breaches |
50
+
51
+ ```typescript
52
+ on(
53
+ every("5m"),
54
+ flow({
55
+ name: "ops.slo-check",
56
+ effects: { reads: ["runs"], sends: ["slo-alert"] },
57
+ do: async (_, fx) => {
58
+ const breaches = await fx.runs.checkSlo(
59
+ "checkout.create",
60
+ { availability: "99.9%", latency: { p95: "200ms" } },
61
+ 5 * 60_000,
62
+ );
63
+ if (breaches.length === 0) return;
64
+ await fx.send(sloAlert, {
65
+ to: "oncall@example.com",
66
+ data: { flow: "checkout.create", count: breaches.length },
67
+ });
68
+ },
69
+ }),
70
+ );
71
+ ```
39
72
 
40
73
  ## Flows
41
74
 
@@ -228,7 +261,8 @@ must log who started the call chain.
228
261
 
229
262
  <Callout title="No fx.metric">
230
263
  Investigated and declined. [Runs](/docs/console/runs) already own per-invocation observability as
231
- wide events; a parallel counter/gauge API would be a second instrumentation path without a sink.
264
+ wide events. Native alerting is `fx.runs` + Clock + Channel not a second counter/gauge API.
265
+ Optional OTLP export for existing Grafana/Datadog stacks is additive and never required.
232
266
  </Callout>
233
267
 
234
268
  ## Learn more
@@ -13,13 +13,14 @@ import { parseAiSetupArgs } from "./index.ts";
13
13
 
14
14
  describe("catalog", () => {
15
15
  test("recommendChatModel respects RAM tiers", () => {
16
- expect(recommendChatModel(8).id).toBe("gemma4:e4b");
16
+ expect(recommendChatModel(8).ramGb).toBeLessThanOrEqual(8);
17
17
  expect(recommendChatModel(16).ramGb).toBeLessThanOrEqual(16);
18
18
  expect(recommendChatModel(null).recommended).toBe(true);
19
19
  });
20
20
 
21
21
  test("cloud providers ship short curated chat lists + recommended", () => {
22
22
  expect(cloudChatModels("openai").length).toBeGreaterThan(0);
23
+ expect(cloudChatModels("openai").length).toBeLessThanOrEqual(10);
23
24
  expect(cloudChatModels("anthropic").some((m) => m.recommended)).toBe(true);
24
25
  expect(recommendCloudChat("openai")).toBe("gpt-4o-mini");
25
26
  expect(recommendCloudChat("anthropic")).toContain("claude");
@@ -58,11 +59,15 @@ describe("apply", () => {
58
59
  });
59
60
  `;
60
61
  const next = upsertAiDrivers(src, "ollama");
61
- expect(next).toMatch(/^ {4}ai:\s*\{/m);
62
- expect(next).toContain('local: "ollama"');
63
- const channel = next.match(/^ {4}channel:\s*\{[\s\S]*?\n {4}\},?\n/m)?.[0] ?? "";
64
- expect(channel).toContain("email:");
65
- expect(channel).not.toContain("ai:");
62
+ const body = next
63
+ .replace(/^import\s+[\s\S]*?from\s+["'][^"']+["'];?\s*/m, "")
64
+ .replace(/export\s+default\s+/, "return ");
65
+ const config = new Function("defineConfig", body)(<T>(c: T): T => c) as {
66
+ drivers?: { ai?: unknown; channel?: { ai?: unknown; email?: unknown } };
67
+ };
68
+ expect(config.drivers?.ai).toMatchObject({ local: "ollama", docker: "ollama" });
69
+ expect(config.drivers?.channel?.ai).toBeUndefined();
70
+ expect(config.drivers?.channel?.email).toBeDefined();
66
71
  });
67
72
 
68
73
  test("upsertEnv uncomment / set", () => {
@@ -70,6 +75,18 @@ describe("apply", () => {
70
75
  expect(upsertEnv(env, "OKE_AI_DRIVER", "ollama")).toContain("OKE_AI_DRIVER=ollama");
71
76
  });
72
77
 
78
+ test("upsertEnv comment keeps stack keys as overrides", () => {
79
+ const env = `# OKE_AI_DRIVER=mock\nOKE_AI_URL=http://x\n`;
80
+ const next = upsertEnv(env, "OKE_AI_URL", "http://127.0.0.1:8080/v1", { comment: true });
81
+ expect(next).toContain("# OKE_AI_URL=http://127.0.0.1:8080/v1");
82
+ expect(next).not.toMatch(/^OKE_AI_URL=/m);
83
+ });
84
+
85
+ test("upsertEnv writes API token when provided", () => {
86
+ const env = `# OKE_AI_DRIVER=mock\n`;
87
+ expect(upsertEnv(env, "OPENAI_API_KEY", "sk-test")).toContain("OPENAI_API_KEY=sk-test");
88
+ });
89
+
73
90
  test("renderAiTs includes vision + embed", () => {
74
91
  const ts = renderAiTs({
75
92
  driver: "ollama",
@@ -82,7 +99,7 @@ describe("apply", () => {
82
99
  expect(ts).toContain("docsEmbed");
83
100
  });
84
101
 
85
- test("applyAiSetup writes config, env, ai.ts", () => {
102
+ test("applyAiSetup writes config, env, core/ai.ts", () => {
86
103
  const dir = mkdtempSync(join(tmpdir(), "oke-ai-setup-"));
87
104
  try {
88
105
  writeFileSync(
@@ -119,14 +136,17 @@ export default defineConfig({
119
136
 
120
137
  const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
121
138
  expect(config).toContain('local: "ollama"');
122
- expect(config).toContain('ai: "ollama/ollama:latest"');
139
+ expect(config).toContain('ai: "ollama/ollama:0.32.6"');
123
140
  const env = readFileSync(join(dir, ".env.local"), "utf8");
124
- expect(env).toContain("OKE_AI_DRIVER=ollama");
125
- expect(env).toContain("OKE_AI_MODEL=gemma4:e4b");
126
- const aiTs = readFileSync(join(dir, "src", "ai.ts"), "utf8");
141
+ expect(env).toContain("# OKE_AI_DRIVER=ollama");
142
+ expect(env).toMatch(/^OKE_AI_MODEL=gemma4:e4b$/m);
143
+ expect(env).not.toMatch(/^#\s*OKE_AI_MODEL=gemma4:e4b$/m);
144
+ expect(env).toContain("# OKE_AI_URL=http://127.0.0.1:11434");
145
+ expect(env).not.toMatch(/^OKE_AI_URL=/m);
146
+ const aiTs = readFileSync(join(dir, "src", "core", "ai.ts"), "utf8");
127
147
  expect(aiTs).toContain("smart");
128
148
  const app = readFileSync(join(dir, "src", "app.ts"), "utf8");
129
- expect(app).toContain('import "./ai"');
149
+ expect(app).toContain('import "./core/ai"');
130
150
  } finally {
131
151
  rmSync(dir, { recursive: true, force: true });
132
152
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Write AI driver config, env, and `src/ai.ts` for `oke ai setup`.
2
+ * Write AI driver config, env, and `src/core/ai.ts` for `oke ai setup`.
3
3
  */
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
+ import { OLLAMA_IMAGE } from "../../docker/recipes/index.ts";
7
8
 
8
9
  /** Choices applied to the project. */
9
10
  export type AiSetupApplyInput = {
@@ -13,6 +14,10 @@ export type AiSetupApplyInput = {
13
14
  readonly visionModel?: string | null;
14
15
  readonly embedModel?: string | null;
15
16
  readonly apiKeyEnv?: string;
17
+ /** When set with {@link apiKeyEnv}, writes the token into `.env.local`. */
18
+ readonly apiKey?: string;
19
+ /** Optional `images.ai` pin (llama.cpp / Ollama / vLLM / SGLang). */
20
+ readonly image?: string;
16
21
  };
17
22
 
18
23
  /** Options for {@link applyAiSetup}. */
@@ -49,8 +54,10 @@ export function applyAiSetup(
49
54
  if (updateDrivers) {
50
55
  let config = readFileSync(configPath, "utf8");
51
56
  config = upsertAiDrivers(config, input.driver);
52
- if (input.driver === "ollama") {
53
- config = upsertImage(config, "ai", "ollama/ollama:latest");
57
+ if (input.image) {
58
+ config = upsertImage(config, "ai", input.image);
59
+ } else if (input.driver === "ollama") {
60
+ config = upsertImage(config, "ai", OLLAMA_IMAGE);
54
61
  }
55
62
  writeFileSync(configPath, config, "utf8");
56
63
  }
@@ -62,20 +69,29 @@ export function applyAiSetup(
62
69
  : existsSync(envExample)
63
70
  ? readFileSync(envExample, "utf8")
64
71
  : "";
65
- env = upsertEnv(env, "OKE_AI_DRIVER", input.driver);
66
- if (input.baseUrl) env = upsertEnv(env, "OKE_AI_URL", input.baseUrl);
72
+ // Driver / URL stay commented so docker mode can hydrate `OKE_AI_URL` from
73
+ // `docker/.env.docker` without being shadowed. The selected chat model is
74
+ // written active — `oke dev` seeds it into stack controls / `LLAMA_ARG_DOCKER_REPO`
75
+ // (commented-only left the compose default `smollm2` forever).
76
+ env = upsertEnv(env, "OKE_AI_DRIVER", input.driver, { comment: true });
77
+ if (input.baseUrl) env = upsertEnv(env, "OKE_AI_URL", input.baseUrl, { comment: true });
67
78
  if (input.chatModel) env = upsertEnv(env, "OKE_AI_MODEL", input.chatModel);
68
- if (input.visionModel) env = upsertEnv(env, "OKE_AI_VISION_MODEL", input.visionModel);
69
- if (input.embedModel) env = upsertEnv(env, "OKE_AI_EMBED_MODEL", input.embedModel);
79
+ if (input.visionModel) {
80
+ env = upsertEnv(env, "OKE_AI_VISION_MODEL", input.visionModel, { comment: true });
81
+ }
82
+ if (input.embedModel) {
83
+ env = upsertEnv(env, "OKE_AI_EMBED_MODEL", input.embedModel, { comment: true });
84
+ }
70
85
  if (input.apiKeyEnv) {
71
- // Ensure the key name is present (value left blank for the user)
72
- if (!new RegExp(`^${input.apiKeyEnv}=`, "m").test(env)) {
73
- env = `${env.trimEnd()}\n${input.apiKeyEnv}=\n`;
86
+ if (input.apiKey !== undefined && input.apiKey.length > 0) {
87
+ env = upsertEnv(env, input.apiKeyEnv, input.apiKey);
88
+ } else if (!new RegExp(`^#?\\s*${input.apiKeyEnv}=`, "m").test(env)) {
89
+ env = `${env.trimEnd()}\n# ${input.apiKeyEnv}=\n`;
74
90
  }
75
91
  }
76
92
  writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
77
93
 
78
- const aiTsPath = join(cwd, "src", "ai.ts");
94
+ const aiTsPath = join(cwd, "src", "core", "ai.ts");
79
95
  mkdirSync(dirname(aiTsPath), { recursive: true });
80
96
  writeFileSync(aiTsPath, renderAiTs(input), "utf8");
81
97
  ensureAiImported(cwd);
@@ -89,11 +105,11 @@ export function applyAiSetup(
89
105
  */
90
106
  export function upsertAiDrivers(source: string, driver: string): string {
91
107
  const block = `{
92
- local: "${driver}",
93
- docker: "${driver === "mock" ? "mock" : driver}",
94
- test: "mock",
95
- prod: "${driver === "mock" ? "mock" : driver}",
96
- }`;
108
+ local: "${driver}",
109
+ docker: "${driver === "mock" ? "mock" : driver}",
110
+ test: "mock",
111
+ prod: "${driver === "mock" ? "mock" : driver}",
112
+ }`;
97
113
  // Top-level drivers.ai only (4-space indent) — never nest under channel.email.
98
114
  const aiRe = /^( {4})ai:\s*\{[\s\S]*?\n\1\}/m;
99
115
  if (aiRe.test(source)) {
@@ -131,13 +147,30 @@ export function upsertImage(source: string, key: string, image: string): string
131
147
  return source.replace(imagesRe, `images: {${body}\n${line}\n }`);
132
148
  }
133
149
 
150
+ /** Options for {@link upsertEnv}. */
151
+ export type UpsertEnvOptions = {
152
+ /**
153
+ * When true, write `# KEY=value` (opt-in override). Used for AI stack keys
154
+ * so they do not shadow `docker/.env.docker`.
155
+ */
156
+ readonly comment?: boolean;
157
+ };
158
+
134
159
  /**
160
+ * Insert or replace a dotenv assignment (optionally commented).
161
+ *
135
162
  * @param env - dotenv text
136
163
  * @param key - Variable name
137
164
  * @param value - Value
165
+ * @param options - Comment vs active line
138
166
  */
139
- export function upsertEnv(env: string, key: string, value: string): string {
140
- const line = `${key}=${value}`;
167
+ export function upsertEnv(
168
+ env: string,
169
+ key: string,
170
+ value: string,
171
+ options: UpsertEnvOptions = {},
172
+ ): string {
173
+ const line = options.comment ? `# ${key}=${value}` : `${key}=${value}`;
141
174
  const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
142
175
  if (re.test(env)) return env.replace(re, line);
143
176
  const aiSection = /(# ── AI[^\n]*\n)/;
@@ -197,18 +230,27 @@ export function renderAiTs(input: AiSetupApplyInput): string {
197
230
  }
198
231
 
199
232
  /**
200
- * Ensure `src/app.ts` (or `src/core.ts`) imports `./ai`.
233
+ * Ensure `src/app.ts` or `src/core/index.ts` imports AI wiring.
201
234
  *
202
235
  * @param cwd - Project root
203
236
  */
204
237
  function ensureAiImported(cwd: string): void {
205
- for (const rel of ["src/app.ts", "src/core.ts"]) {
238
+ const candidates: ReadonlyArray<{ readonly rel: string; readonly importLine: string }> = [
239
+ { rel: "src/app.ts", importLine: `import "./core/ai";\n` },
240
+ { rel: "src/core/index.ts", importLine: `import "./ai";\n` },
241
+ { rel: "src/core.ts", importLine: `import "./ai";\n` },
242
+ ];
243
+ for (const { rel, importLine } of candidates) {
206
244
  const path = join(cwd, rel);
207
245
  if (!existsSync(path)) continue;
208
246
  const src = readFileSync(path, "utf8");
209
- if (/from\s+["']\.\/ai["']/.test(src) || /import\s+["']\.\/ai["']/.test(src)) return;
210
- const next = `import "./ai";\n${src}`;
211
- writeFileSync(path, next, "utf8");
247
+ if (
248
+ /from\s+["']\.\/(?:core\/)?ai["']/.test(src) ||
249
+ /import\s+["']\.\/(?:core\/)?ai["']/.test(src)
250
+ ) {
251
+ return;
252
+ }
253
+ writeFileSync(path, `${importLine}${src}`, "utf8");
212
254
  return;
213
255
  }
214
256
  }