okengine 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +73 -181
  3. package/package.json +3 -3
  4. package/site/content/docs/console/clock.mdx +25 -5
  5. package/site/content/docs/console/signals.mdx +13 -4
  6. package/site/content/docs/elements/ai.mdx +29 -2
  7. package/site/content/docs/elements/channel.mdx +42 -25
  8. package/site/content/docs/elements/clock.mdx +59 -25
  9. package/site/content/docs/elements/flow.mdx +8 -2
  10. package/site/content/docs/elements/gate.mdx +8 -1
  11. package/site/content/docs/elements/signal.mdx +173 -29
  12. package/site/content/docs/elements/store.mdx +595 -73
  13. package/site/content/docs/elements/vault.mdx +15 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +13 -6
  15. package/site/content/docs/get-started/installation.mdx +52 -14
  16. package/site/content/docs/get-started/introduction.mdx +1 -1
  17. package/site/content/docs/plugins/magic-link.mdx +42 -0
  18. package/site/content/docs/plugins/phone-number.mdx +78 -17
  19. package/site/content/docs/plugins/two-factor.mdx +1 -0
  20. package/site/content/docs/reference/cli.md +32 -2
  21. package/site/content/docs/reference/configuration.mdx +15 -13
  22. package/site/content/docs/reference/environment-variables.mdx +32 -18
  23. package/site/content/docs/reference/errors.mdx +12 -11
  24. package/site/content/docs/reference/fx.mdx +15 -13
  25. package/site/content/docs/reference/security.md +3 -1
  26. package/src/cli/ai-setup/ai-setup.test.ts +144 -0
  27. package/src/cli/ai-setup/apply.ts +214 -0
  28. package/src/cli/ai-setup/catalog.ts +263 -0
  29. package/src/cli/ai-setup/detect-ollama.ts +166 -0
  30. package/src/cli/ai-setup/index.ts +228 -0
  31. package/src/cli/ai-setup/prompts.ts +649 -0
  32. package/src/cli/ai-setup/recommend.test.ts +100 -0
  33. package/src/cli/ai-setup/recommend.ts +203 -0
  34. package/src/cli/ai.ts +38 -0
  35. package/src/cli/db-auto-push.test.ts +6 -1
  36. package/src/cli/db-auto-push.ts +8 -3
  37. package/src/cli/db-seed.ts +359 -0
  38. package/src/cli/db.test.ts +341 -3
  39. package/src/cli/db.ts +75 -8
  40. package/src/cli/dev-db-push.test.ts +59 -0
  41. package/src/cli/dev.test.ts +40 -0
  42. package/src/cli/dev.ts +55 -1
  43. package/src/cli/docker-clean.test.ts +179 -0
  44. package/src/cli/docker-clean.ts +406 -0
  45. package/src/cli/docker.ts +16 -4
  46. package/src/cli/ensure-drizzle-config.ts +1 -1
  47. package/src/cli/hero-meta.test.ts +6 -5
  48. package/src/cli/hero-meta.ts +7 -1
  49. package/src/cli/index.ts +5 -0
  50. package/src/cli/load-config.images.test.ts +22 -0
  51. package/src/cli/load-config.ts +7 -2
  52. package/src/cli/openbao-bootstrap.test.ts +1 -0
  53. package/src/cli/openbao-bootstrap.ts +9 -2
  54. package/src/cli/registry.ts +116 -2
  55. package/src/client/live-gap.test.ts +35 -0
  56. package/src/client/transport.test.ts +21 -0
  57. package/src/client/transport.ts +27 -5
  58. package/src/compiler/effects-infer.ts +1 -0
  59. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  60. package/src/config/define-config.test.ts +6 -6
  61. package/src/config/index.ts +5 -1
  62. package/src/console/server/console.test.ts +45 -0
  63. package/src/console/server/flows.ts +39 -9
  64. package/src/console/server/i18n.ts +41 -0
  65. package/src/console/server/vault.ts +12 -38
  66. package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
  67. package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
  68. package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
  69. package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
  70. package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
  71. package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
  72. package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
  73. package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
  74. package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
  75. package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
  76. package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
  77. package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
  78. package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
  79. package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
  80. package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
  81. package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
  82. package/src/console/ui/dist/index.html +1 -1
  83. package/src/console/ui/shell/components/ui.tsx +5 -1
  84. package/src/console/ui/shell/setup/Wizard.tsx +26 -5
  85. package/src/docker/cleanup.test.ts +193 -0
  86. package/src/docker/cleanup.ts +355 -0
  87. package/src/docker/docker.test.ts +4 -0
  88. package/src/docker/index.ts +19 -0
  89. package/src/docker/recipes/ollama.ts +10 -5
  90. package/src/drivers/ai-ollama.ts +45 -2
  91. package/src/drivers/ai-providers.test.ts +31 -0
  92. package/src/drivers/channel-sently.test.ts +8 -0
  93. package/src/drivers/channel-taqnyat-mail.ts +34 -0
  94. package/src/drivers/channel-types.ts +71 -0
  95. package/src/drivers/clock-postgres.test.ts +258 -0
  96. package/src/drivers/clock-postgres.ts +410 -0
  97. package/src/drivers/index.ts +21 -0
  98. package/src/drivers/journal-postgres.test.ts +175 -0
  99. package/src/drivers/journal-postgres.ts +492 -0
  100. package/src/drivers/signal-engine.ts +98 -25
  101. package/src/drivers/signal-nats.ts +4 -4
  102. package/src/drivers/signal-postgres.ts +158 -64
  103. package/src/drivers/signal-redis.ts +4 -4
  104. package/src/drivers/signal-types.ts +61 -2
  105. package/src/drivers/vault-driver-removal.test.ts +5 -1
  106. package/src/elements/channel/runtime.ts +51 -0
  107. package/src/elements/channel.test.ts +71 -0
  108. package/src/elements/clock/chaos-child.ts +389 -0
  109. package/src/elements/clock/chaos.test.ts +204 -0
  110. package/src/elements/clock/durable.ts +7 -0
  111. package/src/elements/clock/reconcile.ts +110 -0
  112. package/src/elements/clock/runtime.ts +13 -0
  113. package/src/elements/clock.test.ts +164 -2
  114. package/src/elements/clock.ts +2 -1
  115. package/src/elements/gate/runtime.ts +6 -0
  116. package/src/elements/signal/chaos-child.ts +41 -4
  117. package/src/elements/signal/declare.ts +4 -1
  118. package/src/elements/signal/delivery-modes.test.ts +179 -0
  119. package/src/elements/signal/key-ordering.test.ts +306 -0
  120. package/src/elements/signal/lease-reclaim.test.ts +123 -0
  121. package/src/elements/signal/optional-emit.test.ts +76 -0
  122. package/src/elements/signal/order-lifecycle.test.ts +130 -0
  123. package/src/elements/signal/orphan-messages.test.ts +152 -0
  124. package/src/elements/signal/runtime.ts +11 -4
  125. package/src/elements/signal/schema-emit.test.ts +107 -0
  126. package/src/elements/signal.test.ts +89 -5
  127. package/src/elements/store/files-fx.ts +104 -0
  128. package/src/elements/store/files-image.test.ts +251 -0
  129. package/src/elements/store/files-image.ts +494 -0
  130. package/src/elements/store/runtime.ts +60 -0
  131. package/src/elements/store/seed.test.ts +27 -0
  132. package/src/elements/store/seed.ts +68 -0
  133. package/src/elements/store/sql-session.test.ts +39 -0
  134. package/src/elements/store/sql-session.ts +55 -0
  135. package/src/elements/store/upsert-app.test.ts +103 -0
  136. package/src/elements/store.ts +29 -0
  137. package/src/elements/vault/boot-chain.ts +150 -0
  138. package/src/elements/vault/runtime.ts +8 -0
  139. package/src/index.ts +15 -0
  140. package/src/kernel/app.ts +168 -15
  141. package/src/kernel/boot-bind/channel.test.ts +16 -0
  142. package/src/kernel/boot-bind/channel.ts +13 -0
  143. package/src/kernel/boot-bind/clock.ts +63 -7
  144. package/src/kernel/boot-bind/gate.ts +71 -3
  145. package/src/kernel/boot-bind/honor-config.test.ts +348 -0
  146. package/src/kernel/boot-bind/journal.ts +89 -0
  147. package/src/kernel/boot-bind/signal.ts +64 -7
  148. package/src/kernel/boot-bind/vault.ts +27 -8
  149. package/src/kernel/boot.test.ts +6 -4
  150. package/src/kernel/boot.ts +63 -15
  151. package/src/kernel/concurrency.ts +1 -1
  152. package/src/kernel/errors.ts +8 -2
  153. package/src/kernel/fx.test.ts +19 -0
  154. package/src/kernel/fx.ts +153 -9
  155. package/src/kernel/index.ts +6 -0
  156. package/src/kernel/journal-boot.test.ts +397 -0
  157. package/src/kernel/journal-suspend.ts +35 -0
  158. package/src/kernel/journal.test.ts +142 -0
  159. package/src/kernel/journal.ts +202 -27
  160. package/src/plugins/auth-methods.security.test.ts +10 -7
  161. package/src/plugins/phone-number.ts +67 -10
  162. package/src/plugins/taqnyat.live.test.ts +174 -0
  163. package/src/runtime/dev-request-log.test.ts +20 -1
  164. package/src/runtime/dev-request-log.ts +38 -0
  165. package/src/term.test.ts +15 -0
  166. package/src/term.ts +8 -3
  167. package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Smart Ollama recommendation scoring + RAM tier rules.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { recommendChatModel } from "./catalog.ts";
7
+ import {
8
+ fittingChatModels,
9
+ formatMachineSummary,
10
+ isTightFit,
11
+ modelFitsComfortably,
12
+ modelFitsOnMachine,
13
+ recommendChatForNeeds,
14
+ type AiNeeds,
15
+ } from "./recommend.ts";
16
+
17
+ const balanced: AiNeeds = {
18
+ useCase: "balanced",
19
+ priority: "balanced",
20
+ wantVision: false,
21
+ };
22
+
23
+ describe("modelFitsOnMachine", () => {
24
+ test("catalog ramGb is machine tier, not download size", () => {
25
+ expect(modelFitsOnMachine({ id: "x", label: "x", hint: "", role: "chat", ramGb: 16 }, 24)).toBe(
26
+ true,
27
+ );
28
+ expect(modelFitsOnMachine({ id: "x", label: "x", hint: "", role: "chat", ramGb: 32 }, 24)).toBe(
29
+ false,
30
+ );
31
+ });
32
+ });
33
+
34
+ describe("recommendChatForNeeds", () => {
35
+ test("8GB → entry-level only", () => {
36
+ const pick = recommendChatForNeeds(8, balanced);
37
+ expect(pick.id).toBe("gemma4:e4b");
38
+ expect(pick.ramGb).toBeLessThanOrEqual(8);
39
+ });
40
+
41
+ test("16GB → never recommends 27b / 32GB-class", () => {
42
+ const pick = recommendChatForNeeds(16, { ...balanced, useCase: "coding", priority: "quality" });
43
+ expect(pick.ramGb).toBeLessThanOrEqual(16);
44
+ expect(pick.id).not.toBe("qwen3.5:27b");
45
+ });
46
+
47
+ test("24GB does not treat full RAM as model size", () => {
48
+ const pick = recommendChatForNeeds(24, balanced);
49
+ // Must leave headroom — not a fictional "24GB model"
50
+ expect(pick.ramGb).toBeLessThan(24);
51
+ expect(pick.ramGb).toBeLessThanOrEqual(16);
52
+ const summary = formatMachineSummary(24, pick);
53
+ expect(summary).toContain("Your machine: ~24GB");
54
+ expect(summary).toContain(`${pick.ramGb}GB-class`);
55
+ expect(summary).not.toMatch(/download a 24GB/i);
56
+ });
57
+
58
+ test("coding preference leans Qwen when it fits", () => {
59
+ const pick = recommendChatForNeeds(24, {
60
+ useCase: "coding",
61
+ priority: "balanced",
62
+ wantVision: false,
63
+ });
64
+ expect(pick.id.startsWith("qwen")).toBe(true);
65
+ });
66
+
67
+ test("reasoning preference leans DeepSeek when it fits", () => {
68
+ const pick = recommendChatForNeeds(16, {
69
+ useCase: "reasoning",
70
+ priority: "balanced",
71
+ wantVision: false,
72
+ });
73
+ expect(pick.id).toContain("deepseek");
74
+ });
75
+ });
76
+
77
+ describe("comfortable vs tight", () => {
78
+ test("16GB-class on 16GB machine is tight", () => {
79
+ const qwen = fittingChatModels(16).find((m) => m.id === "qwen3.5:9b");
80
+ expect(qwen).toBeDefined();
81
+ expect(modelFitsOnMachine(qwen!, 16)).toBe(true);
82
+ expect(modelFitsComfortably(qwen!, 16)).toBe(false);
83
+ expect(isTightFit(qwen!, 16)).toBe(true);
84
+ });
85
+
86
+ test("16GB-class on 24GB machine is comfortable", () => {
87
+ const qwen = fittingChatModels(24).find((m) => m.id === "qwen3.5:9b")!;
88
+ expect(modelFitsComfortably(qwen, 24)).toBe(true);
89
+ expect(isTightFit(qwen, 24)).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("recommendChatModel", () => {
94
+ test("respects tiers with headroom preference", () => {
95
+ expect(recommendChatModel(8).id).toBe("gemma4:e4b");
96
+ expect(recommendChatModel(16).ramGb).toBeLessThanOrEqual(16);
97
+ expect(recommendChatModel(24).ramGb).toBeLessThanOrEqual(16);
98
+ expect(recommendChatModel(null).recommended).toBe(true);
99
+ });
100
+ });
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Smart local-model recommendations — machine-tier fit + use-case scoring.
3
+ *
4
+ * Catalog `ramGb` is the **recommended machine tier** for that model (not the
5
+ * download size). A 24GB laptop must never be told it will "download a 24GB
6
+ * model" — that number was machine RAM shown in the wrong place.
7
+ *
8
+ * Most dev machines are 8–16GB. Prefer picks that leave ~4GB for OS / IDE /
9
+ * browser when several models fit.
10
+ */
11
+
12
+ import { CHAT_MODELS, VISION_MODELS, type CatalogModel } from "./catalog.ts";
13
+
14
+ /** Reserved for OS + IDE + browser while the model runs. */
15
+ export const OS_HEADROOM_GB = 4;
16
+
17
+ /** What the developer mainly wants from the local model. */
18
+ export type AiUseCase = "coding" | "general" | "reasoning" | "balanced";
19
+
20
+ /** Speed vs quality preference. */
21
+ export type AiPriority = "speed" | "balanced" | "quality";
22
+
23
+ /** Answers from the short Ollama questionnaire. */
24
+ export type AiNeeds = {
25
+ readonly useCase: AiUseCase;
26
+ readonly priority: AiPriority;
27
+ readonly wantVision: boolean;
28
+ };
29
+
30
+ /**
31
+ * RAM left after reserving OS/IDE headroom.
32
+ *
33
+ * @param totalRamGb - Detected machine RAM
34
+ */
35
+ export function usableRamGb(totalRamGb: number): number {
36
+ return Math.max(0, totalRamGb - OS_HEADROOM_GB);
37
+ }
38
+
39
+ /**
40
+ * Whether the machine meets the model's recommended tier (`model.ramGb`).
41
+ *
42
+ * @param model - Catalog entry
43
+ * @param totalRamGb - Machine RAM (null → only the catalog default recommended)
44
+ */
45
+ export function modelFitsOnMachine(model: CatalogModel, totalRamGb: number | null): boolean {
46
+ if (totalRamGb === null || !Number.isFinite(totalRamGb)) {
47
+ return Boolean(model.recommended);
48
+ }
49
+ return model.ramGb <= totalRamGb;
50
+ }
51
+
52
+ /**
53
+ * Whether the model fits with comfortable headroom for OS/IDE.
54
+ *
55
+ * @param model - Catalog entry
56
+ * @param totalRamGb - Machine RAM
57
+ */
58
+ export function modelFitsComfortably(model: CatalogModel, totalRamGb: number | null): boolean {
59
+ if (totalRamGb === null || !Number.isFinite(totalRamGb)) {
60
+ return Boolean(model.recommended);
61
+ }
62
+ const smallest = [...CHAT_MODELS].sort((a, b) => a.ramGb - b.ramGb)[0]!;
63
+ // Always allow the entry-level model on machines that meet its tier.
64
+ if (model.id === smallest.id) return model.ramGb <= totalRamGb;
65
+ return model.ramGb + OS_HEADROOM_GB <= totalRamGb;
66
+ }
67
+
68
+ /**
69
+ * True when the model meets the tier but leaves little room for OS/IDE.
70
+ *
71
+ * @param model - Catalog entry
72
+ * @param totalRamGb - Machine RAM
73
+ */
74
+ export function isTightFit(model: CatalogModel, totalRamGb: number | null): boolean {
75
+ if (totalRamGb === null || !Number.isFinite(totalRamGb)) return false;
76
+ return modelFitsOnMachine(model, totalRamGb) && !modelFitsComfortably(model, totalRamGb);
77
+ }
78
+
79
+ /**
80
+ * Chat models that meet the machine tier, smallest → largest.
81
+ *
82
+ * @param totalRamGb - Machine RAM
83
+ */
84
+ export function fittingChatModels(totalRamGb: number | null): readonly CatalogModel[] {
85
+ const fits = CHAT_MODELS.filter((m) => modelFitsOnMachine(m, totalRamGb));
86
+ if (fits.length > 0) return [...fits].sort((a, b) => a.ramGb - b.ramGb);
87
+ return [[...CHAT_MODELS].sort((a, b) => a.ramGb - b.ramGb)[0]!];
88
+ }
89
+
90
+ /**
91
+ * Prefer comfortable fits; fall back to any tier-fit if needed.
92
+ *
93
+ * @param totalRamGb - Machine RAM
94
+ */
95
+ export function comfortableChatModels(totalRamGb: number | null): readonly CatalogModel[] {
96
+ const comfortable = CHAT_MODELS.filter((m) => modelFitsComfortably(m, totalRamGb));
97
+ if (comfortable.length > 0) {
98
+ return [...comfortable].sort((a, b) => a.ramGb - b.ramGb);
99
+ }
100
+ return fittingChatModels(totalRamGb);
101
+ }
102
+
103
+ /**
104
+ * Score a fitting chat model for the user's needs (higher = better).
105
+ *
106
+ * @param model - Candidate
107
+ * @param needs - Questionnaire answers
108
+ * @param totalRamGb - Machine RAM (tight-fit penalty)
109
+ */
110
+ export function scoreChatModel(
111
+ model: CatalogModel,
112
+ needs: AiNeeds,
113
+ totalRamGb: number | null = null,
114
+ ): number {
115
+ let score = 0;
116
+ if (needs.priority === "speed") score += 100 - model.ramGb * 3;
117
+ else if (needs.priority === "quality") score += model.ramGb * 4;
118
+ else score += model.ramGb * 2;
119
+
120
+ if (needs.useCase === "coding") {
121
+ if (model.id.startsWith("qwen")) score += 40;
122
+ if (model.id.includes("deepseek")) score += 15;
123
+ } else if (needs.useCase === "reasoning") {
124
+ if (model.id.includes("deepseek") || model.id.includes("r1")) score += 45;
125
+ if (model.id.startsWith("qwen")) score += 20;
126
+ } else if (needs.useCase === "general") {
127
+ if (model.id.includes("llama") || model.id.includes("gemma")) score += 35;
128
+ if (model.id.startsWith("qwen")) score += 20;
129
+ } else {
130
+ if (model.recommended) score += 25;
131
+ if (model.id.startsWith("qwen3.5:9b")) score += 30;
132
+ if (model.id.startsWith("gemma")) score += 20;
133
+ }
134
+
135
+ // Prefer leaving OS/IDE headroom unless the user asked for max quality.
136
+ if (isTightFit(model, totalRamGb) && needs.priority !== "quality") {
137
+ score -= 35;
138
+ }
139
+ return score;
140
+ }
141
+
142
+ /**
143
+ * Pick the best chat model for RAM + needs.
144
+ *
145
+ * @param totalRamGb - Machine RAM
146
+ * @param needs - Questionnaire (defaults to balanced/speed-friendly)
147
+ */
148
+ export function recommendChatForNeeds(
149
+ totalRamGb: number | null,
150
+ needs: AiNeeds = { useCase: "balanced", priority: "balanced", wantVision: false },
151
+ ): CatalogModel {
152
+ // Prefer comfortable pool; quality may still score a tight tier-fit higher.
153
+ const pool =
154
+ needs.priority === "quality"
155
+ ? fittingChatModels(totalRamGb)
156
+ : comfortableChatModels(totalRamGb);
157
+ let best = pool[0]!;
158
+ let bestScore = Number.NEGATIVE_INFINITY;
159
+ for (const m of pool) {
160
+ const s = scoreChatModel(m, needs, totalRamGb);
161
+ if (s > bestScore) {
162
+ best = m;
163
+ bestScore = s;
164
+ }
165
+ }
166
+ return best;
167
+ }
168
+
169
+ /**
170
+ * Vision model that fits; null if user skipped or none fit.
171
+ *
172
+ * @param totalRamGb - Machine RAM
173
+ * @param wantVision - From questionnaire
174
+ */
175
+ export function recommendVisionForNeeds(
176
+ totalRamGb: number | null,
177
+ wantVision: boolean,
178
+ ): CatalogModel | null {
179
+ if (!wantVision) return null;
180
+ const fits = VISION_MODELS.filter((m) => modelFitsOnMachine(m, totalRamGb));
181
+ return fits.find((m) => m.recommended) ?? fits[0] ?? null;
182
+ }
183
+
184
+ /**
185
+ * Human summary line for the recommendation panel.
186
+ *
187
+ * Separates **machine RAM** from **model tier** so "~24GB" is never read as
188
+ * download size.
189
+ *
190
+ * @param totalRamGb - Machine RAM
191
+ * @param chat - Chosen chat model
192
+ */
193
+ export function formatMachineSummary(totalRamGb: number | null, chat: CatalogModel): string {
194
+ if (totalRamGb === null || !Number.isFinite(totalRamGb)) {
195
+ return `⭐ Suggested: ${chat.label} (≈${chat.ramGb}GB-class machine · not download size)`;
196
+ }
197
+ const budget = usableRamGb(totalRamGb);
198
+ const tight = isTightFit(chat, totalRamGb) ? " · tight on this machine" : "";
199
+ return [
200
+ `Your machine: ~${totalRamGb}GB RAM (keep ~${OS_HEADROOM_GB}GB free for OS/IDE → ~${budget}GB model budget)`,
201
+ `⭐ For you: ${chat.label} — ≈${chat.ramGb}GB-class (Ollama pull is usually much smaller than RAM)${tight}`,
202
+ ].join("\n");
203
+ }
package/src/cli/ai.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `oke ai` — AI element CLI commands.
3
+ */
4
+
5
+ import { EXIT_OK, EXIT_USAGE } from "./exit.ts";
6
+ import { aiSetupHelp, parseAiSetupArgs, runAiSetup } from "./ai-setup/index.ts";
7
+
8
+ /**
9
+ * CLI entry for `oke ai …`.
10
+ *
11
+ * @param args - Args after `ai`
12
+ */
13
+ export async function aiCli(args: readonly string[]): Promise<number> {
14
+ const [sub, ...rest] = args;
15
+
16
+ if (sub === undefined || sub === "--help" || sub === "-h" || sub === "help") {
17
+ console.log(`oke ai — AI element helpers
18
+
19
+ Commands:
20
+ setup Configure AI driver + models (interactive)
21
+
22
+ ${aiSetupHelp()}`);
23
+ return sub ? EXIT_OK : EXIT_USAGE;
24
+ }
25
+
26
+ if (sub === "setup") {
27
+ try {
28
+ return await runAiSetup(parseAiSetupArgs(rest));
29
+ } catch (e) {
30
+ console.error(e instanceof Error ? e.message : e);
31
+ return 1;
32
+ }
33
+ }
34
+
35
+ console.error(`oke ai: unknown subcommand ${sub}`);
36
+ console.error("Run `oke ai --help` for usage.");
37
+ return EXIT_USAGE;
38
+ }
@@ -15,7 +15,12 @@ describe("isDomainSchemaWatchPath", () => {
15
15
  expect(isDomainSchemaWatchPath("drizzle.config.ts")).toBe(true);
16
16
  expect(isDomainSchemaWatchPath("flows/notes/schema.ts")).toBe(true);
17
17
  expect(isDomainSchemaWatchPath("src/schema.decl.ts")).toBe(true);
18
- expect(isDomainSchemaWatchPath("src/schema.generated.ts")).toBe(true);
18
+ });
19
+
20
+ test("ignores emit output (avoids push ↔ generated feedback loop)", () => {
21
+ expect(isDomainSchemaWatchPath("src/schema.generated.ts")).toBe(false);
22
+ expect(isDomainSchemaWatchPath("schema.generated.ts")).toBe(false);
23
+ expect(isDomainSchemaWatchPath("flows/notes/schema.generated.tsx")).toBe(false);
19
24
  });
20
25
 
21
26
  test("ignores unrelated files", () => {
@@ -10,20 +10,25 @@ export const DB_AUTO_PUSH_DEBOUNCE_MS = 300;
10
10
  /**
11
11
  * Whether a watched path should trigger `oke db push`.
12
12
  *
13
+ * Inputs only: declaration / drizzle config / app entry. Never the emit
14
+ * output (`schema.generated.ts`) — push rewrites that file every run, so
15
+ * watching it loops `oke db push` forever under `oke dev`.
16
+ *
13
17
  * @param filename - Relative path from the watcher (may be undefined)
14
18
  */
15
19
  export function isDomainSchemaWatchPath(filename: string | null | undefined): boolean {
16
20
  if (!filename) return false;
17
21
  const normalized = filename.replace(/\\/g, "/");
18
22
  const base = basename(normalized);
23
+ // Emit output — never a watch trigger (feedback loop with `oke db push`).
24
+ if (base === "schema.generated.ts" || base === "schema.generated.tsx") return false;
19
25
  if (base === "schema.ts" || base === "schema.tsx") return true;
20
26
  if (base === "schema.decl.ts" || base === "schema.decl.tsx") return true;
21
- if (base === "schema.generated.ts" || base === "schema.generated.tsx") return true;
22
27
  if (base === "drizzle.config.ts" || base === "drizzle.config.js") return true;
23
28
  // App entry / plugins — `.plug()` table contributions feed emit.
24
29
  if (base === "app.ts" || base === "app.tsx") return true;
25
- // Nested domain schemas commonly live under src/**/schema.ts
26
- if (/(^|\/)schema(\.decl|\.generated)?\.tsx?$/.test(normalized)) return true;
30
+ // Nested domain schemas commonly live under src/**/schema.ts (not .generated).
31
+ if (/(^|\/)schema(\.decl)?\.tsx?$/.test(normalized)) return true;
27
32
  return false;
28
33
  }
29
34