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.
- package/AGENTS.md +2 -2
- package/README.md +73 -181
- package/package.json +3 -3
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +42 -25
- package/site/content/docs/elements/clock.mdx +59 -25
- package/site/content/docs/elements/flow.mdx +8 -2
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +595 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +52 -14
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +42 -0
- package/site/content/docs/plugins/phone-number.mdx +78 -17
- package/site/content/docs/plugins/two-factor.mdx +1 -0
- package/site/content/docs/reference/cli.md +32 -2
- package/site/content/docs/reference/configuration.mdx +15 -13
- package/site/content/docs/reference/environment-variables.mdx +32 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/db-seed.ts +359 -0
- package/src/cli/db.test.ts +341 -3
- package/src/cli/db.ts +75 -8
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +22 -0
- package/src/cli/load-config.ts +7 -2
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/registry.ts +116 -2
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/effects-infer.ts +1 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +5 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-types.ts +71 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +21 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/runtime.ts +51 -0
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/clock/chaos-child.ts +389 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +13 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +2 -1
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store/seed.test.ts +27 -0
- package/src/elements/store/seed.ts +68 -0
- package/src/elements/store/sql-session.test.ts +39 -0
- package/src/elements/store/sql-session.ts +55 -0
- package/src/elements/store/upsert-app.test.ts +103 -0
- package/src/elements/store.ts +29 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +168 -15
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +13 -0
- package/src/kernel/boot-bind/clock.ts +63 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +348 -0
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +63 -15
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +19 -0
- package/src/kernel/fx.ts +153 -9
- package/src/kernel/index.ts +6 -0
- package/src/kernel/journal-boot.test.ts +397 -0
- package/src/kernel/journal-suspend.ts +35 -0
- package/src/kernel/journal.test.ts +142 -0
- package/src/kernel/journal.ts +202 -27
- package/src/plugins/auth-methods.security.test.ts +10 -7
- package/src/plugins/phone-number.ts +67 -10
- package/src/plugins/taqnyat.live.test.ts +174 -0
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated Ollama model catalog for `oke ai setup`.
|
|
3
|
+
*
|
|
4
|
+
* Short lists with Recommended — not the full Ollama library.
|
|
5
|
+
* Tags verified against common 2026 library names; override freely.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Role a model can fill in the wizard. */
|
|
9
|
+
export type CatalogRole = "chat" | "vision" | "embed";
|
|
10
|
+
|
|
11
|
+
/** One curated model entry. */
|
|
12
|
+
export type CatalogModel = {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly label: string;
|
|
15
|
+
readonly hint: string;
|
|
16
|
+
readonly role: CatalogRole;
|
|
17
|
+
readonly recommended?: boolean;
|
|
18
|
+
/** Approximate RAM (GB) needed to run comfortably. */
|
|
19
|
+
readonly ramGb: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Chat models (short list). */
|
|
23
|
+
export const CHAT_MODELS: readonly CatalogModel[] = [
|
|
24
|
+
{
|
|
25
|
+
id: "gemma4:e4b",
|
|
26
|
+
label: "Gemma 4 4B",
|
|
27
|
+
hint: "Fast · ≈8GB-class machine",
|
|
28
|
+
role: "chat",
|
|
29
|
+
recommended: true,
|
|
30
|
+
ramGb: 8,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "qwen3.5:9b",
|
|
34
|
+
label: "Qwen3.5 9B",
|
|
35
|
+
hint: "Better coding · ≈16GB-class",
|
|
36
|
+
role: "chat",
|
|
37
|
+
ramGb: 16,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "qwen3.5:27b",
|
|
41
|
+
label: "Qwen3.5 27B",
|
|
42
|
+
hint: "Best local quality · ≈32GB-class",
|
|
43
|
+
role: "chat",
|
|
44
|
+
ramGb: 32,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "llama4:scout",
|
|
48
|
+
label: "Llama 4 Scout",
|
|
49
|
+
hint: "General purpose · ≈16GB-class",
|
|
50
|
+
role: "chat",
|
|
51
|
+
ramGb: 16,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "deepseek-r1:8b",
|
|
55
|
+
label: "DeepSeek R1 8B",
|
|
56
|
+
hint: "Strong reasoning · ≈10GB-class",
|
|
57
|
+
role: "chat",
|
|
58
|
+
ramGb: 10,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/** Vision models (short list + skip handled in prompts). */
|
|
63
|
+
export const VISION_MODELS: readonly CatalogModel[] = [
|
|
64
|
+
{
|
|
65
|
+
id: "qwen3-vl:4b",
|
|
66
|
+
label: "Qwen3-VL 4B",
|
|
67
|
+
hint: "Vision-language",
|
|
68
|
+
role: "vision",
|
|
69
|
+
recommended: true,
|
|
70
|
+
ramGb: 8,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "gemma4:e4b",
|
|
74
|
+
label: "Gemma Vision",
|
|
75
|
+
hint: "Multimodal Gemma 4",
|
|
76
|
+
role: "vision",
|
|
77
|
+
ramGb: 8,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** Embedding models. */
|
|
82
|
+
export const EMBED_MODELS: readonly CatalogModel[] = [
|
|
83
|
+
{
|
|
84
|
+
id: "nomic-embed-text",
|
|
85
|
+
label: "nomic-embed-text",
|
|
86
|
+
hint: "Default local RAG embedder",
|
|
87
|
+
role: "embed",
|
|
88
|
+
recommended: true,
|
|
89
|
+
ramGb: 1,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "bge-m3",
|
|
93
|
+
label: "bge-m3",
|
|
94
|
+
hint: "Multilingual + long docs",
|
|
95
|
+
role: "embed",
|
|
96
|
+
ramGb: 2,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "jina-embeddings-v4",
|
|
100
|
+
label: "jina-embeddings-v4",
|
|
101
|
+
hint: "High-quality embedder",
|
|
102
|
+
role: "embed",
|
|
103
|
+
ramGb: 2,
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** All curated ids (for detect / not-installed). */
|
|
108
|
+
export const ALL_CURATED: readonly CatalogModel[] = [
|
|
109
|
+
...CHAT_MODELS,
|
|
110
|
+
...VISION_MODELS.filter((m) => !CHAT_MODELS.some((c) => c.id === m.id)),
|
|
111
|
+
...EMBED_MODELS,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Recommend a chat model for the host RAM (comfortable headroom).
|
|
116
|
+
*
|
|
117
|
+
* Prefer {@link recommendChatForNeeds} when use-case answers are available.
|
|
118
|
+
*
|
|
119
|
+
* @param totalRamGb - Detected total system RAM in GB (or null)
|
|
120
|
+
*/
|
|
121
|
+
export function recommendChatModel(totalRamGb: number | null): CatalogModel {
|
|
122
|
+
// Keep catalog free of recommend.ts cycles — same rules as comfortableChatModels.
|
|
123
|
+
const HEADROOM = 4;
|
|
124
|
+
const sorted = [...CHAT_MODELS].sort((a, b) => a.ramGb - b.ramGb);
|
|
125
|
+
const smallest = sorted[0]!;
|
|
126
|
+
if (totalRamGb === null || !Number.isFinite(totalRamGb)) {
|
|
127
|
+
return CHAT_MODELS.find((m) => m.recommended) ?? smallest;
|
|
128
|
+
}
|
|
129
|
+
const comfortable = sorted.filter(
|
|
130
|
+
(m) => (m.id === smallest.id && m.ramGb <= totalRamGb) || m.ramGb + HEADROOM <= totalRamGb,
|
|
131
|
+
);
|
|
132
|
+
if (comfortable.length > 0) return comfortable[comfortable.length - 1]!;
|
|
133
|
+
// Fall back to largest that meets the machine tier (tight but runnable).
|
|
134
|
+
const tierOk = sorted.filter((m) => m.ramGb <= totalRamGb);
|
|
135
|
+
return tierOk.length > 0 ? tierOk[tierOk.length - 1]! : smallest;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Recommend vision / embed defaults.
|
|
140
|
+
*
|
|
141
|
+
* @param role - vision or embed
|
|
142
|
+
*/
|
|
143
|
+
export function recommendForRole(role: "vision" | "embed"): CatalogModel {
|
|
144
|
+
const list = role === "vision" ? VISION_MODELS : EMBED_MODELS;
|
|
145
|
+
return list.find((m) => m.recommended) ?? list[0]!;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Thin cloud provider menu (non-Ollama). */
|
|
149
|
+
export const CLOUD_PROVIDERS = [
|
|
150
|
+
{
|
|
151
|
+
value: "openai",
|
|
152
|
+
label: "OpenAI",
|
|
153
|
+
driver: "openai-compatible" as const,
|
|
154
|
+
baseUrl: "https://api.openai.com/v1",
|
|
155
|
+
},
|
|
156
|
+
{ value: "anthropic", label: "Anthropic", driver: "anthropic" as const, baseUrl: undefined },
|
|
157
|
+
{
|
|
158
|
+
value: "gemini",
|
|
159
|
+
label: "Gemini",
|
|
160
|
+
driver: "openai-compatible" as const,
|
|
161
|
+
baseUrl: undefined,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
value: "lmstudio",
|
|
165
|
+
label: "LM Studio",
|
|
166
|
+
driver: "openai-compatible" as const,
|
|
167
|
+
baseUrl: "http://127.0.0.1:1234/v1",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
value: "openrouter",
|
|
171
|
+
label: "OpenRouter",
|
|
172
|
+
driver: "openai-compatible" as const,
|
|
173
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: "custom",
|
|
177
|
+
label: "Custom OpenAI Compatible",
|
|
178
|
+
driver: "openai-compatible" as const,
|
|
179
|
+
baseUrl: undefined,
|
|
180
|
+
},
|
|
181
|
+
] as const;
|
|
182
|
+
|
|
183
|
+
/** Cloud chat model entry (no RAM tier). */
|
|
184
|
+
export type CloudModel = {
|
|
185
|
+
readonly id: string;
|
|
186
|
+
readonly label: string;
|
|
187
|
+
readonly hint: string;
|
|
188
|
+
readonly recommended?: boolean;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** Short curated chat models per cloud / openai-compatible provider. */
|
|
192
|
+
export const CLOUD_CHAT_MODELS: Readonly<Record<string, readonly CloudModel[]>> = {
|
|
193
|
+
openai: [
|
|
194
|
+
{ id: "gpt-4o-mini", label: "GPT-4o mini", hint: "Fast · cheap default", recommended: true },
|
|
195
|
+
{ id: "gpt-4o", label: "GPT-4o", hint: "Strong general / vision" },
|
|
196
|
+
{ id: "gpt-4.1", label: "GPT-4.1", hint: "Latest flagship" },
|
|
197
|
+
{ id: "o4-mini", label: "o4-mini", hint: "Reasoning · lower cost" },
|
|
198
|
+
],
|
|
199
|
+
anthropic: [
|
|
200
|
+
{
|
|
201
|
+
id: "claude-sonnet-4-20250514",
|
|
202
|
+
label: "Claude Sonnet 4",
|
|
203
|
+
hint: "Balanced default",
|
|
204
|
+
recommended: true,
|
|
205
|
+
},
|
|
206
|
+
{ id: "claude-opus-4-20250514", label: "Claude Opus 4", hint: "Highest quality" },
|
|
207
|
+
{ id: "claude-haiku-4-20250514", label: "Claude Haiku 4", hint: "Fast · cheap" },
|
|
208
|
+
],
|
|
209
|
+
gemini: [
|
|
210
|
+
{
|
|
211
|
+
id: "gemini-2.5-flash",
|
|
212
|
+
label: "Gemini 2.5 Flash",
|
|
213
|
+
hint: "Fast default",
|
|
214
|
+
recommended: true,
|
|
215
|
+
},
|
|
216
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", hint: "Higher quality" },
|
|
217
|
+
],
|
|
218
|
+
openrouter: [
|
|
219
|
+
{
|
|
220
|
+
id: "openai/gpt-4o-mini",
|
|
221
|
+
label: "GPT-4o mini",
|
|
222
|
+
hint: "via OpenRouter",
|
|
223
|
+
recommended: true,
|
|
224
|
+
},
|
|
225
|
+
{ id: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4", hint: "via OpenRouter" },
|
|
226
|
+
{ id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "via OpenRouter" },
|
|
227
|
+
],
|
|
228
|
+
lmstudio: [
|
|
229
|
+
{
|
|
230
|
+
id: "local-model",
|
|
231
|
+
label: "local-model",
|
|
232
|
+
hint: "LM Studio loaded model id",
|
|
233
|
+
recommended: true,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
custom: [
|
|
237
|
+
{
|
|
238
|
+
id: "default",
|
|
239
|
+
label: "default",
|
|
240
|
+
hint: "Whatever your endpoint expects",
|
|
241
|
+
recommended: true,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Curated chat models for a cloud provider (empty → Other-only).
|
|
248
|
+
*
|
|
249
|
+
* @param provider - Menu provider id
|
|
250
|
+
*/
|
|
251
|
+
export function cloudChatModels(provider: string): readonly CloudModel[] {
|
|
252
|
+
return CLOUD_CHAT_MODELS[provider] ?? [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Recommended cloud chat model id for a provider.
|
|
257
|
+
*
|
|
258
|
+
* @param provider - Menu provider id
|
|
259
|
+
*/
|
|
260
|
+
export function recommendCloudChat(provider: string): string {
|
|
261
|
+
const list = cloudChatModels(provider);
|
|
262
|
+
return list.find((m) => m.recommended)?.id ?? list[0]?.id ?? "gpt-4o-mini";
|
|
263
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect local Ollama — CLI (`ollama list` / `ps`) and HTTP `/api/tags`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { OLLAMA_DEFAULT_BASE_URL, normalizeOllamaBaseUrl } from "../../drivers/ai-ollama.ts";
|
|
6
|
+
import { ALL_CURATED, type CatalogModel } from "./catalog.ts";
|
|
7
|
+
|
|
8
|
+
/** Result of probing the local Ollama install. */
|
|
9
|
+
export type OllamaDetectResult = {
|
|
10
|
+
readonly available: boolean;
|
|
11
|
+
readonly baseUrl: string;
|
|
12
|
+
readonly installed: readonly string[];
|
|
13
|
+
readonly running: readonly string[];
|
|
14
|
+
readonly curatedInstalled: readonly CatalogModel[];
|
|
15
|
+
readonly curatedMissing: readonly CatalogModel[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse `ollama list` tabular output into model names.
|
|
20
|
+
*
|
|
21
|
+
* @param stdout - CLI stdout
|
|
22
|
+
*/
|
|
23
|
+
export function parseOllamaList(stdout: string): string[] {
|
|
24
|
+
const lines = stdout
|
|
25
|
+
.split("\n")
|
|
26
|
+
.map((l) => l.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (/^NAME\b/i.test(line)) continue;
|
|
31
|
+
const name = line.split(/\s+/)[0];
|
|
32
|
+
if (name) out.push(name);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse `ollama ps` tabular output into running model names.
|
|
39
|
+
*
|
|
40
|
+
* @param stdout - CLI stdout
|
|
41
|
+
*/
|
|
42
|
+
export function parseOllamaPs(stdout: string): string[] {
|
|
43
|
+
return parseOllamaList(stdout);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize installed tag for comparison (`qwen3.5:9b` vs `qwen3.5:9b-mlx`).
|
|
48
|
+
*
|
|
49
|
+
* @param id - Catalog or installed id
|
|
50
|
+
* @param installed - Installed names
|
|
51
|
+
*/
|
|
52
|
+
export function isInstalled(id: string, installed: readonly string[]): boolean {
|
|
53
|
+
const base = id.split(":")[0] ?? id;
|
|
54
|
+
return installed.some((name) => {
|
|
55
|
+
if (name === id) return true;
|
|
56
|
+
if (name.startsWith(`${id}-`)) return true;
|
|
57
|
+
if (name.startsWith(`${base}:`) && id.startsWith(`${base}:`)) {
|
|
58
|
+
// treat mlx / quant suffixes as matching the same family:tag when prefix matches
|
|
59
|
+
const instTag = name.slice(base.length + 1);
|
|
60
|
+
const wantTag = id.slice(base.length + 1);
|
|
61
|
+
return (
|
|
62
|
+
instTag === wantTag || instTag.startsWith(`${wantTag}-`) || wantTag.startsWith(instTag)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect Ollama via CLI then HTTP fallback.
|
|
71
|
+
*
|
|
72
|
+
* @param options - Base URL / spawn / fetch seams
|
|
73
|
+
*/
|
|
74
|
+
export async function detectOllama(
|
|
75
|
+
options: {
|
|
76
|
+
readonly baseUrl?: string;
|
|
77
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
78
|
+
readonly run?: (cmd: readonly string[]) => Promise<{ code: number; stdout: string }>;
|
|
79
|
+
} = {},
|
|
80
|
+
): Promise<OllamaDetectResult> {
|
|
81
|
+
const baseUrl = normalizeOllamaBaseUrl(options.baseUrl ?? OLLAMA_DEFAULT_BASE_URL);
|
|
82
|
+
const run =
|
|
83
|
+
options.run ??
|
|
84
|
+
(async (cmd) => {
|
|
85
|
+
const proc = Bun.spawn([...cmd], { stdout: "pipe", stderr: "pipe" });
|
|
86
|
+
const stdout = await new Response(proc.stdout).text();
|
|
87
|
+
const code = await proc.exited;
|
|
88
|
+
return { code, stdout };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
let installed: string[] = [];
|
|
92
|
+
let running: string[] = [];
|
|
93
|
+
let available = false;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const list = await run(["ollama", "list"]);
|
|
97
|
+
if (list.code === 0) {
|
|
98
|
+
available = true;
|
|
99
|
+
installed = parseOllamaList(list.stdout);
|
|
100
|
+
}
|
|
101
|
+
const ps = await run(["ollama", "ps"]);
|
|
102
|
+
if (ps.code === 0) {
|
|
103
|
+
running = parseOllamaPs(ps.stdout);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// CLI missing — try HTTP
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!available) {
|
|
110
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetchFn(`${baseUrl}/api/tags`, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
signal: AbortSignal.timeout(2_000),
|
|
115
|
+
});
|
|
116
|
+
if (res.ok) {
|
|
117
|
+
available = true;
|
|
118
|
+
const json = (await res.json()) as { models?: readonly { name?: string }[] };
|
|
119
|
+
installed = (json.models ?? [])
|
|
120
|
+
.map((m) => m.name)
|
|
121
|
+
.filter((n): n is string => typeof n === "string");
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// unavailable
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const curatedInstalled = ALL_CURATED.filter((m) => isInstalled(m.id, installed));
|
|
129
|
+
const curatedMissing = ALL_CURATED.filter((m) => !isInstalled(m.id, installed));
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
available,
|
|
133
|
+
baseUrl,
|
|
134
|
+
installed,
|
|
135
|
+
running,
|
|
136
|
+
curatedInstalled,
|
|
137
|
+
curatedMissing,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Best-effort total system RAM in GB.
|
|
143
|
+
*/
|
|
144
|
+
export function detectTotalRamGb(): number | null {
|
|
145
|
+
try {
|
|
146
|
+
if (process.platform === "darwin") {
|
|
147
|
+
const proc = Bun.spawnSync(["sysctl", "-n", "hw.memsize"], { stdout: "pipe" });
|
|
148
|
+
if (proc.exitCode === 0) {
|
|
149
|
+
const bytes = Number(proc.stdout.toString().trim());
|
|
150
|
+
if (Number.isFinite(bytes) && bytes > 0) return Math.round(bytes / 1024 ** 3);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (process.platform === "linux") {
|
|
154
|
+
const proc = Bun.spawnSync(["awk", "/MemTotal/ {print $2}", "/proc/meminfo"], {
|
|
155
|
+
stdout: "pipe",
|
|
156
|
+
});
|
|
157
|
+
if (proc.exitCode === 0) {
|
|
158
|
+
const kb = Number(proc.stdout.toString().trim());
|
|
159
|
+
if (Number.isFinite(kb) && kb > 0) return Math.round(kb / 1024 ** 2);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke ai setup` — interactive AI provider + model configuration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cancel, intro, outro, spinner } from "@clack/prompts";
|
|
6
|
+
import { applyAiSetup, type AiSetupApplyInput } from "./apply.ts";
|
|
7
|
+
import { askAiSetup } from "./prompts.ts";
|
|
8
|
+
|
|
9
|
+
/** Parsed flags for AI setup. */
|
|
10
|
+
export type AiSetupCliArgs = {
|
|
11
|
+
readonly help: boolean;
|
|
12
|
+
readonly yes: boolean;
|
|
13
|
+
readonly provider?: string;
|
|
14
|
+
readonly chat?: string;
|
|
15
|
+
readonly vision?: string;
|
|
16
|
+
readonly embed?: string;
|
|
17
|
+
readonly pull: boolean;
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse argv after `ai setup`.
|
|
23
|
+
*
|
|
24
|
+
* @param argv - Remaining args
|
|
25
|
+
* @param cwd - Project root
|
|
26
|
+
*/
|
|
27
|
+
export function parseAiSetupArgs(
|
|
28
|
+
argv: readonly string[],
|
|
29
|
+
cwd: string = process.cwd(),
|
|
30
|
+
): AiSetupCliArgs {
|
|
31
|
+
let help = false;
|
|
32
|
+
let yes = false;
|
|
33
|
+
let provider: string | undefined;
|
|
34
|
+
let chat: string | undefined;
|
|
35
|
+
let vision: string | undefined;
|
|
36
|
+
let embed: string | undefined;
|
|
37
|
+
let pull = true;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const a = argv[i]!;
|
|
41
|
+
if (a === "--help" || a === "-h") {
|
|
42
|
+
help = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (a === "--yes" || a === "-y") {
|
|
46
|
+
yes = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (a === "--no-pull") {
|
|
50
|
+
pull = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (a === "--pull") {
|
|
54
|
+
pull = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (a === "--provider") {
|
|
58
|
+
provider = argv[++i];
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (a.startsWith("--provider=")) {
|
|
62
|
+
provider = a.slice("--provider=".length);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (a === "--chat") {
|
|
66
|
+
chat = argv[++i];
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (a.startsWith("--chat=")) {
|
|
70
|
+
chat = a.slice("--chat=".length);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (a === "--vision") {
|
|
74
|
+
vision = argv[++i];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (a.startsWith("--vision=")) {
|
|
78
|
+
vision = a.slice("--vision=".length);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (a === "--embed") {
|
|
82
|
+
embed = argv[++i];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (a.startsWith("--embed=")) {
|
|
86
|
+
embed = a.slice("--embed=".length);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`oke ai setup: unknown option ${a}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { help, yes, provider, chat, vision, embed, pull, cwd };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Help text for `oke ai setup`.
|
|
97
|
+
*/
|
|
98
|
+
export function aiSetupHelp(): string {
|
|
99
|
+
return `oke ai setup — configure AI driver + models
|
|
100
|
+
|
|
101
|
+
Usage:
|
|
102
|
+
oke ai setup
|
|
103
|
+
oke ai setup --provider ollama --yes
|
|
104
|
+
oke ai setup --provider anthropic --chat claude-sonnet-4-20250514 --yes
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--provider <id> ollama | openai | anthropic | gemini | lmstudio | openrouter | custom
|
|
108
|
+
--chat <model> Chat model id
|
|
109
|
+
--vision <model> Vision model id (ollama)
|
|
110
|
+
--embed <model> Embedding model id (ollama)
|
|
111
|
+
--pull / --no-pull Pull missing Ollama models (default: pull)
|
|
112
|
+
-y, --yes Non-interactive (requires --provider)
|
|
113
|
+
-h, --help Show this help
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Run AI setup (interactive or flags).
|
|
119
|
+
*
|
|
120
|
+
* @param args - Parsed args
|
|
121
|
+
* @returns Exit code
|
|
122
|
+
*/
|
|
123
|
+
export async function runAiSetup(args: AiSetupCliArgs): Promise<number> {
|
|
124
|
+
if (args.help) {
|
|
125
|
+
console.log(aiSetupHelp());
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const tty = Boolean(process.stdin.isTTY);
|
|
130
|
+
let input: AiSetupApplyInput | null = null;
|
|
131
|
+
|
|
132
|
+
if (args.yes || !tty) {
|
|
133
|
+
if (!args.provider) {
|
|
134
|
+
console.error("oke ai setup: --provider is required in non-interactive mode");
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
input = nonInteractiveInput(args);
|
|
138
|
+
} else {
|
|
139
|
+
intro("oke ai setup");
|
|
140
|
+
input = await askAiSetup({ provider: args.provider });
|
|
141
|
+
if (input === null) {
|
|
142
|
+
cancel("Cancelled.");
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
// Flag overrides for models when provided alongside interactive provider
|
|
146
|
+
if (args.chat) input = { ...input, chatModel: args.chat };
|
|
147
|
+
if (args.vision !== undefined) input = { ...input, visionModel: args.vision || null };
|
|
148
|
+
if (args.embed) input = { ...input, embedModel: args.embed };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (input === null) return 1;
|
|
152
|
+
|
|
153
|
+
if (args.pull && input.driver === "ollama" && (args.yes || !tty) && args.chat) {
|
|
154
|
+
// Non-interactive pull of requested models
|
|
155
|
+
const toPull = [input.chatModel, input.visionModel, input.embedModel].filter(
|
|
156
|
+
(m): m is string => typeof m === "string" && m.length > 0,
|
|
157
|
+
);
|
|
158
|
+
for (const id of toPull) {
|
|
159
|
+
const proc = Bun.spawn(["ollama", "pull", id], {
|
|
160
|
+
stdout: "inherit",
|
|
161
|
+
stderr: "inherit",
|
|
162
|
+
});
|
|
163
|
+
await proc.exited;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const spun = tty ? spinner() : undefined;
|
|
168
|
+
spun?.start("Writing AI config…");
|
|
169
|
+
try {
|
|
170
|
+
const result = applyAiSetup(args.cwd, input);
|
|
171
|
+
spun?.stop("AI configured.");
|
|
172
|
+
if (tty) {
|
|
173
|
+
outro(
|
|
174
|
+
`Wrote ${result.aiTsPath}\nDriver ${input.driver}` +
|
|
175
|
+
(input.chatModel ? ` · chat ${input.chatModel}` : ""),
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`oke ai setup: driver=${input.driver} chat=${input.chatModel ?? "—"}`);
|
|
179
|
+
}
|
|
180
|
+
return 0;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
spun?.stop("Failed.");
|
|
183
|
+
console.error(e instanceof Error ? e.message : e);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build apply input from flags (non-interactive).
|
|
190
|
+
*
|
|
191
|
+
* @param args - Parsed args
|
|
192
|
+
*/
|
|
193
|
+
function nonInteractiveInput(args: AiSetupCliArgs): AiSetupApplyInput {
|
|
194
|
+
const provider = args.provider!;
|
|
195
|
+
if (provider === "ollama") {
|
|
196
|
+
return {
|
|
197
|
+
driver: "ollama",
|
|
198
|
+
baseUrl: process.env.OKE_AI_URL ?? "http://127.0.0.1:11434",
|
|
199
|
+
chatModel: args.chat ?? "gemma4:e4b",
|
|
200
|
+
visionModel: args.vision === undefined ? "qwen3-vl:4b" : args.vision || null,
|
|
201
|
+
embedModel: args.embed ?? "nomic-embed-text",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (provider === "anthropic") {
|
|
205
|
+
return {
|
|
206
|
+
driver: "anthropic",
|
|
207
|
+
chatModel: args.chat ?? "claude-sonnet-4-20250514",
|
|
208
|
+
visionModel: null,
|
|
209
|
+
embedModel: null,
|
|
210
|
+
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const baseUrls: Record<string, string | undefined> = {
|
|
214
|
+
openai: "https://api.openai.com/v1",
|
|
215
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
216
|
+
lmstudio: "http://127.0.0.1:1234/v1",
|
|
217
|
+
gemini: undefined,
|
|
218
|
+
custom: undefined,
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
driver: "openai-compatible",
|
|
222
|
+
...(baseUrls[provider] !== undefined ? { baseUrl: baseUrls[provider] } : {}),
|
|
223
|
+
chatModel: args.chat ?? "gpt-4o-mini",
|
|
224
|
+
visionModel: null,
|
|
225
|
+
embedModel: null,
|
|
226
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
227
|
+
};
|
|
228
|
+
}
|