create-oke 0.6.0 → 0.7.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/README.md +23 -8
- package/package.json +3 -3
- package/src/ai-setup/apply.ts +214 -0
- package/src/ai-setup/catalog.ts +263 -0
- package/src/ai-setup/detect-ollama.ts +180 -0
- package/src/ai-setup/from-pref.ts +99 -0
- package/src/ai-setup/prompts.ts +649 -0
- package/src/ai-setup/recommend.ts +203 -0
- package/src/cli.test.ts +385 -28
- package/src/cli.ts +332 -29
- package/src/create-defaults.test.ts +92 -0
- package/src/create-defaults.ts +246 -0
- package/src/customize-flow.test.ts +82 -0
- package/src/customize-flow.ts +432 -0
- package/src/drivers-catalog.ts +217 -0
- package/src/scaffold.ts +97 -10
- package/src/templates.ts +14 -9
- package/src/transform.test.ts +105 -0
- package/src/transform.ts +227 -0
- package/src/wizard-select.ts +50 -0
- package/{template → templates/advanced}/.env.example +40 -2
- package/templates/advanced/README.md +32 -0
- package/templates/advanced/oke.config.ts +74 -0
- package/templates/advanced/package.json +18 -0
- package/templates/advanced/src/app.ts +30 -0
- package/templates/advanced/src/channels.ts +13 -0
- package/templates/advanced/src/core.ts +8 -0
- package/{template → templates/advanced}/src/flows/main/index.ts +9 -6
- package/templates/advanced/src/flows/main/shapes.ts +1 -0
- package/templates/advanced/src/flows/main/signals.ts +1 -0
- package/templates/advanced/src/flows/notes/index.ts +224 -0
- package/templates/advanced/src/flows/notes/shapes.ts +52 -0
- package/templates/advanced/src/flows/notes/signals.ts +13 -0
- package/templates/advanced/src/gates.ts +12 -0
- package/templates/advanced/src/locales/ar.ts +18 -0
- package/templates/advanced/src/locales/en.ts +22 -0
- package/templates/advanced/src/schema.decl.ts +13 -0
- package/templates/advanced/src/vault.ts +7 -0
- package/templates/advanced/tests/advanced.test.ts +42 -0
- package/templates/standard/.env.example +94 -0
- package/templates/standard/README.md +46 -0
- package/templates/standard/drizzle.config.ts +25 -0
- package/{template → templates/standard}/oke.config.ts +9 -9
- package/{template → templates/standard}/package.json +1 -1
- package/templates/standard/src/app.ts +30 -0
- package/templates/standard/src/channels.ts +13 -0
- package/{template → templates/standard}/src/core.ts +1 -0
- package/templates/standard/src/flows/main/index.ts +34 -0
- package/templates/standard/src/flows/main/shapes.ts +1 -0
- package/templates/standard/src/flows/main/signals.ts +1 -0
- package/templates/standard/src/flows/notes/index.ts +143 -0
- package/templates/standard/src/flows/notes/shapes.ts +26 -0
- package/templates/standard/src/flows/notes/signals.ts +13 -0
- package/templates/standard/src/gates.ts +12 -0
- package/templates/standard/src/locales/ar.ts +18 -0
- package/templates/standard/src/locales/en.ts +22 -0
- package/templates/standard/src/schema.decl.ts +13 -0
- package/templates/standard/src/vault.ts +7 -0
- package/templates/standard/tests/standard.test.ts +51 -0
- package/template/README.md +0 -77
- package/template/bun.lock +0 -845
- package/template/src/app.ts +0 -18
- package/template/src/channels.ts +0 -4
- package/template/src/flows/main/shapes.ts +0 -2
- package/template/src/flows/main/signals.ts +0 -4
- package/template/src/gates.ts +0 -4
- package/template/src/locales/ar.ts +0 -20
- package/template/src/locales/en.ts +0 -26
- package/template/src/schema.decl.ts +0 -12
- package/template/src/vault.ts +0 -4
- package/template/tests/standard.test.ts +0 -18
- /package/{template → templates/advanced}/drizzle.config.ts +0 -0
package/README.md
CHANGED
|
@@ -8,8 +8,21 @@ cd my-app
|
|
|
8
8
|
oke dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
On a TTY, create-oke opens a Clack wizard
|
|
12
|
-
|
|
11
|
+
On a TTY, create-oke opens a Clack wizard:
|
|
12
|
+
|
|
13
|
+
1. Project name
|
|
14
|
+
2. **Starter** — `standard` (local-first Notes) or `advanced` (same domain,
|
|
15
|
+
docker-ready + files/digest/AI hooks)
|
|
16
|
+
3. **Recommended defaults** · **Customize** (pick **local** or **docker** first,
|
|
17
|
+
walk facets for that side, then optionally customize the other; saved to
|
|
18
|
+
user-global `~/.oke/create-defaults.json`) · **Reuse previous settings**
|
|
19
|
+
(only when saved settings exist for the selected template).
|
|
20
|
+
The project-name step rejects a path that already exists and is not empty.
|
|
21
|
+
Customize steps include **← Back**. Scaffold writes `.oke/mode` from the
|
|
22
|
+
primary side (recommended: standard → `local`, advanced → `docker`).
|
|
23
|
+
4. Install dependencies and start `oke dev`?
|
|
24
|
+
|
|
25
|
+
Pass `--yes` for non-interactive recommended defaults (zero prompts).
|
|
13
26
|
|
|
14
27
|
| Port | What |
|
|
15
28
|
| ------- | ------------------- |
|
|
@@ -17,21 +30,23 @@ install dependencies and start `oke dev`. Pass `--yes` for non-interactive use.
|
|
|
17
30
|
| `:6533` | Console |
|
|
18
31
|
| `:6535` | MCP (live Manifest) |
|
|
19
32
|
|
|
20
|
-
The single bundled template is **standard**: the full recommended file layout
|
|
21
|
-
with empty scaffolding.
|
|
22
|
-
|
|
23
33
|
```bash
|
|
24
34
|
bunx create-oke@latest my-app --template standard
|
|
35
|
+
bunx create-oke@latest my-app --template advanced
|
|
25
36
|
bunx create-oke@latest my-app --sql postgres
|
|
26
37
|
bunx create-oke@latest my-app --yes
|
|
38
|
+
bunx create-oke@latest my-app --ai # force AI model wizard before install
|
|
39
|
+
bunx create-oke@latest my-app --no-ai
|
|
27
40
|
```
|
|
28
41
|
|
|
29
42
|
`--sql sqlite|postgres` is opt-in. The default keeps the dual-mode Store
|
|
30
43
|
config (`local: sqlite` · `docker`/`prod: postgres`) and abstract
|
|
31
44
|
`src/schema.decl.ts` (dialect emitted from the active driver). Pass
|
|
32
45
|
`--sql postgres` to pin `oke.config.ts` `store.sql` local/docker/prod to
|
|
33
|
-
`postgres`.
|
|
34
|
-
|
|
46
|
+
`postgres`. Customize can set drivers the template exposes; when AI is enabled,
|
|
47
|
+
model selection runs in the wizard **before** `bun install` (writes `.env.local`
|
|
48
|
+
|
|
49
|
+
- `src/ai.ts`).
|
|
35
50
|
|
|
36
51
|
Store-bearing scaffolds ship `drizzle.config.ts` and `.env.example`. Local
|
|
37
52
|
`oke dev` auto-runs `oke db push` on schema change; use `oke mode docker` /
|
|
@@ -47,4 +62,4 @@ Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
|
|
|
47
62
|
| npm (CLI) | [`create-oke`](https://www.npmjs.com/package/create-oke) — use this with `bunx` |
|
|
48
63
|
| JSR | [`@omqkhafi/create-oke`](https://jsr.io/@omqkhafi/create-oke) — same sources; prefer npm for the binary |
|
|
49
64
|
|
|
50
|
-
Requires **Bun ≥ 1.3**. Docs: [oke.omqkhafi.dev](https://oke.omqkhafi.dev) · [handbook](https://oke.omqkhafi.dev/docs).
|
|
65
|
+
Requires **Bun ≥ 1.3.14**. Docs: [oke.omqkhafi.dev](https://oke.omqkhafi.dev) · [handbook](https://oke.omqkhafi.dev/docs).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-oke",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Scaffold an okengine app — bunx create-oke@latest <name>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
|
-
"
|
|
16
|
+
"templates"
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
19
|
"sideEffects": false,
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"typescript": "^7.0.2"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"bun": ">=1.3"
|
|
36
|
+
"bun": ">=1.3.14"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write AI driver config, env, and `src/ai.ts` for `oke ai setup`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
/** Choices applied to the project. */
|
|
9
|
+
export type AiSetupApplyInput = {
|
|
10
|
+
readonly driver: "ollama" | "anthropic" | "openai-compatible" | "mock";
|
|
11
|
+
readonly baseUrl?: string;
|
|
12
|
+
readonly chatModel?: string;
|
|
13
|
+
readonly visionModel?: string | null;
|
|
14
|
+
readonly embedModel?: string | null;
|
|
15
|
+
readonly apiKeyEnv?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Options for {@link applyAiSetup}. */
|
|
19
|
+
export type AiSetupApplyOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* When false, leave `drivers.ai` / `images.ai` alone (create-oke already
|
|
22
|
+
* wrote per-env pins). Default true for standalone `oke ai setup`.
|
|
23
|
+
*/
|
|
24
|
+
readonly updateDrivers?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply AI setup to a project root.
|
|
29
|
+
*
|
|
30
|
+
* @param cwd - Project root
|
|
31
|
+
* @param input - Selected provider / models
|
|
32
|
+
* @param options - Pin-preserving knobs for create-oke
|
|
33
|
+
*/
|
|
34
|
+
export function applyAiSetup(
|
|
35
|
+
cwd: string,
|
|
36
|
+
input: AiSetupApplyInput,
|
|
37
|
+
options: AiSetupApplyOptions = {},
|
|
38
|
+
): {
|
|
39
|
+
readonly configPath: string;
|
|
40
|
+
readonly envPath: string;
|
|
41
|
+
readonly aiTsPath: string;
|
|
42
|
+
} {
|
|
43
|
+
const updateDrivers = options.updateDrivers !== false;
|
|
44
|
+
const configPath = join(cwd, "oke.config.ts");
|
|
45
|
+
if (!existsSync(configPath)) {
|
|
46
|
+
throw new Error("oke ai setup: no oke.config.ts in the current directory");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (updateDrivers) {
|
|
50
|
+
let config = readFileSync(configPath, "utf8");
|
|
51
|
+
config = upsertAiDrivers(config, input.driver);
|
|
52
|
+
if (input.driver === "ollama") {
|
|
53
|
+
config = upsertImage(config, "ai", "ollama/ollama:latest");
|
|
54
|
+
}
|
|
55
|
+
writeFileSync(configPath, config, "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const envPath = join(cwd, ".env.local");
|
|
59
|
+
const envExample = join(cwd, ".env.example");
|
|
60
|
+
let env = existsSync(envPath)
|
|
61
|
+
? readFileSync(envPath, "utf8")
|
|
62
|
+
: existsSync(envExample)
|
|
63
|
+
? readFileSync(envExample, "utf8")
|
|
64
|
+
: "";
|
|
65
|
+
env = upsertEnv(env, "OKE_AI_DRIVER", input.driver);
|
|
66
|
+
if (input.baseUrl) env = upsertEnv(env, "OKE_AI_URL", input.baseUrl);
|
|
67
|
+
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);
|
|
70
|
+
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`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
|
|
77
|
+
|
|
78
|
+
const aiTsPath = join(cwd, "src", "ai.ts");
|
|
79
|
+
mkdirSync(dirname(aiTsPath), { recursive: true });
|
|
80
|
+
writeFileSync(aiTsPath, renderAiTs(input), "utf8");
|
|
81
|
+
ensureAiImported(cwd);
|
|
82
|
+
|
|
83
|
+
return { configPath, envPath, aiTsPath };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param source - oke.config.ts
|
|
88
|
+
* @param driver - AI driver id
|
|
89
|
+
*/
|
|
90
|
+
export function upsertAiDrivers(source: string, driver: string): string {
|
|
91
|
+
const block = `{
|
|
92
|
+
local: "${driver}",
|
|
93
|
+
docker: "${driver === "mock" ? "mock" : driver}",
|
|
94
|
+
test: "mock",
|
|
95
|
+
prod: "${driver === "mock" ? "mock" : driver}",
|
|
96
|
+
}`;
|
|
97
|
+
// Top-level drivers.ai only (4-space indent) — never nest under channel.email.
|
|
98
|
+
const aiRe = /^( {4})ai:\s*\{[\s\S]*?\n\1\}/m;
|
|
99
|
+
if (aiRe.test(source)) {
|
|
100
|
+
return source.replace(aiRe, `$1ai: ${block}`);
|
|
101
|
+
}
|
|
102
|
+
const channelRe = /^( {4})channel:\s*\{[\s\S]*?\n\1\},?\n/m;
|
|
103
|
+
if (channelRe.test(source)) {
|
|
104
|
+
return source.replace(channelRe, (match) => `${match} ai: ${block},\n`);
|
|
105
|
+
}
|
|
106
|
+
const driversClose = /(drivers:\s*\{[\s\S]*?)(\n\s*\},)/;
|
|
107
|
+
if (!driversClose.test(source)) {
|
|
108
|
+
throw new Error("oke ai setup: could not find drivers block in oke.config.ts");
|
|
109
|
+
}
|
|
110
|
+
return source.replace(driversClose, `$1\n ai: ${block},$2`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param source - Config source
|
|
115
|
+
* @param key - Image role
|
|
116
|
+
* @param image - Image ref
|
|
117
|
+
*/
|
|
118
|
+
export function upsertImage(source: string, key: string, image: string): string {
|
|
119
|
+
const keyLit = key.includes(".") ? `"${key}"` : key;
|
|
120
|
+
const line = ` ${keyLit}: "${image}",`;
|
|
121
|
+
const imagesRe = /images:\s*\{([\s\S]*?)\n\s*\}/;
|
|
122
|
+
const m = imagesRe.exec(source);
|
|
123
|
+
if (!m) return source;
|
|
124
|
+
const body = m[1]!;
|
|
125
|
+
if (new RegExp(`${keyLit}\\s*:`).test(body) || new RegExp(`"${key}"\\s*:`).test(body)) {
|
|
126
|
+
return source.replace(
|
|
127
|
+
new RegExp(`(["']?${key.replace(".", "\\.")}["']?\\s*:\\s*)"[^"]*"`),
|
|
128
|
+
`$1"${image}"`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return source.replace(imagesRe, `images: {${body}\n${line}\n }`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param env - dotenv text
|
|
136
|
+
* @param key - Variable name
|
|
137
|
+
* @param value - Value
|
|
138
|
+
*/
|
|
139
|
+
export function upsertEnv(env: string, key: string, value: string): string {
|
|
140
|
+
const line = `${key}=${value}`;
|
|
141
|
+
const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
|
|
142
|
+
if (re.test(env)) return env.replace(re, line);
|
|
143
|
+
const aiSection = /(# ── AI[^\n]*\n)/;
|
|
144
|
+
if (aiSection.test(env)) {
|
|
145
|
+
return env.replace(aiSection, `$1${line}\n`);
|
|
146
|
+
}
|
|
147
|
+
return `${env.trimEnd()}\n\n# ── AI ──────────────────────────────────────────────────────\n${line}\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param input - Setup choices
|
|
152
|
+
*/
|
|
153
|
+
export function renderAiTs(input: AiSetupApplyInput): string {
|
|
154
|
+
const chat = input.chatModel ?? "default";
|
|
155
|
+
const provider =
|
|
156
|
+
input.driver === "ollama"
|
|
157
|
+
? "ollama"
|
|
158
|
+
: input.driver === "anthropic"
|
|
159
|
+
? "anthropic"
|
|
160
|
+
: input.driver === "mock"
|
|
161
|
+
? "mock"
|
|
162
|
+
: "openai-compatible";
|
|
163
|
+
|
|
164
|
+
const lines = [
|
|
165
|
+
`import { ai } from "okengine";`,
|
|
166
|
+
``,
|
|
167
|
+
`export const smart = ai.model("smart", {`,
|
|
168
|
+
` provider: "${provider}",`,
|
|
169
|
+
` model: process.env.OKE_AI_MODEL ?? "${chat}",`,
|
|
170
|
+
`});`,
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
if (input.visionModel) {
|
|
174
|
+
lines.push(
|
|
175
|
+
``,
|
|
176
|
+
`export const vision = ai.model("vision", {`,
|
|
177
|
+
` provider: "${provider}",`,
|
|
178
|
+
` model: process.env.OKE_AI_VISION_MODEL ?? "${input.visionModel}",`,
|
|
179
|
+
`});`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (input.embedModel) {
|
|
184
|
+
lines.push(
|
|
185
|
+
``,
|
|
186
|
+
`export const embedModel = ai.model("embed", {`,
|
|
187
|
+
` provider: "${provider}",`,
|
|
188
|
+
` model: process.env.OKE_AI_EMBED_MODEL ?? "${input.embedModel}",`,
|
|
189
|
+
`});`,
|
|
190
|
+
``,
|
|
191
|
+
`export const docsEmbed = ai.embed("docs", { model: embedModel });`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
lines.push(``);
|
|
196
|
+
return `${lines.join("\n")}\n`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Ensure `src/app.ts` (or `src/core.ts`) imports `./ai`.
|
|
201
|
+
*
|
|
202
|
+
* @param cwd - Project root
|
|
203
|
+
*/
|
|
204
|
+
function ensureAiImported(cwd: string): void {
|
|
205
|
+
for (const rel of ["src/app.ts", "src/core.ts"]) {
|
|
206
|
+
const path = join(cwd, rel);
|
|
207
|
+
if (!existsSync(path)) continue;
|
|
208
|
+
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");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -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
|
+
}
|