@engenty/wizards-runtime 0.7.6 → 0.7.8
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/apps/runtime/dist/authoring/guide.js +12 -3
- package/apps/runtime/dist/browser/fast/classifier.js +9 -4
- package/apps/runtime/dist/cli/main.js +1 -1
- package/apps/runtime/dist/cli/menu.js +5 -1
- package/apps/runtime/dist/cli/ollama.js +302 -0
- package/apps/runtime/dist/cli/setup.js +102 -2
- package/apps/runtime/dist/decision-model.js +33 -0
- package/apps/runtime/dist/docs/dev/index.md +6 -1
- package/apps/runtime/dist/docs/dev/integration/channels.md +11 -0
- package/apps/runtime/dist/docs/dev/integration/claude-code-plugin.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/embed.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/index.md +36 -0
- package/apps/runtime/dist/docs/dev/integration/link-and-share.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/mcp-apps.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/mcp.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/meta.json +20 -0
- package/apps/runtime/dist/docs/dev/integration/openui.md +181 -0
- package/apps/runtime/dist/docs/dev/integration/own-ui.md +10 -0
- package/apps/runtime/dist/docs/dev/integration/results.md +11 -0
- package/apps/runtime/dist/docs/dev/integration/wordpress.md +10 -0
- package/apps/runtime/dist/docs/dev/meta.json +1 -1
- package/apps/runtime/dist/docs/user/command-line.md +1 -1
- package/apps/runtime/dist/docs/user/install.md +1 -0
- package/apps/runtime/dist/engine/decide.js +3 -1
- package/apps/runtime/dist/engine/steps.js +2 -1
- package/apps/runtime/dist/film/project.js +221 -65
- package/apps/runtime/dist/film/step.js +16 -5
- package/apps/runtime/dist/models.js +43 -3
- package/apps/runtime/dist/routes/runs.js +14 -1
- package/apps/runtime/dist/tools/code.js +173 -0
- package/apps/runtime/dist/tools/connector.js +6 -0
- package/apps/runtime/dist/tools/index.js +14 -3
- package/apps/web/dist/assets/{AboutDialog-DYgXPagX.js → AboutDialog-ByZyHXDj.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-BXqBW8j3.js → ChangelogDialog-QHYP0jAv.js} +1 -1
- package/apps/web/dist/assets/{ChatView-CK2--tsE.js → ChatView-DupTtI8K.js} +1 -1
- package/apps/web/dist/assets/EditorPage-k6qouLeS.js +4 -0
- package/apps/web/dist/assets/{FlowPreview-CusP7ci9.js → FlowPreview-Dx8AjXvE.js} +1 -1
- package/apps/web/dist/assets/{Harness-CV9uYzbv.js → Harness-NvIb36TG.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-BakW5q6s.js → NewWizardChannels-B2YgtJlt.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-BoA-LJuW.js → NewWizardPage-BgLY6SL6.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-DaD1XnOe.js → OnboardingPage-CxhwefJU.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-BFgk1DEv.js → ProjectPage-6ceSSS7W.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-C9MwEvxy.js → PublicRunner-6lQvReDx.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-DC2Kp7l8.js → ReadOnly-CGNHlgUT.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-CY6NZSrI.js → RunDrawer-TVGNnWyx.js} +2 -2
- package/apps/web/dist/assets/{RunnerView-BT26XgcY.js → RunnerView-DInSK0VZ.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-D3k5pXN5.js +1 -0
- package/apps/web/dist/assets/{SetupPage-DtFYAbwJ.js → SetupPage-Bj5frdzY.js} +1 -1
- package/apps/web/dist/assets/{SharePage-t2xwNS61.js → SharePage-hG7Z6SLy.js} +1 -1
- package/apps/web/dist/assets/changelog-CO9vKjV0.js +1 -0
- package/apps/web/dist/assets/{esm-DXeMu8Lb.js → esm-DVk3RJij.js} +1 -1
- package/apps/web/dist/assets/index-DfaoPjNW.css +1 -0
- package/apps/web/dist/assets/index-DlDsr9Lc.js +14 -0
- package/apps/web/dist/assets/oss-credits-X2JPiMOR.js +1 -0
- package/apps/web/dist/assets/{phone-CgrIS4_-.js → phone-CXXHXYnA.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-2IMOOlLl.js → settings-menu-DXzZCIMx.js} +1 -1
- package/apps/web/dist/assets/{ui-dzGvc3xk.js → ui-DQ4L2npj.js} +2 -2
- package/apps/web/dist/assets/{upload-BSuTilFF.js → upload-BnYHIfu5.js} +1 -1
- package/apps/web/dist/assets/{lock-R_CfU0Xo.js → wrench-BrUJ7SV6.js} +1 -1
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/install.sh +2 -2
- package/apps/web/dist/mcp/flow.html +4 -4
- package/node_modules/@engenty/wizards/dist/openui/Wizard.d.ts +20 -0
- package/node_modules/@engenty/wizards/dist/openui/Wizard.js +62 -0
- package/node_modules/@engenty/wizards/dist/openui/index.d.ts +9 -0
- package/node_modules/@engenty/wizards/dist/openui/index.js +9 -0
- package/node_modules/@engenty/wizards/dist/openui/message.d.ts +11 -0
- package/node_modules/@engenty/wizards/dist/openui/message.js +69 -0
- package/node_modules/@engenty/wizards/dist/openui/run.d.ts +8 -0
- package/node_modules/@engenty/wizards/dist/openui/run.js +137 -0
- package/node_modules/@engenty/wizards/dist/openui/server.d.ts +37 -0
- package/node_modules/@engenty/wizards/dist/openui/server.js +77 -0
- package/node_modules/@engenty/wizards/package.json +4 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +7 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.js +16 -0
- package/node_modules/@engenty-wizards/shared/dist/fit.js +7 -2
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +2 -0
- package/package.json +4 -3
- package/apps/web/dist/assets/EditorPage-jvcwyXbU.js +0 -3
- package/apps/web/dist/assets/SettingsPage-AFt9IDLt.js +0 -1
- package/apps/web/dist/assets/changelog-CATQFxCQ.js +0 -1
- package/apps/web/dist/assets/index-AfUa8xKD.css +0 -1
- package/apps/web/dist/assets/index-Drw7FMTQ.js +0 -14
- package/apps/web/dist/assets/oss-credits-cZ9NUaLZ.js +0 -1
|
@@ -133,7 +133,7 @@ type AgentStep = {
|
|
|
133
133
|
// (fields name the keys later steps read). Use it where a model would only fetch and copy
|
|
134
134
|
// data (free appointment times) — it answers in a second instead of half a minute.
|
|
135
135
|
// String inputs are templates; a filled-in whole number or true/false is passed as one.
|
|
136
|
-
tools: ("web_search"|"web_fetch"|"browser"|"sandbox"|"image"|"http"|"pages")[]
|
|
136
|
+
tools: ("web_search"|"web_fetch"|"browser"|"sandbox"|"image"|"http"|"pages"|"code")[]
|
|
137
137
|
mcp?: string[] // ids of the project's MCP servers this step may use
|
|
138
138
|
connections?: string[] // ids of wizard connections this step may read (mail → mail_search, mail_read, mail_save)
|
|
139
139
|
output: { format: "text"|"markdown"|"json", fields?: { id, kind: "text"|"number"|"list"|"table"|"yesno"|"choice"|"score", description, columns?: string[], options?: string[] }[], schema?: Schema, surface?: SurfaceComponent[] | true }
|
|
@@ -156,6 +156,10 @@ type AgentStep = {
|
|
|
156
156
|
// project_list (by categories), page_read / table_read / project_document (an item whole, by its path).
|
|
157
157
|
// "pages" brings: page_read / page_write — Markdown pages of the space the wizard keeps for every run (notes, a log
|
|
158
158
|
// that grows with append), shown to the admin under Space → Data; page_read also reads Knowledge by path.
|
|
159
|
+
// "code" brings: execute_typescript — the model writes one program that calls the step's other tools (not the browser) as
|
|
160
|
+
// functions: loops, Promise.all, filtering, sums; only its answer reaches the model. For many calls of one kind (fetch 30
|
|
161
|
+
// pages, read every row of a list, search a mailbox month by month). It needs other tools to call; a change the person
|
|
162
|
+
// must allow first, generate_image and scan_documents are never called from a program.
|
|
159
163
|
// "browser" brings: browser_open/read/click/type/select/scroll/press/back/wait, browser_screenshot (the model looks at the page),
|
|
160
164
|
// browser_run_fast (a decision model clicks through a page's mechanical part, where one is reached),
|
|
161
165
|
// browser_request_credentials (the PERSON types a login into the page — the model never sees it; sign-ins can be remembered),
|
|
@@ -255,10 +259,14 @@ type FilmStep = {
|
|
|
255
259
|
brief: string // a template: what the film is — story, look, length, what to use. The kit carries the craft.
|
|
256
260
|
skills?: string[] // skill packages the client reads; default ["hyperframes"]. A workspace package: "skills/<name>" (has SKILL.md)
|
|
257
261
|
kit?: string // workspace path of a STYLE KIT (.zip): a showreel made once with its rules (frame.md),
|
|
258
|
-
// techniques (SHOWCASE.md), helpers and stills — the bar every film of this wizard reaches
|
|
262
|
+
// techniques (SHOWCASE.md), helpers and stills — the bar every film of this wizard reaches.
|
|
263
|
+
// A kit with base/ (index.9x16.html, index.16x9.html, index.1x1.html, PARTS.md, BRAND.md,
|
|
264
|
+
// scene templates) is the project a film starts from: the client composes instead of building
|
|
259
265
|
inputs?: { [name]: string } // name (lowercase, -) → fieldId | steps.stepId: voice lines, pictures, uploads become files in
|
|
260
266
|
// input/<name>/, text in input/<name>.md, json in input/<name>.json
|
|
261
267
|
format?: "16:9" | "9:16" | "1:1"
|
|
268
|
+
from?: string // "steps.<filmStepId>": start from that earlier film's code and lay it out for this format —
|
|
269
|
+
// the second format of a film in minutes; made from scratch when that step did not run
|
|
262
270
|
seconds?: number // about how long
|
|
263
271
|
model?: "standard" | "high" // default standard
|
|
264
272
|
effort?: Effort
|
|
@@ -267,7 +275,8 @@ type FilmStep = {
|
|
|
267
275
|
// the AI client installed on the person's computer (Claude Code) writes the film as code in a folder of its own, with the
|
|
268
276
|
// skills, the kit, the space's brand (colours, logo, facts) and the inputs; it looks at its frames and fixes them; the
|
|
269
277
|
// runtime renders the MP4. It runs only where Claude Code is set up as the AI subscription — never on credits or in the
|
|
270
|
-
// cloud — and takes
|
|
278
|
+
// cloud — and takes 5–40 minutes (less with a base kit; what a base film makes from the space's brand is kept for the
|
|
279
|
+
// next films there). Sounds come from a fixed catalog (CC0); voice comes from a generate voice step before it.
|
|
271
280
|
|
|
272
281
|
type ReviewStep = { type: "review", show: (stepId | "lists.<listId>")[], edit?: boolean, regenerate?: boolean }
|
|
273
282
|
// shows earlier outputs; the person accepts, edits text outputs (edit), or asks for a new version with a note (regenerate)
|
|
@@ -56,12 +56,16 @@ export function validateChoiceAnswer(answer, ids) {
|
|
|
56
56
|
}
|
|
57
57
|
const RETRYABLE = new Set([429, 503, 529]);
|
|
58
58
|
/**
|
|
59
|
-
* The run's way to a decision model (credits, an AI Gateway key or a TypeSafe key), or
|
|
60
|
-
* when there is none — then the fast loop is not offered.
|
|
59
|
+
* The run's way to a hosted decision model (credits, an AI Gateway key or a TypeSafe key), or
|
|
60
|
+
* null when there is none — then the fast loop is not offered. Clef in the local Ollama is not
|
|
61
|
+
* taken: a page's elements are more than its 26 options, and on a laptop one step took over a
|
|
62
|
+
* minute (base M4, Ollama 0.35 and 0.40, 20 to 160 s a call, also with the elements asked in
|
|
63
|
+
* groups of 26) and stopped at a search box Jev fills, where the agent's own browser tools take
|
|
64
|
+
* seconds.
|
|
61
65
|
*/
|
|
62
66
|
export async function classifierClient(signal) {
|
|
63
67
|
const access = await systemOneAccess().catch(() => null);
|
|
64
|
-
if (!access) {
|
|
68
|
+
if (!access || access.local) {
|
|
65
69
|
return null;
|
|
66
70
|
}
|
|
67
71
|
return {
|
|
@@ -83,7 +87,8 @@ export async function classifierClient(signal) {
|
|
|
83
87
|
return (await res.json());
|
|
84
88
|
}
|
|
85
89
|
if (!(RETRYABLE.has(res.status) && attempt < 2)) {
|
|
86
|
-
|
|
90
|
+
const why = (await res.text().catch(() => "")).trim().slice(0, 300);
|
|
91
|
+
throw new Error(`The decision model answered ${res.status}${why ? `: ${why}` : ""}`);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
},
|
|
@@ -29,7 +29,7 @@ ${line(command(), "show the status and a menu (start, stop, update…); runs the
|
|
|
29
29
|
${more("(needs a terminal)")}
|
|
30
30
|
${line(`${command()} start`, "start it in this terminal and open the studio (Ctrl-C stops it)")}
|
|
31
31
|
${line(`${command()} studio`, "open the studio, starting it in the background if needed")}
|
|
32
|
-
${line(`${command()} setup`, "set up your engenty account, an AI client, ffmpeg and start at login")}
|
|
32
|
+
${line(`${command()} setup`, "set up your engenty account, an AI client, ffmpeg, a decision model and start at login")}
|
|
33
33
|
${line(`${command()} account`, "show your engenty account; add sign-in or sign-out to change it")}
|
|
34
34
|
${line(`${command()} open`, "sign this browser in to the running studio (--print: only print the link)")}
|
|
35
35
|
${line(`${command()} status`, "show what’s installed and running")}
|
|
@@ -71,7 +71,11 @@ function choices({ running, atLogin, release, account }) {
|
|
|
71
71
|
hint: account.kind === "linked"
|
|
72
72
|
? `signed in as ${describeAccount(account)}`
|
|
73
73
|
: describeAccount(account),
|
|
74
|
-
}, { value: "status", label: "Status", hint: "what’s installed, and any problems" }, {
|
|
74
|
+
}, { value: "status", label: "Status", hint: "what’s installed, and any problems" }, {
|
|
75
|
+
value: "setup",
|
|
76
|
+
label: "Setup",
|
|
77
|
+
hint: "account, AI client, ffmpeg, decision model, start at login",
|
|
78
|
+
}, {
|
|
75
79
|
value: "uninstall",
|
|
76
80
|
label: "Uninstall",
|
|
77
81
|
hint: "remove it from this computer; asks about your data first",
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, openSync, realpathSync, rmSync, } from "node:fs";
|
|
3
|
+
import { statfs } from "node:fs/promises";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
+
import { decisionModelIn, ollamaRoot } from "../decision-model.js";
|
|
8
|
+
import { runLogged } from "./exec.js";
|
|
9
|
+
import { findOnPath } from "./machine.js";
|
|
10
|
+
/** The first Ollama that answers decision questions at /v1/systemone. */
|
|
11
|
+
const DECISIONS_SINCE = [0, 35, 0];
|
|
12
|
+
/** What Clef-flash takes on disk, and about as much memory while it answers, in GB. */
|
|
13
|
+
export const DECISION_MODEL_GB = 11;
|
|
14
|
+
/** The Ollama app on a Mac: its server and its command are inside it. */
|
|
15
|
+
const MAC_APPS = ["/Applications/Ollama.app", join(homedir(), "Applications", "Ollama.app")];
|
|
16
|
+
const MAC_DOWNLOAD = "https://ollama.com/download/Ollama-darwin.zip";
|
|
17
|
+
/** Ollama's own installer on Linux: it puts the server in a systemd service, with sudo. */
|
|
18
|
+
const LINUX_SCRIPT = "https://ollama.com/install.sh";
|
|
19
|
+
/**
|
|
20
|
+
* What that installer needs and a fresh Debian lacks: zstd, to unpack Ollama. Installed first
|
|
21
|
+
* with the system's package manager, under the same password.
|
|
22
|
+
*/
|
|
23
|
+
const LINUX_ZSTD = [
|
|
24
|
+
"if ! command -v zstd >/dev/null 2>&1; then",
|
|
25
|
+
" if command -v apt-get >/dev/null 2>&1; then sudo apt-get update -qq && sudo apt-get install -y -qq zstd",
|
|
26
|
+
" elif command -v dnf >/dev/null 2>&1; then sudo dnf install -y -q zstd",
|
|
27
|
+
" elif command -v pacman >/dev/null 2>&1; then sudo pacman -S --noconfirm --needed zstd",
|
|
28
|
+
" elif command -v zypper >/dev/null 2>&1; then sudo zypper --non-interactive install zstd",
|
|
29
|
+
" fi",
|
|
30
|
+
"fi",
|
|
31
|
+
].join("\n");
|
|
32
|
+
/** Where Ollama answers: OLLAMA_URL as the runtime takes it (…/v1), without the /v1. */
|
|
33
|
+
export function ollamaAddress(env = process.env) {
|
|
34
|
+
return ollamaRoot(env.OLLAMA_URL || "http://127.0.0.1:11434/v1");
|
|
35
|
+
}
|
|
36
|
+
function executable(file) {
|
|
37
|
+
try {
|
|
38
|
+
accessSync(file, constants.X_OK);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The `ollama` command: on the PATH, or inside the app on a Mac that never linked it. */
|
|
46
|
+
export function findOllama(env = process.env) {
|
|
47
|
+
const onPath = findOnPath("ollama", env.PATH ?? "");
|
|
48
|
+
if (onPath || process.platform !== "darwin") {
|
|
49
|
+
return onPath;
|
|
50
|
+
}
|
|
51
|
+
return (MAC_APPS.map((app) => join(app, "Contents", "Resources", "ollama")).find(executable) ?? null);
|
|
52
|
+
}
|
|
53
|
+
async function getJson(url) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
56
|
+
return res.ok ? (await res.json()) : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function ollamaState(env = process.env) {
|
|
63
|
+
const root = ollamaAddress(env);
|
|
64
|
+
const version = (await getJson(`${root}/api/version`))?.version ?? null;
|
|
65
|
+
const tags = version ? await getJson(`${root}/api/tags`) : null;
|
|
66
|
+
return {
|
|
67
|
+
path: findOllama(env),
|
|
68
|
+
version,
|
|
69
|
+
model: decisionModelIn((tags?.models ?? []).map((m) => m.name)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Whether this Ollama is new enough to answer decision questions. */
|
|
73
|
+
export function servesDecisions(version) {
|
|
74
|
+
const parts = version.split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
|
|
75
|
+
for (const [i, wanted] of DECISIONS_SINCE.entries()) {
|
|
76
|
+
const have = parts[i] ?? 0;
|
|
77
|
+
if (have !== wanted) {
|
|
78
|
+
return have > wanted;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/** Free space where Ollama keeps its models (the home folder), in GB. */
|
|
84
|
+
export async function freeGb(dir = homedir()) {
|
|
85
|
+
try {
|
|
86
|
+
const stats = await statfs(dir);
|
|
87
|
+
return Math.floor((stats.bavail * stats.bsize) / 2 ** 30);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The app an `ollama` command belongs to on a Mac; null for one from Homebrew or elsewhere. */
|
|
94
|
+
function macApp(path) {
|
|
95
|
+
if (process.platform !== "darwin") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!path) {
|
|
99
|
+
return MAC_APPS.find((app) => existsSync(app)) ?? null;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const real = realpathSync(path);
|
|
103
|
+
const at = real.indexOf(".app/");
|
|
104
|
+
return at > 0 ? real.slice(0, at + 4) : null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Installs Ollama. On a Mac its app goes into Applications (~/Applications where that is not
|
|
112
|
+
* writable) and asks for no password; on Linux Ollama's own script runs in this terminal and
|
|
113
|
+
* asks for one, as it installs a system service.
|
|
114
|
+
*/
|
|
115
|
+
export async function installOllama(paths, onLine) {
|
|
116
|
+
const logFile = join(paths.logs, "install.log");
|
|
117
|
+
if (process.platform === "darwin") {
|
|
118
|
+
let target = "/Applications";
|
|
119
|
+
try {
|
|
120
|
+
accessSync(target, constants.W_OK);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
target = join(homedir(), "Applications");
|
|
124
|
+
mkdirSync(target, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
const tmp = mkdtempSync(join(tmpdir(), "ollama-"));
|
|
127
|
+
try {
|
|
128
|
+
const zip = join(tmp, "Ollama.zip");
|
|
129
|
+
const download = await runLogged("curl", ["-fsSL", MAC_DOWNLOAD, "-o", zip], {
|
|
130
|
+
logFile,
|
|
131
|
+
onLine,
|
|
132
|
+
});
|
|
133
|
+
if (download.code !== 0) {
|
|
134
|
+
return { ok: false, message: `The download failed. ${lastLine(download.tail)}` };
|
|
135
|
+
}
|
|
136
|
+
const unpack = await runLogged("ditto", ["-x", "-k", zip, target], { logFile, onLine });
|
|
137
|
+
if (unpack.code !== 0) {
|
|
138
|
+
return { ok: false, message: `It could not be unpacked. ${lastLine(unpack.tail)}` };
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, message: join(target, "Ollama.app") };
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (process.platform === "linux") {
|
|
147
|
+
const code = await new Promise((resolve) => {
|
|
148
|
+
const child = spawn("sh", ["-c", `${LINUX_ZSTD}\ncurl -fsSL ${LINUX_SCRIPT} | sh`], {
|
|
149
|
+
stdio: "inherit",
|
|
150
|
+
});
|
|
151
|
+
child.on("error", () => resolve(127));
|
|
152
|
+
child.on("close", (status) => resolve(status ?? 1));
|
|
153
|
+
});
|
|
154
|
+
return code === 0
|
|
155
|
+
? { ok: true, message: findOllama() ?? "ollama" }
|
|
156
|
+
: { ok: false, message: `Ollama’s installer stopped (exit ${code}).` };
|
|
157
|
+
}
|
|
158
|
+
return { ok: false, message: "Ollama is installed by this setup on macOS and Linux only." };
|
|
159
|
+
}
|
|
160
|
+
const lastLine = (text) => text.split("\n").pop() ?? "";
|
|
161
|
+
/**
|
|
162
|
+
* Whether the Ollama app asks for the Mac's password when it starts: the first time, it wants to
|
|
163
|
+
* link its `ollama` command into /usr/local/bin, and its server waits for the answer (cancel
|
|
164
|
+
* works too). Not once that link points into the app.
|
|
165
|
+
*/
|
|
166
|
+
export function appAsksForPassword(path) {
|
|
167
|
+
const app = macApp(path);
|
|
168
|
+
if (!app) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return realpathSync("/usr/local/bin/ollama") !== join(app, "Contents", "Resources", "ollama");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Starts Ollama's server where it does not run: the app on a Mac (it then starts at login by
|
|
180
|
+
* itself), Homebrew's service, else `ollama serve` in the background. Its version once it
|
|
181
|
+
* answers; null where it does not in time — three minutes for the app, which may wait for a
|
|
182
|
+
* password, half a minute otherwise.
|
|
183
|
+
*/
|
|
184
|
+
export async function startOllama(path, paths) {
|
|
185
|
+
const logFile = join(paths.logs, "install.log");
|
|
186
|
+
const app = macApp(path);
|
|
187
|
+
const brew = findOnPath("brew", process.env.PATH ?? "");
|
|
188
|
+
let real = path ?? "";
|
|
189
|
+
try {
|
|
190
|
+
real = path ? realpathSync(path) : "";
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// a dangling link: started as it is
|
|
194
|
+
}
|
|
195
|
+
if (app) {
|
|
196
|
+
await runLogged("open", ["-g", app], { logFile });
|
|
197
|
+
}
|
|
198
|
+
else if (brew && real.includes("/Cellar/ollama/")) {
|
|
199
|
+
await runLogged(brew, ["services", "start", "ollama"], { logFile });
|
|
200
|
+
}
|
|
201
|
+
else if (path) {
|
|
202
|
+
mkdirSync(paths.logs, { recursive: true });
|
|
203
|
+
const out = openSync(join(paths.logs, "ollama.log"), "a");
|
|
204
|
+
spawn(path, ["serve"], { detached: true, stdio: ["ignore", out, out] }).unref();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const deadline = Date.now() + (app ? 180_000 : 30_000);
|
|
210
|
+
while (Date.now() < deadline) {
|
|
211
|
+
const { version } = await ollamaState();
|
|
212
|
+
if (version) {
|
|
213
|
+
return version;
|
|
214
|
+
}
|
|
215
|
+
await sleep(500);
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Whether Ollama runs the model: one question it has to answer. Null when it does; otherwise
|
|
221
|
+
* Ollama's reason, such as a newer Ollama that no longer runs a model in the format it was pulled
|
|
222
|
+
* in. The model is loaded for it, which takes some seconds the first time.
|
|
223
|
+
*/
|
|
224
|
+
export async function decisionModelProblem(model, env = process.env) {
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(`${ollamaAddress(env)}/v1/systemone`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "content-type": "application/json" },
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
model,
|
|
231
|
+
state: "The setup checks the decision model.",
|
|
232
|
+
questions: { check: { type: "noul", instructions: "Is this a check?" } },
|
|
233
|
+
}),
|
|
234
|
+
signal: AbortSignal.timeout(180_000),
|
|
235
|
+
});
|
|
236
|
+
if (res.ok) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const text = (await res.text()).trim();
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(text).error ?? text;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return text || `Ollama answered ${res.status}.`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return error.message;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Pulls a model into the running Ollama; `onProgress` hears the bytes so far and in all as they
|
|
253
|
+
* come. Rejects with Ollama's own reason.
|
|
254
|
+
*/
|
|
255
|
+
export async function pullModel(model, onProgress, env = process.env) {
|
|
256
|
+
const res = await fetch(`${ollamaAddress(env)}/api/pull`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "content-type": "application/json" },
|
|
259
|
+
body: JSON.stringify({ model, stream: true }),
|
|
260
|
+
});
|
|
261
|
+
if (!(res.ok && res.body)) {
|
|
262
|
+
throw new Error(`Ollama answered ${res.status}. ${await res.text().catch(() => "")}`.trim());
|
|
263
|
+
}
|
|
264
|
+
// Each part of the model reports on its own; the sum is the download.
|
|
265
|
+
const parts = new Map();
|
|
266
|
+
const reader = res.body.getReader();
|
|
267
|
+
const decoder = new TextDecoder();
|
|
268
|
+
let pending = "";
|
|
269
|
+
let finished = false;
|
|
270
|
+
for (;;) {
|
|
271
|
+
const { done, value } = await reader.read();
|
|
272
|
+
if (done) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
pending += decoder.decode(value, { stream: true });
|
|
276
|
+
const lines = pending.split("\n");
|
|
277
|
+
pending = lines.pop() ?? "";
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
if (!line.trim()) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const event = JSON.parse(line);
|
|
283
|
+
if (event.error) {
|
|
284
|
+
throw new Error(event.error);
|
|
285
|
+
}
|
|
286
|
+
if (event.digest && event.total) {
|
|
287
|
+
parts.set(event.digest, { total: event.total, completed: event.completed ?? 0 });
|
|
288
|
+
let sum = 0;
|
|
289
|
+
let all = 0;
|
|
290
|
+
for (const part of parts.values()) {
|
|
291
|
+
sum += part.completed;
|
|
292
|
+
all += part.total;
|
|
293
|
+
}
|
|
294
|
+
onProgress(sum, all);
|
|
295
|
+
}
|
|
296
|
+
finished ||= event.status === "success";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!finished) {
|
|
300
|
+
throw new Error("The download broke off.");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { homedir, totalmem } from "node:os";
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
|
+
import { LOCAL_DECISION_MODEL } from "../decision-model.js";
|
|
5
6
|
import { accountStep } from "./account.js";
|
|
6
7
|
import { autostartForStatus, setAutostart } from "./autostart.js";
|
|
7
8
|
import { installClient, installCommand } from "./clients.js";
|
|
@@ -9,6 +10,7 @@ import { appStates, connectApps } from "./connect.js";
|
|
|
9
10
|
import { runLogged } from "./exec.js";
|
|
10
11
|
import { command, installedByScript, isCheckout, packageVersion, readInstallNote, writeInstallNote, } from "./home.js";
|
|
11
12
|
import { CLIENTS, detectClients, findChrome, findFfmpeg, findOnPath, suggestedClient, vendorApp, } from "./machine.js";
|
|
13
|
+
import { appAsksForPassword, DECISION_MODEL_GB, decisionModelProblem, freeGb, installOllama, ollamaState, pullModel, servesDecisions, startOllama, } from "./ollama.js";
|
|
12
14
|
import { runningRuntime, settings } from "./start.js";
|
|
13
15
|
import { badge, busy, cmd, cyan, dim, tilde } from "./ui.js";
|
|
14
16
|
/** The studio as an app of its own: Chrome (or Edge) installs it from the page. */
|
|
@@ -123,6 +125,101 @@ async function toolsStep(interactive) {
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
}
|
|
128
|
+
const gb = (bytes) => (bytes / 2 ** 30).toFixed(1);
|
|
129
|
+
/**
|
|
130
|
+
* A decision model on this computer: Ollama with Cloudflare's Clef-flash. It answers a wizard's
|
|
131
|
+
* decisions (which branch, which option) without a key or credits. Asked, never assumed: it is a
|
|
132
|
+
* download of about 11 GB.
|
|
133
|
+
*/
|
|
134
|
+
async function decisionModelStep(paths, interactive) {
|
|
135
|
+
const state = await ollamaState();
|
|
136
|
+
if (state.version && !servesDecisions(state.version)) {
|
|
137
|
+
p.log.warn(`Ollama ${state.version} is too old for a decision model (it needs 0.35 or newer).\n${dim(`Update Ollama, then run ${cmd("setup")} again.`)}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Pulled is not enough: a newer Ollama may no longer run a model pulled in an older format.
|
|
141
|
+
let broken = null;
|
|
142
|
+
if (state.model) {
|
|
143
|
+
const check = busy(`Checking ${state.model}…`);
|
|
144
|
+
broken = await decisionModelProblem(state.model);
|
|
145
|
+
if (!broken) {
|
|
146
|
+
check.stop(`Decision model: ${state.model} ${dim(`in Ollama ${state.version}`)}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
check.fail(`${state.model} doesn’t run in Ollama ${state.version}: ${broken}`);
|
|
150
|
+
}
|
|
151
|
+
const model = state.model ?? LOCAL_DECISION_MODEL;
|
|
152
|
+
const what = state.path ? `Clef-flash into Ollama` : "Ollama with Clef-flash";
|
|
153
|
+
if (!interactive) {
|
|
154
|
+
p.log.info(dim(broken
|
|
155
|
+
? `To download it again: ${cyan(`ollama pull ${model}`)}`
|
|
156
|
+
: `Optional: a decision model on this computer (${what}, about ${DECISION_MODEL_GB} GB). To add it: ${cmd("setup")}`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const free = await freeGb();
|
|
160
|
+
if (free !== null && free < DECISION_MODEL_GB + 2) {
|
|
161
|
+
p.log.warn(`There isn’t room for a decision model: it needs about ${DECISION_MODEL_GB + 2} GB and ${free} GB are free.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const memory = Math.round(totalmem() / 2 ** 30);
|
|
165
|
+
const answer = await p.confirm({
|
|
166
|
+
message: broken
|
|
167
|
+
? `Download ${model} again, in the format this Ollama runs? About ${DECISION_MODEL_GB} GB.`
|
|
168
|
+
: `Install a decision model on this computer? ${what} makes the wizards’ decisions (which branch, which option) here, without a key or credits. About ${DECISION_MODEL_GB} GB${memory < 16 ? `; it needs 16 GB of memory and this computer has ${memory}` : ""}.`,
|
|
169
|
+
initialValue: Boolean(broken),
|
|
170
|
+
});
|
|
171
|
+
if (cancelled(answer)) {
|
|
172
|
+
return "cancelled";
|
|
173
|
+
}
|
|
174
|
+
if (!answer) {
|
|
175
|
+
p.log.info(dim(`To add it later: ${cmd("setup")}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let { path: ollama, version } = state;
|
|
179
|
+
if (!ollama) {
|
|
180
|
+
// On Linux Ollama's installer writes to this terminal itself, and may ask for a password.
|
|
181
|
+
const task = process.platform === "linux" ? null : p.taskLog({ title: "Installing Ollama", limit: 6 });
|
|
182
|
+
if (!task) {
|
|
183
|
+
p.log.info("Ollama’s own installer runs now and asks for your password.");
|
|
184
|
+
}
|
|
185
|
+
const outcome = await installOllama(paths, (line) => task?.message(line));
|
|
186
|
+
if (!outcome.ok) {
|
|
187
|
+
const text = `Ollama didn’t install. ${outcome.message}`;
|
|
188
|
+
task ? task.error(text) : p.log.error(text);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const installed = `Ollama installed ${dim(tilde(outcome.message))}`;
|
|
192
|
+
task ? task.success(installed) : p.log.success(installed);
|
|
193
|
+
// Linux's installer starts it as a service already.
|
|
194
|
+
({ path: ollama, version } = await ollamaState());
|
|
195
|
+
}
|
|
196
|
+
if (!version) {
|
|
197
|
+
if (appAsksForPassword(ollama)) {
|
|
198
|
+
p.log.info("Ollama starts now and asks once for your Mac’s password, to put its `ollama` command in /usr/local/bin. Enter it, or cancel: Ollama runs either way.");
|
|
199
|
+
}
|
|
200
|
+
const spinner = p.spinner();
|
|
201
|
+
spinner.start("Waiting for Ollama");
|
|
202
|
+
version = await startOllama(ollama, paths);
|
|
203
|
+
if (!version) {
|
|
204
|
+
spinner.error(`Ollama isn’t answering. Start it, then run ${cmd("setup")} again.`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!servesDecisions(version)) {
|
|
208
|
+
spinner.error(`Ollama ${version} is too old for a decision model (it needs 0.35 or newer).`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
spinner.stop(`Ollama ${version} is running`);
|
|
212
|
+
}
|
|
213
|
+
const spinner = p.spinner();
|
|
214
|
+
spinner.start(`Downloading ${model}`);
|
|
215
|
+
try {
|
|
216
|
+
await pullModel(model, (done, total) => spinner.message(`Downloading ${model} ${gb(done)} of ${gb(total)} GB`));
|
|
217
|
+
spinner.stop(`Decision model: ${model} ${dim(`in Ollama ${version}`)}`);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
spinner.error(`${model} didn’t download. ${error.message}\n${dim(`To try again: ${cyan(`ollama pull ${model}`)}`)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
126
223
|
/**
|
|
127
224
|
* Starting at login: on a Mac a LaunchAgent starts the runtime, on Linux a systemd user
|
|
128
225
|
* service does. Asked, never assumed; `autostart off` undoes it.
|
|
@@ -287,7 +384,7 @@ async function howToUse(paths) {
|
|
|
287
384
|
}
|
|
288
385
|
/**
|
|
289
386
|
* The guided setup: the account this computer joins, then what this machine has, what is
|
|
290
|
-
* missing, and what to install — an AI client to think with, ffmpeg, starting at login. It
|
|
387
|
+
* missing, and what to install — an AI client to think with, ffmpeg, a decision model, starting at login. It
|
|
291
388
|
* starts nothing; it ends with what runs and how to start, stop and check it. Returns false
|
|
292
389
|
* when it was stopped.
|
|
293
390
|
*/
|
|
@@ -308,6 +405,9 @@ export async function setup(paths, options) {
|
|
|
308
405
|
if ((await toolsStep(interactive)) === "cancelled") {
|
|
309
406
|
return false;
|
|
310
407
|
}
|
|
408
|
+
if ((await decisionModelStep(paths, interactive)) === "cancelled") {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
311
411
|
if ((await autostartStep(paths, interactive)) === "cancelled") {
|
|
312
412
|
return false;
|
|
313
413
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decision model on the person's own machine: Cloudflare's Clef-flash (open weights, Apache
|
|
3
|
+
* 2.0), which Ollama 0.35 and newer serves at /v1/systemone with the same questions and answers
|
|
4
|
+
* as TypeSafe's Jev. The setup offers to pull it; the runtime asks it where no hosted decision
|
|
5
|
+
* model is set up.
|
|
6
|
+
*/
|
|
7
|
+
/** The model the setup pulls. Clef, its larger sibling, is taken as well where it is there. */
|
|
8
|
+
export const LOCAL_DECISION_MODEL = "clef-flash";
|
|
9
|
+
/** Ollama's own address from the one the runtime uses for its OpenAI-style API (…/v1). */
|
|
10
|
+
export function ollamaRoot(url) {
|
|
11
|
+
return url.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
12
|
+
}
|
|
13
|
+
/** The decision model among the models Ollama lists: Clef-flash, else Clef. */
|
|
14
|
+
export function decisionModelIn(names) {
|
|
15
|
+
const named = (base) => names.find((n) => n === base || n.startsWith(`${base}:`));
|
|
16
|
+
return named(LOCAL_DECISION_MODEL) ?? named("clef") ?? null;
|
|
17
|
+
}
|
|
18
|
+
/** The decision model the Ollama at `url` holds; null where it does not run or has none. */
|
|
19
|
+
export async function ollamaDecisionModel(url, timeoutMs = 1500) {
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${ollamaRoot(url)}/api/tags`, {
|
|
22
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const { models } = (await res.json());
|
|
28
|
+
return decisionModelIn((models ?? []).map((m) => m.name));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Developer
|
|
3
3
|
description: Extend engenty wizards with plugins – tools for AI steps, routes with their own tables, pages in the studio.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.3
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You extend engenty wizards with **plugins**. A plugin adds to a runtime without changing its
|
|
@@ -21,6 +21,11 @@ code, and the runtime loads it from its files while it runs.
|
|
|
21
21
|
A wizard is JSON, and it travels as a `.wizard` file. [The wizard format](./wizard-format.md)
|
|
22
22
|
describes the format, the JSON Schema for your editor and `wizards validate`.
|
|
23
23
|
|
|
24
|
+
## Wizards in other systems
|
|
25
|
+
|
|
26
|
+
[Integration](./integration/index.md) shows how a published wizard reaches other systems: people
|
|
27
|
+
over a link, a website or a channel, AI apps over MCP, and your own code.
|
|
28
|
+
|
|
24
29
|
## What you need
|
|
25
30
|
|
|
26
31
|
- engenty wizards on your computer, or a checkout of
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Channels
|
|
3
|
+
description: Run a wizard as a chat, a conversation or a call, and over WhatsApp, SMS or the phone.
|
|
4
|
+
version: 0.7.4
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This page is not written yet.
|
|
8
|
+
|
|
9
|
+
Until then, [Channels](../../user/test-and-share.md#channels) in the user guide describes how a
|
|
10
|
+
wizard runs as a chat, a conversation or a call, and over WhatsApp, SMS or the phone. A plugin
|
|
11
|
+
adds a channel of its own with a [runner](../plugins/runners.md).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Claude Code plugin
|
|
3
|
+
description: Build and use wizards from Claude Code with the /wizard command and its skills.
|
|
4
|
+
version: 0.7.4
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This page is not written yet.
|
|
8
|
+
|
|
9
|
+
Until then, [The Claude Code plugin](../../user/ai-apps.md#the-claude-code-plugin) in the user
|
|
10
|
+
guide shows how you install it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Embed
|
|
3
|
+
description: Put a published wizard on your website with one script tag.
|
|
4
|
+
version: 0.7.4
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This page is not written yet.
|
|
8
|
+
|
|
9
|
+
Until then, [Embed in a website](../../user/test-and-share.md#embed-in-a-website) in the user
|
|
10
|
+
guide shows the script tag and its options.
|