@engenty/wizards-runtime 0.7.7 → 0.7.9
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 +8 -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/clients.js +118 -0
- package/apps/runtime/dist/db/client.js +37 -3
- package/apps/runtime/dist/db/migrations/tenant/0017_run_client.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0017_snapshot.json +3333 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +2 -0
- package/apps/runtime/dist/decision-model.js +33 -0
- package/apps/runtime/dist/docs/dev/api.md +39 -0
- package/apps/runtime/dist/docs/dev/index.md +1 -0
- package/apps/runtime/dist/docs/dev/integration/openui.md +5 -3
- package/apps/runtime/dist/docs/dev/meta.json +1 -1
- package/apps/runtime/dist/docs/user/ai-apps.md +14 -3
- 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/runner.js +1 -0
- package/apps/runtime/dist/film/project.js +221 -65
- package/apps/runtime/dist/film/step.js +16 -5
- package/apps/runtime/dist/index.js +13 -4
- package/apps/runtime/dist/models.js +43 -3
- package/apps/runtime/dist/plugin.js +24 -8
- package/apps/runtime/dist/routes/runs.js +9 -0
- package/apps/runtime/dist/tools/code.js +53 -13
- package/apps/web/dist/assets/AboutDialog-CShc0mRo.js +1 -0
- package/apps/web/dist/assets/ChangelogDialog-C4dVSwtN.js +1 -0
- package/apps/web/dist/assets/ChatView-KkOboyOo.js +10 -0
- package/apps/web/dist/assets/EditorPage-lQ3YgsFT.js +4 -0
- package/apps/web/dist/assets/{FlowPreview-Dj2gAfuf.js → FlowPreview-oX_Emm7u.js} +1 -1
- package/apps/web/dist/assets/{Harness-SLbsntQF.js → Harness-D6mgLWTb.js} +2 -2
- package/apps/web/dist/assets/{NewWizardChannels-CsDMlji7.js → NewWizardChannels-CPlh_37J.js} +1 -1
- package/apps/web/dist/assets/NewWizardPage-D0FVJtXK.js +7 -0
- package/apps/web/dist/assets/OnboardingPage-BXPOMrQR.js +1 -0
- package/apps/web/dist/assets/ProjectPage-DroTprqs.js +1 -0
- package/apps/web/dist/assets/PublicRunner-j3qoKjQQ.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-CK0piLUV.js → ReadOnly-CKgSQ2z1.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-CJZ_ISAd.js +4 -0
- package/apps/web/dist/assets/RunnerView-BW1h4x63.js +1 -0
- package/apps/web/dist/assets/SettingsPage-BHZ0Z-og.js +1 -0
- package/apps/web/dist/assets/SetupPage-CDkYkjJX.js +1 -0
- package/apps/web/dist/assets/{SharePage-Cc3Opj2F.js → SharePage-BlVjVjHG.js} +1 -1
- package/apps/web/dist/assets/changelog-Be0i5Jjw.js +1 -0
- package/apps/web/dist/assets/engenty-WpMi3xwq.js +1 -0
- package/apps/web/dist/assets/{esm-CfnFE_jE.js → esm-C-ZWeuhU.js} +1 -1
- package/apps/web/dist/assets/{fluffy-engenty-ClRou7yu.js → fluffy-engenty-DPNHytrz.js} +135 -16
- package/apps/web/dist/assets/goose-helmet-Cvc7KjUg.bin +0 -0
- package/apps/web/dist/assets/goose-helmet-livery-CZD9u0XS.webp +0 -0
- package/apps/web/dist/assets/index-C4aHMcg9.css +1 -0
- package/apps/web/dist/assets/index-C61HAltK.js +14 -0
- package/apps/web/dist/assets/oss-credits-qGhcbqiM.js +1 -0
- package/apps/web/dist/assets/{phone-BWOB67tX.js → phone-BjmhIKuT.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-CEQlVs1O.js → settings-menu-Dpkp3f56.js} +1 -1
- package/apps/web/dist/assets/ui-DMmT-cSO.js +137 -0
- package/apps/web/dist/assets/{upload-pBKn2EFp.js → upload-JX9BcMTq.js} +1 -1
- package/apps/web/dist/assets/{lock-DAvRJY4B.js → wrench-BSxnPs2_.js} +1 -1
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/install.sh +2 -2
- package/apps/web/dist/mcp/flow.html +142 -23
- package/node_modules/@engenty/wizards/package.json +1 -4
- package/node_modules/@engenty-wizards/shared/dist/claude-plugin.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/claude-plugin.js +10 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +15 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +2 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +4 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +28 -6
- package/plugin/README.md +58 -0
- package/plugin/hooks/engenty.ts +140 -0
- package/plugin/hooks/hooks.json +1 -0
- package/plugin/hooks/pane.test.tsx +177 -0
- package/plugin/hooks/register.tsx +396 -0
- package/plugin/types/index.d.ts +44 -0
- package/apps/web/dist/assets/AboutDialog-BP8-Ugxf.js +0 -1
- package/apps/web/dist/assets/ChangelogDialog-CD6CQNW9.js +0 -1
- package/apps/web/dist/assets/ChatView-4T0QarEr.js +0 -10
- package/apps/web/dist/assets/EditorPage-CWdV7Xdn.js +0 -3
- package/apps/web/dist/assets/NewWizardPage-_bFYjlgh.js +0 -7
- package/apps/web/dist/assets/OnboardingPage-DObQeCDg.js +0 -1
- package/apps/web/dist/assets/ProjectPage-DX-oNLWy.js +0 -1
- package/apps/web/dist/assets/PublicRunner-Bn9rVNEb.js +0 -1
- package/apps/web/dist/assets/RunDrawer-CVMTLse_.js +0 -4
- package/apps/web/dist/assets/RunnerView-Cjhuu9ua.js +0 -1
- package/apps/web/dist/assets/SettingsPage-DTzuiSAj.js +0 -1
- package/apps/web/dist/assets/SetupPage-D9jlhgiP.js +0 -1
- package/apps/web/dist/assets/changelog-CJNkd1_L.js +0 -1
- package/apps/web/dist/assets/engenty-eXZ-EAwd.js +0 -1
- package/apps/web/dist/assets/index-AfUa8xKD.css +0 -1
- package/apps/web/dist/assets/index-B5F1_qA_.js +0 -14
- package/apps/web/dist/assets/oss-credits-X2JPiMOR.js +0 -1
- package/apps/web/dist/assets/ui-BmR_4eL2.js +0 -137
- package/node_modules/@engenty/wizards/dist/openui/Wizard.d.ts +0 -20
- package/node_modules/@engenty/wizards/dist/openui/Wizard.js +0 -62
- package/node_modules/@engenty/wizards/dist/openui/index.d.ts +0 -9
- package/node_modules/@engenty/wizards/dist/openui/index.js +0 -9
- package/node_modules/@engenty/wizards/dist/openui/message.d.ts +0 -11
- package/node_modules/@engenty/wizards/dist/openui/message.js +0 -69
- package/node_modules/@engenty/wizards/dist/openui/run.d.ts +0 -8
- package/node_modules/@engenty/wizards/dist/openui/run.js +0 -137
- package/node_modules/@engenty/wizards/dist/openui/server.d.ts +0 -37
- package/node_modules/@engenty/wizards/dist/openui/server.js +0 -77
- package/plugin/.mcp.json +0 -8
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
"exports": {
|
|
7
7
|
"./plugin": "./dist/plugin/index.js",
|
|
8
8
|
"./plugin/studio": "./dist/plugin/studio.js",
|
|
9
|
-
"./plugin/thread": "./dist/plugin/thread.js"
|
|
10
|
-
"./openui": "./dist/openui/index.js",
|
|
11
|
-
"./openui/server": "./dist/openui/server.js",
|
|
12
|
-
"./openui/styles.css": "./src/openui/styles.css"
|
|
9
|
+
"./plugin/thread": "./dist/plugin/thread.js"
|
|
13
10
|
}
|
|
14
11
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code plugin `wizards` comes from two marketplaces: `engenty`, the GitHub repository
|
|
3
|
+
* engenty/wizards, and one each deployment serves with its own server bundled. The served one is
|
|
4
|
+
* named after the deployment's host, so a person can add both: `wizards@engenty` and
|
|
5
|
+
* `wizards@wizards-example-com`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const marketplaceNameOf: (appUrl: string) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code plugin `wizards` comes from two marketplaces: `engenty`, the GitHub repository
|
|
3
|
+
* engenty/wizards, and one each deployment serves with its own server bundled. The served one is
|
|
4
|
+
* named after the deployment's host, so a person can add both: `wizards@engenty` and
|
|
5
|
+
* `wizards@wizards-example-com`.
|
|
6
|
+
*/
|
|
7
|
+
export const marketplaceNameOf = (appUrl) => new URL(appUrl).host
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "");
|
|
@@ -805,6 +805,7 @@ export declare const filmStepSchema: z.ZodObject<{
|
|
|
805
805
|
"1:1": "1:1";
|
|
806
806
|
"9:16": "9:16";
|
|
807
807
|
}>>;
|
|
808
|
+
from: z.ZodOptional<z.ZodString>;
|
|
808
809
|
seconds: z.ZodOptional<z.ZodNumber>;
|
|
809
810
|
model: z.ZodOptional<z.ZodEnum<{
|
|
810
811
|
classifier: "classifier";
|
|
@@ -1653,6 +1654,7 @@ export declare const stepSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1653
1654
|
"1:1": "1:1";
|
|
1654
1655
|
"9:16": "9:16";
|
|
1655
1656
|
}>>;
|
|
1657
|
+
from: z.ZodOptional<z.ZodString>;
|
|
1656
1658
|
seconds: z.ZodOptional<z.ZodNumber>;
|
|
1657
1659
|
model: z.ZodOptional<z.ZodEnum<{
|
|
1658
1660
|
classifier: "classifier";
|
|
@@ -2653,6 +2655,7 @@ export declare const wizardSchema: z.ZodObject<{
|
|
|
2653
2655
|
"1:1": "1:1";
|
|
2654
2656
|
"9:16": "9:16";
|
|
2655
2657
|
}>>;
|
|
2658
|
+
from: z.ZodOptional<z.ZodString>;
|
|
2656
2659
|
seconds: z.ZodOptional<z.ZodNumber>;
|
|
2657
2660
|
model: z.ZodOptional<z.ZodEnum<{
|
|
2658
2661
|
classifier: "classifier";
|
|
@@ -663,6 +663,12 @@ export const filmStepSchema = z.object({
|
|
|
663
663
|
*/
|
|
664
664
|
inputs: z.record(z.string(), z.string()).default({}),
|
|
665
665
|
format: z.enum(FILM_FORMATS).default("16:9"),
|
|
666
|
+
/**
|
|
667
|
+
* "steps.<id>" of an earlier film step: this film starts from that one's finished code and
|
|
668
|
+
* lays it out for its own format — scenes, timing and sound stay. Made from scratch when that
|
|
669
|
+
* step did not run.
|
|
670
|
+
*/
|
|
671
|
+
from: z.string().optional(),
|
|
666
672
|
/** About how long the film runs, in seconds. */
|
|
667
673
|
seconds: z.number().int().min(3).max(600).optional(),
|
|
668
674
|
/** The kind of model; default `standard` — the kit carries the craft. */
|
|
@@ -1427,6 +1433,15 @@ export function validateWizard(def, files) {
|
|
|
1427
1433
|
});
|
|
1428
1434
|
}
|
|
1429
1435
|
}
|
|
1436
|
+
if (step.from) {
|
|
1437
|
+
const id = dataRef(step.from).replace(/^steps\./, "");
|
|
1438
|
+
if (!seenSteps.has(id) || def.steps.find((s) => s.id === id)?.type !== "film") {
|
|
1439
|
+
issues.push({
|
|
1440
|
+
stepId: step.id,
|
|
1441
|
+
message: `Film from "${step.from}" is not an earlier film step ("steps.<id>").`,
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1430
1445
|
if (step.kit && !step.kit.endsWith(".zip")) {
|
|
1431
1446
|
issues.push({ stepId: step.id, message: `Film kit "${step.kit}" is a .zip.` });
|
|
1432
1447
|
}
|
|
@@ -243,6 +243,8 @@ declare const NOTES_EN: {
|
|
|
243
243
|
readonly wizardLimit: "This wizard has reached its limit for today. Try again tomorrow.";
|
|
244
244
|
readonly confirmHuman: "Confirm that you’re a person.";
|
|
245
245
|
readonly tooManyRuns: "You’ve started a lot of runs just now. Try again in an hour.";
|
|
246
|
+
readonly clientRefused: "This app may not start runs here.";
|
|
247
|
+
readonly clientLimit: "This app has started a lot of runs just now. Try again later.";
|
|
246
248
|
readonly runOver: "The run is over.";
|
|
247
249
|
readonly runBusy: "The wizard is working right now.";
|
|
248
250
|
readonly pageOutdated: "This page is out of date.";
|
|
@@ -76,6 +76,8 @@ const NOTES_EN = {
|
|
|
76
76
|
wizardLimit: "This wizard has reached its limit for today. Try again tomorrow.",
|
|
77
77
|
confirmHuman: "Confirm that you’re a person.",
|
|
78
78
|
tooManyRuns: "You’ve started a lot of runs just now. Try again in an hour.",
|
|
79
|
+
clientRefused: "This app may not start runs here.",
|
|
80
|
+
clientLimit: "This app has started a lot of runs just now. Try again later.",
|
|
79
81
|
runOver: "The run is over.",
|
|
80
82
|
runBusy: "The wizard is working right now.",
|
|
81
83
|
pageOutdated: "This page is out of date.",
|
|
@@ -367,6 +369,8 @@ const NOTES_DE = {
|
|
|
367
369
|
wizardLimit: "Für heute ist das Limit dieses Wizards erreicht. Versuch es morgen wieder.",
|
|
368
370
|
confirmHuman: "Bestätige kurz, dass du ein Mensch bist.",
|
|
369
371
|
tooManyRuns: "Du hast gerade viele Läufe gestartet. Versuch es in einer Stunde wieder.",
|
|
372
|
+
clientRefused: "Diese App darf hier keine Läufe starten.",
|
|
373
|
+
clientLimit: "Diese App hat gerade viele Läufe gestartet. Versuch es später wieder.",
|
|
370
374
|
runOver: "Der Lauf ist beendet.",
|
|
371
375
|
runBusy: "Der Wizard arbeitet gerade.",
|
|
372
376
|
pageOutdated: "Diese Seite ist nicht mehr aktuell.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@engenty/wizards-runtime",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Make a wish. Get your wizard. It guides you step by step. Let AI do the work. Runs on your machine, on the AI subscription you already have. The runtime, which `npx wizards` installs.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"homepage": "https://engenty.ai",
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "wizards",
|
|
3
3
|
"displayName": "engenty wizards",
|
|
4
|
-
"version": "
|
|
5
|
-
"description": "Build page-by-page AI wizards that people open on a shared link — designed, tested and published from Claude Code —
|
|
6
|
-
"author": {
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
"version": "0.7.9",
|
|
5
|
+
"description": "Build page-by-page AI wizards that people open on a shared link — designed, tested and published from Claude Code — run your published ones in the chat, and keep them in a pane beside it.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "engenty"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://engenty.ai",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"wizards",
|
|
12
|
+
"forms",
|
|
13
|
+
"ai-workflows",
|
|
14
|
+
"mcp"
|
|
15
|
+
],
|
|
16
|
+
"types": "./types/index.d.ts",
|
|
17
|
+
"userConfig": {
|
|
18
|
+
"server": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"title": "Wizards server",
|
|
21
|
+
"description": "The MCP server of the wizards runtime, as /mcp lists it. The plugin's own (engenty-wizards) when left as is.",
|
|
22
|
+
"default": "engenty-wizards"
|
|
23
|
+
},
|
|
24
|
+
"browser": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"title": "Browser pane",
|
|
27
|
+
"description": "The MCP server of the app's browser pane, whose navigate tool opens a wizard beside the chat.",
|
|
28
|
+
"default": "Claude_Browser"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
9
31
|
}
|
package/plugin/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# wizards, the Claude Code plugin
|
|
2
|
+
|
|
3
|
+
The plugin a running engenty wizards serves at `<APP_URL>/api/claude-plugin/marketplace.json`,
|
|
4
|
+
with its own address filled in. It holds:
|
|
5
|
+
|
|
6
|
+
- **`/wizard <what it should do>`** builds a wizard from a description, with the `build-wizard`
|
|
7
|
+
skill: authoring guide, closest starter, issues fixed, one test run, the studio link.
|
|
8
|
+
- **`use-wizard`**, the skill that runs one of your published wizards here in the chat.
|
|
9
|
+
- **`/wizards`**, a pane beside the chat with your wizards: each one's engenty, name and
|
|
10
|
+
description. **Run** opens the wizard's own page in the app's browser pane, beside the chat,
|
|
11
|
+
where you fill it as any person would. **Edit** puts a change request into the chat for you to
|
|
12
|
+
finish. The pane opens by itself when the plugin loads; `/wizards close` closes it. A run the
|
|
13
|
+
model drives through the server's tools opens its page beside the chat once it waits for you.
|
|
14
|
+
|
|
15
|
+
The pane is a mod: a hooks module in `hooks/register.tsx` drawing with the engine's elements,
|
|
16
|
+
its state declared in `types/index.d.ts`. `claude plugin validate plugin` checks it,
|
|
17
|
+
`claude plugin test plugin` runs `hooks/pane.test.tsx` against the engine.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
From GitHub, for the `engenty-wizards` MCP server you already have (the local install writes it
|
|
22
|
+
into your AI apps; `claude mcp add --transport http engenty-wizards <APP_URL>/api/mcp` adds a
|
|
23
|
+
hosted one):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
claude plugin marketplace add engenty/wizards
|
|
27
|
+
claude plugin install wizards@engenty
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From a running engenty wizards with a public https address, which bundles its own server:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
claude plugin marketplace add <APP_URL>/api/claude-plugin/marketplace.json
|
|
34
|
+
claude plugin install wizards@<host-with-dashes> # wizards.example.com → wizards-example-com
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`claude plugin update wizards` brings the next version.
|
|
38
|
+
|
|
39
|
+
## Permissions in auto mode
|
|
40
|
+
|
|
41
|
+
The pane calls the server itself, from a button press, not from a model request. In auto mode
|
|
42
|
+
such a call has no classifier verdict and is refused unless an allow rule names the tool. Put
|
|
43
|
+
these in `permissions.allow` of `.claude/settings.local.json` or your user settings:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
["mcp__plugin_wizards_engenty-wizards", "mcp__Claude_Browser__navigate"]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A server you configured yourself under the name `engenty-wizards` is `mcp__engenty-wizards`
|
|
50
|
+
instead. The browser pane's `navigate` may still ask you; the pane then shows a plain link to
|
|
51
|
+
the same page.
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
| Option | Default | What it names |
|
|
56
|
+
| --------- | ----------------- | ---------------------------------------------------------- |
|
|
57
|
+
| `server` | `engenty-wizards` | the wizards runtime's MCP server, as `/mcp` lists it |
|
|
58
|
+
| `browser` | `Claude_Browser` | the app's browser pane, whose `navigate` opens a page |
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat engenties of the studio (apps/web/src/engenty/engenty.tsx) at rest, as one SVG
|
|
3
|
+
* string per kind: the same bodies, the one eye, no animation. The fills are the studio's
|
|
4
|
+
* defaults (ENGENTY_FILL in apps/web/src/engenty/colors.ts).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const FILL = {
|
|
8
|
+
cobalt: "oklch(60% 0.23 262)",
|
|
9
|
+
amber: "oklch(82% 0.17 78)",
|
|
10
|
+
moss: "oklch(72% 0.22 145)",
|
|
11
|
+
rose: "oklch(64% 0.25 18)",
|
|
12
|
+
ember: "oklch(70% 0.21 42)",
|
|
13
|
+
teal: "oklch(74% 0.16 192)",
|
|
14
|
+
violet: "oklch(60% 0.25 300)",
|
|
15
|
+
magenta: "oklch(65% 0.28 345)",
|
|
16
|
+
citron: "oklch(89% 0.2 118)",
|
|
17
|
+
slate: "oklch(66% 0.13 235)",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
type Eye = { ex: number; ey: number; r: number; ink: string };
|
|
21
|
+
|
|
22
|
+
type Kind = {
|
|
23
|
+
fill: string;
|
|
24
|
+
body: string;
|
|
25
|
+
eye: Eye;
|
|
26
|
+
shadow: { cy: number; rx: number };
|
|
27
|
+
/** Marks drawn before the body (a sprout's leaves). */
|
|
28
|
+
under?: string;
|
|
29
|
+
/** Marks drawn over the body, before the eye. */
|
|
30
|
+
over?: string;
|
|
31
|
+
/** Marks drawn after the eye (a mouth, a dot). */
|
|
32
|
+
after?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const stroke = (d: string, color: string, width: number, opacity: number) =>
|
|
36
|
+
`<path d="${d}" fill="none" stroke="${color}" stroke-width="${width}" stroke-linecap="round" opacity="${opacity}"/>`;
|
|
37
|
+
|
|
38
|
+
const KINDS: Record<string, Kind> = {
|
|
39
|
+
round: {
|
|
40
|
+
fill: FILL.cobalt,
|
|
41
|
+
body: "M60 22 Q96 26 96 62 Q96 96 60 100 Q24 96 24 62 Q24 26 60 22 Z",
|
|
42
|
+
over: `<circle cx="97" cy="34" r="6" fill="${FILL.cobalt}" opacity="0.5"/><circle cx="106" cy="24" r="3.5" fill="${FILL.cobalt}" opacity="0.35"/>`,
|
|
43
|
+
eye: { ex: 68, ey: 58, r: 8, ink: "oklch(18% 0.08 260)" },
|
|
44
|
+
shadow: { cy: 108, rx: 26 },
|
|
45
|
+
},
|
|
46
|
+
drop: {
|
|
47
|
+
fill: FILL.amber,
|
|
48
|
+
body: "M60 16 Q90 34 90 66 Q90 96 60 98 Q30 96 30 66 Q30 34 60 16 Z",
|
|
49
|
+
eye: { ex: 60, ey: 56, r: 8, ink: "oklch(30% 0.10 60)" },
|
|
50
|
+
after: stroke("M48 76 Q54 72 60 76 Q66 80 72 76", "oklch(30% 0.10 55)", 2.6, 1),
|
|
51
|
+
shadow: { cy: 106, rx: 24 },
|
|
52
|
+
},
|
|
53
|
+
dome: {
|
|
54
|
+
fill: FILL.moss,
|
|
55
|
+
body: "M60 20 Q92 30 92 70 Q92 96 60 96 Q28 96 28 70 Q28 30 60 20 Z",
|
|
56
|
+
over: `${stroke("M46 90 L46 80 M60 92 L60 82 M74 90 L74 80", "oklch(28% 0.10 155)", 2.4, 0.5)}<circle cx="60" cy="6" r="3" fill="${FILL.moss}"/>`,
|
|
57
|
+
eye: { ex: 60, ey: 54, r: 8.5, ink: "oklch(16% 0.08 160)" },
|
|
58
|
+
shadow: { cy: 104, rx: 27 },
|
|
59
|
+
},
|
|
60
|
+
flame: {
|
|
61
|
+
fill: FILL.rose,
|
|
62
|
+
body: "M60 14 Q84 44 80 76 Q78 98 60 98 Q42 98 40 76 Q36 44 60 14 Z",
|
|
63
|
+
over: stroke("M40 20 Q60 6 80 20", FILL.rose, 3, 0.55),
|
|
64
|
+
eye: { ex: 60, ey: 62, r: 8, ink: "oklch(22% 0.10 18)" },
|
|
65
|
+
after: '<circle cx="60" cy="82" r="2.6" fill="oklch(22% 0.10 18)"/>',
|
|
66
|
+
shadow: { cy: 106, rx: 22 },
|
|
67
|
+
},
|
|
68
|
+
oval: {
|
|
69
|
+
fill: FILL.ember,
|
|
70
|
+
body: "M60 34 Q100 38 100 64 Q100 92 60 94 Q20 92 20 64 Q20 38 60 34 Z",
|
|
71
|
+
over:
|
|
72
|
+
stroke("M28 54 Q60 48 92 54", "oklch(40% 0.16 30)", 2.2, 0.45) +
|
|
73
|
+
stroke("M26 70 Q60 64 94 70", "oklch(40% 0.16 30)", 2.2, 0.45),
|
|
74
|
+
eye: { ex: 66, ey: 58, r: 8, ink: "oklch(24% 0.10 30)" },
|
|
75
|
+
shadow: { cy: 102, rx: 32 },
|
|
76
|
+
},
|
|
77
|
+
bean: {
|
|
78
|
+
fill: FILL.teal,
|
|
79
|
+
body: "M48 96 Q24 92 26 68 Q28 44 52 38 Q78 32 88 50 Q98 66 84 84 Q70 100 48 96 Z",
|
|
80
|
+
over: `<circle cx="32" cy="94" r="4" fill="${FILL.teal}" opacity="0.4"/>`,
|
|
81
|
+
eye: { ex: 70, ey: 58, r: 8, ink: "oklch(18% 0.08 200)" },
|
|
82
|
+
shadow: { cy: 102, rx: 26 },
|
|
83
|
+
},
|
|
84
|
+
pebble: {
|
|
85
|
+
fill: FILL.slate,
|
|
86
|
+
body: "M60 96 Q20 96 18 78 Q16 60 46 56 Q78 52 98 62 Q110 72 104 86 Q98 96 60 96 Z",
|
|
87
|
+
over: '<g fill="oklch(30% 0.05 244)" opacity="0.32"><circle cx="42" cy="76" r="3"/><circle cx="56" cy="86" r="2.2"/><circle cx="86" cy="80" r="2.6"/></g>',
|
|
88
|
+
eye: { ex: 68, ey: 72, r: 7.5, ink: "oklch(20% 0.06 244)" },
|
|
89
|
+
shadow: { cy: 102, rx: 34 },
|
|
90
|
+
},
|
|
91
|
+
sprout: {
|
|
92
|
+
fill: FILL.citron,
|
|
93
|
+
under: `<g fill="${FILL.citron}"><ellipse cx="48" cy="34" rx="6" ry="10"/><ellipse cx="72" cy="32" rx="5.5" ry="9"/></g>`,
|
|
94
|
+
body: "M60 100 Q28 98 28 72 Q28 46 60 44 Q92 46 92 72 Q92 98 60 100 Z",
|
|
95
|
+
eye: { ex: 60, ey: 68, r: 8.5, ink: "oklch(24% 0.09 108)" },
|
|
96
|
+
shadow: { cy: 106, rx: 26 },
|
|
97
|
+
},
|
|
98
|
+
tower: {
|
|
99
|
+
fill: FILL.violet,
|
|
100
|
+
body: "M60 102 Q36 100 38 78 Q40 58 42 44 Q44 26 60 24 Q76 26 78 44 Q80 58 82 78 Q84 100 60 102 Z",
|
|
101
|
+
over:
|
|
102
|
+
stroke("M42 74 Q60 70 78 74", "oklch(28% 0.10 308)", 2.2, 0.4) +
|
|
103
|
+
stroke("M40 88 Q60 84 80 88", "oklch(28% 0.10 308)", 2.2, 0.4),
|
|
104
|
+
eye: { ex: 60, ey: 52, r: 8, ink: "oklch(20% 0.09 308)" },
|
|
105
|
+
shadow: { cy: 108, rx: 20 },
|
|
106
|
+
},
|
|
107
|
+
wedge: {
|
|
108
|
+
fill: FILL.magenta,
|
|
109
|
+
body: "M60 30 Q74 40 86 70 Q96 92 78 98 Q60 102 42 98 Q24 92 34 70 Q46 40 60 30 Z",
|
|
110
|
+
over: stroke("M52 22 Q60 14 68 22", FILL.magenta, 3, 0.5),
|
|
111
|
+
eye: { ex: 60, ey: 70, r: 8, ink: "oklch(22% 0.10 345)" },
|
|
112
|
+
shadow: { cy: 106, rx: 28 },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const kindOf = (kind: string | undefined): Kind => KINDS[kind ?? "round"] ?? KINDS.round!;
|
|
117
|
+
|
|
118
|
+
/** The one eye: white, a pupil, a glint. */
|
|
119
|
+
const eye = ({ ex, ey, r, ink }: Eye) =>
|
|
120
|
+
`<ellipse cx="${ex}" cy="${ey}" rx="${r}" ry="${r * 1.15}" fill="#fff"/>` +
|
|
121
|
+
`<circle cx="${ex}" cy="${ey + 1}" r="${r * 0.48}" fill="${ink}"/>` +
|
|
122
|
+
`<circle cx="${ex - r * 0.25}" cy="${ey - r * 0.3}" r="${r * 0.18}" fill="#fff"/>`;
|
|
123
|
+
|
|
124
|
+
/** The engenty of a kind at rest, `size` pixels square. */
|
|
125
|
+
export function engentySvg(kind: string | undefined, size: number): string {
|
|
126
|
+
const k = kindOf(kind);
|
|
127
|
+
return (
|
|
128
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="${size}" height="${size}" overflow="visible">` +
|
|
129
|
+
`<ellipse cx="60" cy="${k.shadow.cy}" rx="${k.shadow.rx}" ry="4.5" fill="#000" opacity="0.14"/>` +
|
|
130
|
+
(k.under ?? "") +
|
|
131
|
+
`<path d="${k.body}" fill="${k.fill}"/>` +
|
|
132
|
+
(k.over ?? "") +
|
|
133
|
+
eye(k.eye) +
|
|
134
|
+
(k.after ?? "") +
|
|
135
|
+
"</svg>"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The body colour of a kind, for a surface that draws no SVG. */
|
|
140
|
+
export const engentyColor = (kind: string | undefined) => kindOf(kind).fill;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { McpToolResult } from "claude-code";
|
|
2
|
+
import { expect, test } from "claude-code/testing";
|
|
3
|
+
|
|
4
|
+
const PANE_PROPS = {
|
|
5
|
+
title: "engenty.wizards",
|
|
6
|
+
isFocused: true,
|
|
7
|
+
bodyColumns: 80,
|
|
8
|
+
placement: "dock",
|
|
9
|
+
scroll: { offset: 0, bodyRows: 40 },
|
|
10
|
+
view: {},
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
const answer = (value: unknown): McpToolResult => ({
|
|
14
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
15
|
+
isError: false,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const BASE = "http://localhost:24368";
|
|
19
|
+
|
|
20
|
+
const wizard = (id: string, title: string, published: boolean, shared = published) => ({
|
|
21
|
+
wizardId: id,
|
|
22
|
+
draft: {
|
|
23
|
+
title,
|
|
24
|
+
description: `${title} macht etwas.`,
|
|
25
|
+
avatar: "round",
|
|
26
|
+
steps: [
|
|
27
|
+
{ id: "first", type: "page", title: "Thema" },
|
|
28
|
+
{ id: "write", type: "result", title: "Fertig" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
shareEnabled: shared,
|
|
32
|
+
shareUrl: `${BASE}/w/t-${id}`,
|
|
33
|
+
studioUrl: `${BASE}/studio/edit/${id}`,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const waiting = {
|
|
37
|
+
runId: "r1",
|
|
38
|
+
wizardId: "w2",
|
|
39
|
+
mode: "live",
|
|
40
|
+
status: "waiting_input",
|
|
41
|
+
waitingFor: { page: "first", title: "Thema", fields: [] },
|
|
42
|
+
browserUrl: `${BASE}/w/t-w2/r1`,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
test("lists the wizards; Run opens the page beside the chat, Edit asks the chat", async ($, on) => {
|
|
46
|
+
const calls: string[] = [];
|
|
47
|
+
const opened: string[] = [];
|
|
48
|
+
const filled: string[] = [];
|
|
49
|
+
on("prompt.fill", async (_, e) => {
|
|
50
|
+
filled.push(e.text);
|
|
51
|
+
return { isFilled: true };
|
|
52
|
+
});
|
|
53
|
+
on("mcp.call", { server: "engenty-wizards" }, async (_, e) => {
|
|
54
|
+
calls.push(e.tool);
|
|
55
|
+
if (e.tool === "list_wizards") {
|
|
56
|
+
return {
|
|
57
|
+
value: answer([
|
|
58
|
+
{ id: "w1", title: "Brot", published: true, studioUrl: `${BASE}/studio/edit/w1` },
|
|
59
|
+
{ id: "w2", title: "Ohne Link", published: true, studioUrl: `${BASE}/studio/edit/w2` },
|
|
60
|
+
{ id: "w0", title: "Entwurf", published: false, studioUrl: `${BASE}/studio/edit/w0` },
|
|
61
|
+
]),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (e.tool === "get_wizard") {
|
|
65
|
+
const id = String(e.args.wizardId);
|
|
66
|
+
if (id === "w1") return { value: answer(wizard("w1", "Brot", true)) };
|
|
67
|
+
if (id === "w2") return { value: answer(wizard("w2", "Ohne Link", true, false)) };
|
|
68
|
+
return { value: answer(wizard("w0", "Entwurf", false)) };
|
|
69
|
+
}
|
|
70
|
+
if (e.tool === "run_wizard") return { value: answer(waiting) };
|
|
71
|
+
return { deny: `unexpected ${e.tool}` };
|
|
72
|
+
});
|
|
73
|
+
on("mcp.call", { server: "Claude_Browser" }, async (_, e) => {
|
|
74
|
+
opened.push(String(e.args.url));
|
|
75
|
+
return { value: { content: [], isError: false } };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
for (const surface of ["terminal", "desktop"] as const) {
|
|
79
|
+
calls.length = 0;
|
|
80
|
+
opened.length = 0;
|
|
81
|
+
const ui = await $.ui.mount({
|
|
82
|
+
plugin: "wizards",
|
|
83
|
+
surface,
|
|
84
|
+
component: "Pane",
|
|
85
|
+
requestId: "wizards",
|
|
86
|
+
props: PANE_PROPS,
|
|
87
|
+
});
|
|
88
|
+
await ui.press({ key: "Reload" });
|
|
89
|
+
// Details are fetched once per wizard; the second surface finds them in the session's state.
|
|
90
|
+
expect(calls).toEqual(
|
|
91
|
+
surface === "terminal"
|
|
92
|
+
? ["list_wizards", "get_wizard", "get_wizard", "get_wizard"]
|
|
93
|
+
: ["list_wizards"],
|
|
94
|
+
);
|
|
95
|
+
expect(await ui.find({ type: "Text", text: "Brot" })).toBeDefined();
|
|
96
|
+
expect(await ui.find({ type: "Text", text: /Entwurf · draft/ })).toBeDefined();
|
|
97
|
+
expect(await ui.find({ key: "run:w1" })).toBeDefined();
|
|
98
|
+
expect(await ui.find({ key: "run:w0" })).toBeUndefined();
|
|
99
|
+
expect(await ui.find({ key: "edit:w0" })).toBeDefined();
|
|
100
|
+
expect(await ui.find({ type: "Text", text: "localhost:24368" })).toBeDefined();
|
|
101
|
+
|
|
102
|
+
// A wizard with a page of its own opens there.
|
|
103
|
+
await ui.press({ key: "run:w1" });
|
|
104
|
+
expect(opened).toEqual([`${BASE}/w/t-w1`]);
|
|
105
|
+
// One without is started here, and its run's page opens.
|
|
106
|
+
await ui.press({ key: "run:w2" });
|
|
107
|
+
expect(calls.at(-1)).toBe("run_wizard");
|
|
108
|
+
expect(opened.at(-1)).toBe(`${BASE}/w/t-w2/r1`);
|
|
109
|
+
|
|
110
|
+
await ui.press({ key: "edit:w0" });
|
|
111
|
+
expect(filled.at(-1)).toBe('For my engenty wizard "Entwurf" I need the following changes: ');
|
|
112
|
+
await ui.press({ key: "Studio" });
|
|
113
|
+
expect(opened.at(-1)).toBe(`${BASE}/studio`);
|
|
114
|
+
await ui.unmount();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a run the model drives opens beside the chat once", async ($, on) => {
|
|
119
|
+
const opened: string[] = [];
|
|
120
|
+
on("mcp.call", { server: "Claude_Browser" }, async (_, e) => {
|
|
121
|
+
opened.push(String(e.args.url));
|
|
122
|
+
return { value: { content: [], isError: false } };
|
|
123
|
+
});
|
|
124
|
+
on("mcp.call", { server: "engenty-wizards" }, () => ({ deny: "not in this test" }));
|
|
125
|
+
on("tool.call", { tool: "mcp__engenty-wizards__get_run" }, async () => ({
|
|
126
|
+
result: { content: [{ type: "text", text: JSON.stringify(waiting) }] },
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const ui = await $.ui.mount({
|
|
130
|
+
plugin: "wizards",
|
|
131
|
+
surface: "desktop",
|
|
132
|
+
component: "Pane",
|
|
133
|
+
requestId: "wizards",
|
|
134
|
+
props: PANE_PROPS,
|
|
135
|
+
});
|
|
136
|
+
await $.tool.call({ tool: "mcp__engenty-wizards__get_run", tool_use_id: "t1", runId: "r1" });
|
|
137
|
+
await $.tool.call({ tool: "mcp__engenty-wizards__get_run", tool_use_id: "t2", runId: "r1" });
|
|
138
|
+
expect(opened).toEqual([`${BASE}/w/t-w2/r1`]);
|
|
139
|
+
await ui.unmount();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("offers a plain link when the browser pane refuses", async ($, on) => {
|
|
143
|
+
on("mcp.call", { server: "engenty-wizards" }, async (_, e) => {
|
|
144
|
+
if (e.tool === "list_wizards") {
|
|
145
|
+
return { value: answer([{ id: "w1", title: "Brot", published: true }]) };
|
|
146
|
+
}
|
|
147
|
+
if (e.tool === "get_wizard") return { value: answer(wizard("w1", "Brot", true)) };
|
|
148
|
+
return { deny: `unexpected ${e.tool}` };
|
|
149
|
+
});
|
|
150
|
+
on("mcp.call", { server: "Claude_Browser" }, () => ({ deny: "no verdict for navigate" }));
|
|
151
|
+
const ui = await $.ui.mount({
|
|
152
|
+
plugin: "wizards",
|
|
153
|
+
surface: "desktop",
|
|
154
|
+
component: "Pane",
|
|
155
|
+
requestId: "wizards",
|
|
156
|
+
props: PANE_PROPS,
|
|
157
|
+
});
|
|
158
|
+
await ui.press({ key: "Reload" });
|
|
159
|
+
await ui.press({ key: "run:w1" });
|
|
160
|
+
expect(await ui.find({ type: "Text", text: /did not open/ })).toBeDefined();
|
|
161
|
+
expect((await ui.find({ type: "Link" }))?.props.href).toBe(`${BASE}/w/t-w1`);
|
|
162
|
+
await ui.unmount();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("says why when the runtime cannot be reached", async ($, on) => {
|
|
166
|
+
on("mcp.call", { server: "engenty-wizards" }, () => ({ deny: "ECONNREFUSED" }));
|
|
167
|
+
const ui = await $.ui.mount({
|
|
168
|
+
plugin: "wizards",
|
|
169
|
+
surface: "terminal",
|
|
170
|
+
component: "Pane",
|
|
171
|
+
requestId: "wizards",
|
|
172
|
+
props: PANE_PROPS,
|
|
173
|
+
});
|
|
174
|
+
await ui.press({ key: "Reload" });
|
|
175
|
+
expect(await ui.find({ type: "Text", text: /ECONNREFUSED/ })).toBeDefined();
|
|
176
|
+
await ui.unmount();
|
|
177
|
+
});
|