wizards 0.3.8 → 0.4.1
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 +28 -14
- package/apps/runtime/dist/agents/studio-assistant.js +67 -20
- package/apps/runtime/dist/auth/account.js +36 -4
- package/apps/runtime/dist/auth/device.js +101 -0
- package/apps/runtime/dist/auth/index.js +4 -4
- package/apps/runtime/dist/cli/account.js +289 -0
- package/apps/runtime/dist/cli/install-env.js +33 -0
- package/apps/runtime/dist/cli/main.js +17 -3
- package/apps/runtime/dist/cli/menu.js +19 -5
- package/apps/runtime/dist/cli/setup.js +9 -16
- package/apps/runtime/dist/cli/status.js +4 -1
- package/apps/runtime/dist/cli/ui.js +14 -0
- package/apps/runtime/dist/docs/dev/plugins/reference.md +7 -4
- package/apps/runtime/dist/docs/dev/plugins/server.md +3 -1
- package/apps/runtime/dist/docs/dev/plugins/space.md +19 -1
- package/apps/runtime/dist/docs/dev/plugins/studio.md +43 -6
- package/apps/runtime/dist/docs/dev/plugins/styling.md +8 -1
- package/apps/runtime/dist/docs/user/command-line.md +3 -2
- package/apps/runtime/dist/docs/user/create-account.md +47 -0
- package/apps/runtime/dist/docs/user/first-start.md +9 -1
- package/apps/runtime/dist/docs/user/index.md +16 -12
- package/apps/runtime/dist/docs/user/install.md +3 -1
- package/apps/runtime/dist/docs/user/local-only.md +38 -0
- package/apps/runtime/dist/docs/user/meta.json +3 -1
- package/apps/runtime/dist/docs/user/models.md +3 -2
- package/apps/runtime/dist/docs/user/space.md +4 -4
- package/apps/runtime/dist/engine/steps.js +3 -2
- package/apps/runtime/dist/engine/template.js +14 -10
- package/apps/runtime/dist/film/project.js +15 -3
- package/apps/runtime/dist/manage.js +12 -0
- package/apps/runtime/dist/plugins/loader.js +38 -2
- package/apps/runtime/dist/plugins/pro.js +16 -16
- package/apps/runtime/dist/routes/studio.js +69 -3
- package/apps/runtime/dist/services/brand.js +61 -1
- package/apps/runtime/dist/services/cloud.js +10 -10
- package/apps/runtime/dist/services/onboarding.js +152 -0
- package/apps/runtime/dist/services/projects.js +19 -10
- package/apps/runtime/dist/tools/plugin.js +11 -3
- package/apps/web/dist/assets/AboutDialog-CXEpsI6L.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-DsSEkayM.js → ChangelogDialog-Cc2APUGI.js} +1 -1
- package/apps/web/dist/assets/ChatView-B2r4dW_m.js +10 -0
- package/apps/web/dist/assets/EditorPage-CNAYlSms.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-C_x5Kc2j.js → FlowPreview-DzU1nGDb.js} +1 -1
- package/apps/web/dist/assets/{Harness-CvvtSTZC.js → Harness-ALQ9Csm6.js} +2 -2
- package/apps/web/dist/assets/NewWizardChannels-DMPqHkCF.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-pi053e8N.js +7 -0
- package/apps/web/dist/assets/OnboardingPage-TYNX6Umd.js +1 -0
- package/apps/web/dist/assets/ProjectPage-CvEkx_Ux.js +1 -0
- package/apps/web/dist/assets/PublicRunner-BrivKNHo.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-C1UcqOlS.js → ReadOnly-DQPHmvS0.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-3osugizk.js +4 -0
- package/apps/web/dist/assets/{RunnerView-ii-PP5c-.js → RunnerView-Ci4_WzVv.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-DKntf7-9.js +1 -0
- package/apps/web/dist/assets/SetupPage-DZQxEQyk.js +1 -0
- package/apps/web/dist/assets/SharePage-B9l4KthU.js +1 -0
- package/apps/web/dist/assets/changelog-CxdXZo3V.js +1 -0
- package/apps/web/dist/assets/{circle-alert-D-Q6DK7b.js → circle-alert-DCBV7n5k.js} +1 -1
- package/apps/web/dist/assets/{esm-CTna4iL0.js → esm-CuqXcPS3.js} +1 -1
- package/apps/web/dist/assets/index-CmxVazOf.css +1 -0
- package/apps/web/dist/assets/index-j09nLtEs.js +14 -0
- package/apps/web/dist/assets/{lock-DwGN_KOC.js → lock-DlzTfwxP.js} +1 -1
- package/apps/web/dist/assets/{phone-XIVIe17d.js → phone-BFD815nc.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-JuSRq6NP.js → settings-menu-C7VsRpe9.js} +1 -1
- package/apps/web/dist/assets/ui-DTuixH4y.js +134 -0
- package/apps/web/dist/assets/{upload-CDxTpAqx.js → upload-BEf5-d6X.js} +1 -1
- package/apps/web/dist/index.html +4 -5
- package/apps/web/dist/install.sh +17 -7
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +36 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +44 -1
- package/node_modules/@engenty-wizards/shared/dist/feature-flags.d.ts +18 -0
- package/node_modules/@engenty-wizards/shared/dist/feature-flags.js +18 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +22 -6
- package/node_modules/@engenty-wizards/shared/dist/projects.js +20 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/AboutDialog-ClV4gNiW.js +0 -1
- package/apps/web/dist/assets/ChatView-BdYCnDLk.js +0 -10
- package/apps/web/dist/assets/EditorPage-7PZzkNBT.js +0 -3
- package/apps/web/dist/assets/NewWizardChannels-g1qCd_tQ.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-B3hswj5r.js +0 -7
- package/apps/web/dist/assets/ProjectPage-CFDncRxT.js +0 -1
- package/apps/web/dist/assets/PublicRunner-C9N5r-sT.js +0 -1
- package/apps/web/dist/assets/RunDrawer-B8YzFvgz.js +0 -4
- package/apps/web/dist/assets/SettingsPage-ClEP5Q0t.js +0 -1
- package/apps/web/dist/assets/SetupPage-CDFzo6LT.js +0 -1
- package/apps/web/dist/assets/SharePage-BV2S0g84.js +0 -1
- package/apps/web/dist/assets/changelog-wZI-UaUi.js +0 -1
- package/apps/web/dist/assets/index-DUjJx-uk.css +0 -1
- package/apps/web/dist/assets/index-DnQgOKOQ.js +0 -15
- package/apps/web/dist/assets/logo-C4V2_3Ar.js +0 -1
- package/apps/web/dist/assets/ui-DrjXmHdO.js +0 -133
package/README.md
CHANGED
|
@@ -44,37 +44,50 @@ A wizard in use: it asks on one page, then on the next, and ends with the result
|
|
|
44
44
|
<img src="docs/assets/wizard.png" alt="A wizard in use on a phone, three pictures: which week and how many children, what is on the table, and the finished weekly plan with PDF, PNG and sharing">
|
|
45
45
|
</p>
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## Get started
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Create an account at [engenty.ai](https://engenty.ai): it is in a closed beta, so "Get started"
|
|
50
|
+
takes your e-mail for an invitation, or your invitation code. Signed in, the studio asks who
|
|
51
|
+
your team is, reads your website into your space, takes first documents, and you build your
|
|
52
|
+
first wizard in the browser. Links run day and night on the team's credits.
|
|
53
|
+
|
|
54
|
+
## Install it on your computer
|
|
55
|
+
|
|
56
|
+
The same account on your own machine: it thinks with your Claude, ChatGPT, Gemini or Cursor
|
|
57
|
+
subscription, reads your local files and browses for you, and what you publish keeps running in
|
|
58
|
+
the cloud behind the same links. On macOS or Linux, in a terminal:
|
|
50
59
|
|
|
51
60
|
```bash
|
|
52
61
|
curl -fsSL https://engenty.ai/install.sh | bash
|
|
53
62
|
```
|
|
54
63
|
|
|
55
|
-
It needs nothing but `curl` and asks for no password. It brings its own Node.js (a
|
|
56
|
-
already have is left alone), installs engenty wizards into `~/.engenty/wizards` and
|
|
57
|
-
you through the rest:
|
|
64
|
+
It needs nothing but `curl` and `tar` and asks for no password. It brings its own Node.js (a
|
|
65
|
+
Node you already have is left alone), installs engenty wizards into `~/.engenty/wizards` and
|
|
66
|
+
then walks you through the rest:
|
|
58
67
|
|
|
68
|
+
- **Your engenty account.** A code and the browser for the sign-in; your team's space comes
|
|
69
|
+
down to this computer. "Later" skips it, `wizards account sign-in` does it any time.
|
|
70
|
+
`--local` leaves the account out: nothing leaves the computer, links answer on it only
|
|
71
|
+
([local only](https://engenty.ai/docs/user/local-only)).
|
|
59
72
|
- **Something to think with.** It looks for Codex, Claude Code, Gemini CLI and Cursor Agent and
|
|
60
73
|
offers to install one if there is none — first the one whose app (Claude, ChatGPT, Cursor) is
|
|
61
74
|
on your Mac, as its subscription is most likely yours already. The first page of the studio
|
|
62
|
-
installs them too. Or use an API key or [Ollama](https://ollama.com).
|
|
75
|
+
installs them too. Or use your account's credits, an API key or [Ollama](https://ollama.com).
|
|
63
76
|
- **Google Chrome and ffmpeg** (optional): Chrome for PDF and PNG exports and for steps that use
|
|
64
77
|
a browser, ffmpeg for MP4 videos of animated widgets.
|
|
65
78
|
- **Start at login** (optional): engenty wizards starts in the background when you log in (a
|
|
66
79
|
LaunchAgent on a Mac, a systemd user service on Linux). Links and AI clients then always
|
|
67
80
|
reach it. `wizards autostart on` or `off` switches it later.
|
|
68
81
|
|
|
69
|
-
|
|
82
|
+
It starts nothing. Afterwards:
|
|
70
83
|
|
|
71
84
|
```bash
|
|
72
|
-
wizards
|
|
85
|
+
wizards start
|
|
73
86
|
```
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`wizards --help` lists the rest.
|
|
88
|
+
starts it and opens the studio; it runs while that terminal is open, Ctrl-C stops it.
|
|
89
|
+
`wizards` opens a menu with all of it, `wizards status` says what is installed and what runs,
|
|
90
|
+
`wizards setup` runs the guided setup again, and `wizards --help` lists the rest.
|
|
78
91
|
|
|
79
92
|
**Install it as an app.** In Chrome or Edge the studio installs as an app: its own window, its
|
|
80
93
|
own icon in the Dock or taskbar. The studio offers it in a banner at the top; or click the install
|
|
@@ -99,9 +112,10 @@ machine; a checkout brings its own sandbox, the installed version has none yet.
|
|
|
99
112
|
|
|
100
113
|
## First start
|
|
101
114
|
|
|
102
|
-
The setup asks what engenty should think with. Pick an AI
|
|
103
|
-
computer — engenty uses your existing subscription and signs
|
|
104
|
-
|
|
115
|
+
The setup shows your account at the top and asks what engenty should think with. Pick an AI
|
|
116
|
+
client that is installed on your computer — engenty uses your existing subscription and signs
|
|
117
|
+
it in for you in a terminal right on the page — your account's credits, or enter your own keys.
|
|
118
|
+
It stays until a test call works, then the studio opens.
|
|
105
119
|
|
|
106
120
|
Change it later under Settings.
|
|
107
121
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
2
|
-
import { cleanAssetSections, cleanLogoSections, FACT_TYPES, factKey, gradientCss, PROJECT_FILE_KINDS, PROJECT_LIMITS, parseGradient, RULE_MARKS, textStyleLine, } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { cleanAssetSections, cleanLogoSections, FACT_TYPES, FONT_USES, factKey, gradientCss, PROJECT_FILE_KINDS, PROJECT_LIMITS, parseGradient, RULE_MARKS, textStyleLine, } from "@engenty-wizards/shared/projects";
|
|
3
3
|
import { Agent } from "@mastra/core/agent";
|
|
4
4
|
import { createTool } from "@mastra/core/tools";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { knowsTools } from "../assistant/knows.js";
|
|
8
8
|
import { PRIMER } from "../assistant/primer.js";
|
|
9
|
+
import { currentFeatures } from "../features.js";
|
|
9
10
|
import { attachTools, contextWindowOf, costOf, gatewayTools, textModel, usageOf, } from "../models.js";
|
|
10
11
|
import { ServiceError } from "../services/errors.js";
|
|
11
12
|
import { knowledgeCounts } from "../services/knowledge.js";
|
|
@@ -48,7 +49,7 @@ THE SPACE
|
|
|
48
49
|
- Colours: two to six, the main one first, each with a name that says what it is for ("Primär", "Hintergrund", "Text"). Take them from the stylesheet's variables or its most used colours, never from photos. set_colors replaces the list: pass all colours that should remain. Colours in no section are the standard ones; the first of them is the accent. Colour sections are optional like logo sections: only for a further palette a brand guide or the person names for its own use ("Illustration – drawings and charts"), never for the standard colours. A brand's gradients (set_gradients, as CSS) only where a brand guide or the site's stylesheet has them; patterns and background fills are pictures the person uploads.
|
|
49
50
|
- Logos: save the best candidate (an SVG or a large PNG rather than a favicon); a second one only if it truly differs (for dark ground, a signet). A few images that show the business (header, team, products) may be saved as assets — a handful, never everything.
|
|
50
51
|
- Logo sections and rules are optional, and most spaces need neither. Logos in no section are the standard ones; the first of them is the main logo. Add a section (set_logo_sections) only for further variants that a brand guide, a brand or press page or the person says are used elsewhere: the title names the variant ("Logomark", "Wordmark"), the description says when to use it ("Small spaces: app icons, favicons"). The standard logos never go into a section; one logo, or a light and a dark one whose descriptions already tell them apart, get no sections. Rules (set_rules, for the logos, the colours or the type) are short rows, one rule each, with a sign: do ("Keep clear space of the mark's height on all sides"), dont ("Place it on photos"), important, question (something to check with the person), number (steps in order), or none for a plain note (minimum size, file formats). Only what such a source or the person states; never invent rules and never write general advice a brand guide did not say.
|
|
51
|
-
- Typography: the brand's fonts (set_typography), each with what it is for ("Headings", "Body text") and the weights it uses — from the website's stylesheet (read_website lists its fonts) or a brand guide; system and generic faces are no brand fonts. A type scale (text styles: name, size, weight, line height, letter spacing) only when a brand guide gives one.
|
|
52
|
+
- Typography: the brand's fonts (set_typography), each with what it is for ("Headings", "Body text") and the weights it uses — from the website's stylesheet (read_website lists its fonts) or a brand guide; system and generic faces are no brand fonts. A font's licence and what it may be used for (web, documents, print, images, video) only where a brand guide, the font's own page or the person says so; the wizards keep to it. A type scale (text styles: name, size, weight, line height, letter spacing) only when a brand guide gives one.
|
|
52
53
|
- Asset sections are optional too: tabs of one kind each ("Icons", "Background images"), made with set_asset_sections only when the space holds enough assets of clearly different kinds or the person asks; a handful of pictures stay in the general tab.
|
|
53
54
|
- Files the person attached are stored already; the message names them. look_at_file shows you a picture or a document's text. Give a file a description with describe_file when it has none or the person says what it is. Take facts from a document when asked.
|
|
54
55
|
- Wissen (German „Wissen“) is what the wizards' steps look things up in: pages, tables and files, kept apart by typed Kategorien. Documents the person attaches are read into Wissen by themselves: say so; do not copy their text into facts unless asked. A website, a sitemap or a folder whose entries should go into Wissen is a source: where a tool proposes sources, use it (the person takes the proposal on its card); where there is none, say that sources need the Sources plugin.
|
|
@@ -125,9 +126,8 @@ async function projectState(userId, projectId) {
|
|
|
125
126
|
// Optional parts are only there when the space has them.
|
|
126
127
|
...(p.brand.colorSections?.length
|
|
127
128
|
? {
|
|
128
|
-
colorSections: p.brand.colorSections.map(({ title,
|
|
129
|
+
colorSections: p.brand.colorSections.map(({ title, kind }) => ({
|
|
129
130
|
title,
|
|
130
|
-
description,
|
|
131
131
|
kind: kind ?? "colors",
|
|
132
132
|
})),
|
|
133
133
|
}
|
|
@@ -136,6 +136,7 @@ async function projectState(userId, projectId) {
|
|
|
136
136
|
? {
|
|
137
137
|
gradients: p.brand.gradients.map((g) => ({
|
|
138
138
|
name: g.name,
|
|
139
|
+
...(g.description ? { description: g.description } : {}),
|
|
139
140
|
section: p.brand.colorSections?.find((s) => s.id === g.section)?.title,
|
|
140
141
|
css: gradientCss(g),
|
|
141
142
|
})),
|
|
@@ -145,6 +146,7 @@ async function projectState(userId, projectId) {
|
|
|
145
146
|
? {
|
|
146
147
|
patterns: p.brand.patterns.map((x) => ({
|
|
147
148
|
name: x.name,
|
|
149
|
+
...(x.description ? { description: x.description } : {}),
|
|
148
150
|
section: p.brand.colorSections?.find((s) => s.id === x.section)?.title,
|
|
149
151
|
tile: Boolean(x.tile),
|
|
150
152
|
})),
|
|
@@ -165,7 +167,15 @@ async function projectState(userId, projectId) {
|
|
|
165
167
|
}
|
|
166
168
|
: {}),
|
|
167
169
|
...(p.brand.fonts?.length
|
|
168
|
-
? {
|
|
170
|
+
? {
|
|
171
|
+
fonts: p.brand.fonts.map(({ family, role, weights, license, uses }) => ({
|
|
172
|
+
family,
|
|
173
|
+
role,
|
|
174
|
+
weights,
|
|
175
|
+
...(license ? { license } : {}),
|
|
176
|
+
...(uses?.length ? { uses } : {}),
|
|
177
|
+
})),
|
|
178
|
+
}
|
|
169
179
|
: {}),
|
|
170
180
|
...(p.brand.textStyles?.length
|
|
171
181
|
? {
|
|
@@ -188,8 +198,7 @@ async function projectState(userId, projectId) {
|
|
|
188
198
|
})),
|
|
189
199
|
};
|
|
190
200
|
}
|
|
191
|
-
|
|
192
|
-
function spaceTools(input, project, wrote) {
|
|
201
|
+
function spaceTools(input, project, wrote, on) {
|
|
193
202
|
const { signal } = input;
|
|
194
203
|
const userId = input.user.id;
|
|
195
204
|
const projectId = project.id;
|
|
@@ -292,7 +301,7 @@ function spaceTools(input, project, wrote) {
|
|
|
292
301
|
}),
|
|
293
302
|
set_colors: createTool({
|
|
294
303
|
id: "set_colors",
|
|
295
|
-
description: "Set the brand's colours — the whole list, the main colour first. Values as #rrggbb. Optional: `sections` for a further palette of its own use (title
|
|
304
|
+
description: "Set the brand's colours — the whole list, the main colour first. Values as #rrggbb. Optional: `sections` for a further palette of its own use (its title), and on a colour the `section` title it belongs to; colours without one are the standard ones. Without `sections` the space keeps its sections.",
|
|
296
305
|
inputSchema: z.object({
|
|
297
306
|
colors: z
|
|
298
307
|
.array(z.object({
|
|
@@ -302,12 +311,15 @@ function spaceTools(input, project, wrote) {
|
|
|
302
311
|
}))
|
|
303
312
|
.max(PROJECT_LIMITS.colors),
|
|
304
313
|
sections: z
|
|
305
|
-
.array(z.object({ title: z.string().min(1).max(80)
|
|
314
|
+
.array(z.object({ title: z.string().min(1).max(80) }))
|
|
306
315
|
.max(PROJECT_LIMITS.logoSections)
|
|
307
316
|
.optional(),
|
|
308
317
|
}),
|
|
309
318
|
execute: ({ colors, sections }) => attempt(() => inTurn(async () => {
|
|
310
319
|
doing("set_colors");
|
|
320
|
+
if (!on.brandSections && (sections || colors.some((c) => c.section))) {
|
|
321
|
+
throw new Error("Colour sections are not available in this studio: pass the colours without them.");
|
|
322
|
+
}
|
|
311
323
|
const p = await ownedProject(userId, projectId);
|
|
312
324
|
const groups = sections
|
|
313
325
|
? sections.map((s) => ({
|
|
@@ -392,13 +404,22 @@ function spaceTools(input, project, wrote) {
|
|
|
392
404
|
}),
|
|
393
405
|
set_typography: createTool({
|
|
394
406
|
id: "set_typography",
|
|
395
|
-
description: "Set the brand's fonts — the whole list, the main one first — each with what it is for and its
|
|
407
|
+
description: "Set the brand's fonts — the whole list, the main one first — each with what it is for, its weights and, where a source says so, its licence and what it may be used for. Optional: `textStyles`, the type scale a brand guide gives (name, font family, size px, weight, line height %, letter spacing %); without it the space keeps its text styles.",
|
|
396
408
|
inputSchema: z.object({
|
|
397
409
|
fonts: z
|
|
398
410
|
.array(z.object({
|
|
399
411
|
family: z.string().min(1).max(80),
|
|
400
412
|
role: z.string().max(200).describe("What it is for, in the person's language"),
|
|
401
413
|
weights: z.array(z.number().int().min(100).max(900)).max(9),
|
|
414
|
+
license: z
|
|
415
|
+
.string()
|
|
416
|
+
.max(200)
|
|
417
|
+
.optional()
|
|
418
|
+
.describe("The licence, only where a source names it: 'SIL Open Font License'"),
|
|
419
|
+
uses: z
|
|
420
|
+
.array(z.enum(FONT_USES))
|
|
421
|
+
.optional()
|
|
422
|
+
.describe("All the licence allows it for, only where a source says so"),
|
|
402
423
|
}))
|
|
403
424
|
.max(PROJECT_LIMITS.fonts),
|
|
404
425
|
textStyles: z
|
|
@@ -441,15 +462,18 @@ function spaceTools(input, project, wrote) {
|
|
|
441
462
|
}),
|
|
442
463
|
set_gradients: createTool({
|
|
443
464
|
id: "set_gradients",
|
|
444
|
-
description: "Optional. Set the brand's gradients, as a brand guide or the site's stylesheet gives them: each with a name and its CSS (linear-, radial- or conic-gradient with hex stops). They go into a gradients section, made when there is none with that title. Replaces the gradients of that section.",
|
|
465
|
+
description: "Optional. Set the brand's gradients, as a brand guide or the site's stylesheet gives them: each with a name, when to use it where the source says so, and its CSS (linear-, radial- or conic-gradient with hex stops). They go into a gradients section, made when there is none with that title. Replaces the gradients of that section.",
|
|
445
466
|
inputSchema: z.object({
|
|
446
467
|
section: z.string().min(1).max(80).describe("The gradients section's title"),
|
|
447
|
-
description: z.string().max(400).optional().describe("When to use them"),
|
|
448
468
|
gradients: z
|
|
449
|
-
.array(z.object({
|
|
469
|
+
.array(z.object({
|
|
470
|
+
name: z.string().min(1).max(80),
|
|
471
|
+
description: z.string().max(400).optional().describe("When to use it"),
|
|
472
|
+
css: z.string().max(1000),
|
|
473
|
+
}))
|
|
450
474
|
.max(PROJECT_LIMITS.gradients),
|
|
451
475
|
}),
|
|
452
|
-
execute: ({ section,
|
|
476
|
+
execute: ({ section, gradients }) => attempt(() => inTurn(async () => {
|
|
453
477
|
doing("set_gradients");
|
|
454
478
|
const p = await ownedProject(userId, projectId);
|
|
455
479
|
const sections = p.brand.colorSections ?? [];
|
|
@@ -463,16 +487,14 @@ function spaceTools(input, project, wrote) {
|
|
|
463
487
|
await updateProject(userId, projectId, {
|
|
464
488
|
brand: {
|
|
465
489
|
colorSections: found
|
|
466
|
-
? sections
|
|
467
|
-
: [
|
|
468
|
-
...sections,
|
|
469
|
-
{ id, title: section, description: description ?? "", kind: "gradients" },
|
|
470
|
-
],
|
|
490
|
+
? sections
|
|
491
|
+
: [...sections, { id, title: section, kind: "gradients" }],
|
|
471
492
|
gradients: [
|
|
472
493
|
...(p.brand.gradients ?? []).filter((g) => g.section !== id),
|
|
473
494
|
...parsed.map(({ g, read }) => ({
|
|
474
495
|
id: nanoid(10),
|
|
475
496
|
name: g.name,
|
|
497
|
+
...(g.description ? { description: g.description } : {}),
|
|
476
498
|
section: id,
|
|
477
499
|
...read,
|
|
478
500
|
})),
|
|
@@ -582,6 +604,16 @@ function spaceTools(input, project, wrote) {
|
|
|
582
604
|
},
|
|
583
605
|
}),
|
|
584
606
|
};
|
|
607
|
+
if (!on.brandSections) {
|
|
608
|
+
delete tools.set_logo_sections;
|
|
609
|
+
delete tools.set_gradients;
|
|
610
|
+
}
|
|
611
|
+
if (!on.brandRules) {
|
|
612
|
+
delete tools.set_rules;
|
|
613
|
+
}
|
|
614
|
+
if (!on.typography) {
|
|
615
|
+
delete tools.set_typography;
|
|
616
|
+
}
|
|
585
617
|
return tools;
|
|
586
618
|
}
|
|
587
619
|
/** A browser tool as the model calls it: sent to the page, its answer awaited. */
|
|
@@ -673,6 +705,12 @@ export async function runStudioAssistant(input) {
|
|
|
673
705
|
const cards = [];
|
|
674
706
|
const { project } = input;
|
|
675
707
|
const space = project && input.writable ? project : null;
|
|
708
|
+
const switches = await currentFeatures();
|
|
709
|
+
const brandOn = {
|
|
710
|
+
brandSections: switches.brandSections === true,
|
|
711
|
+
brandRules: switches.brandRules === true,
|
|
712
|
+
typography: switches.typography === true,
|
|
713
|
+
};
|
|
676
714
|
const tools = {
|
|
677
715
|
...knowsTools({
|
|
678
716
|
user: input.user,
|
|
@@ -687,7 +725,7 @@ export async function runStudioAssistant(input) {
|
|
|
687
725
|
input.onCard?.(card);
|
|
688
726
|
},
|
|
689
727
|
}),
|
|
690
|
-
...(space ? spaceTools(input, space, wrote) : {}),
|
|
728
|
+
...(space ? spaceTools(input, space, wrote, brandOn) : {}),
|
|
691
729
|
};
|
|
692
730
|
// What the tenant's plugins let the assistant do besides: a source of knowledge, say.
|
|
693
731
|
if (space) {
|
|
@@ -763,6 +801,15 @@ export async function runStudioAssistant(input) {
|
|
|
763
801
|
part === "knowledge"
|
|
764
802
|
? "The person talks to you from Wissen: they want the wizards to know something."
|
|
765
803
|
: "",
|
|
804
|
+
space && !(brandOn.brandSections && brandOn.brandRules && brandOn.typography)
|
|
805
|
+
? `Not in this studio (no tools for it; never offer it): ${[
|
|
806
|
+
brandOn.brandSections ? "" : "sections of logos and colours, gradients and patterns",
|
|
807
|
+
brandOn.brandRules ? "" : "rules for logos, colours and type",
|
|
808
|
+
brandOn.typography ? "" : "typography (fonts, type scale)",
|
|
809
|
+
]
|
|
810
|
+
.filter(Boolean)
|
|
811
|
+
.join("; ")}.`
|
|
812
|
+
: "",
|
|
766
813
|
input.message,
|
|
767
814
|
]
|
|
768
815
|
.filter(Boolean)
|
|
@@ -9,9 +9,11 @@ import { deleteSetting, readSetting, writeSetting } from "../settings.js";
|
|
|
9
9
|
* browser at the Manage-App, the code comes back to this server on the loopback interface. The
|
|
10
10
|
* account is optional; it unlocks credits through the gateway and publishing to the cloud.
|
|
11
11
|
*/
|
|
12
|
-
const CLIENT = { id: "wizards-desktop" };
|
|
12
|
+
export const CLIENT = { id: "wizards-desktop" };
|
|
13
13
|
const REFRESH = "account.refresh";
|
|
14
|
-
const SCOPE = "openid profile email offline_access wizards:read wizards:write wizards:publish";
|
|
14
|
+
export const SCOPE = "openid profile email offline_access wizards:read wizards:write wizards:publish";
|
|
15
|
+
/** The team of the linked account as the Manage-App last named it, for lines that must not wait on it. */
|
|
16
|
+
const TEAM = "account.team";
|
|
15
17
|
/** Loopback redirect on the port this server listens on (RFC 8252). */
|
|
16
18
|
function redirectUri() {
|
|
17
19
|
return `http://127.0.0.1:${env.port}/api/auth/callback`;
|
|
@@ -48,7 +50,11 @@ export async function startLink(options = {}) {
|
|
|
48
50
|
return signUp.toString();
|
|
49
51
|
}
|
|
50
52
|
let cached = null;
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Keeps what the token endpoint handed out: the refresh token in the vault, the account in the
|
|
55
|
+
* settings. The browser flow (finishLink) and the device flow (device.ts) end here alike.
|
|
56
|
+
*/
|
|
57
|
+
export async function keep(tokens) {
|
|
52
58
|
const claims = unverifiedClaims(tokens.access_token);
|
|
53
59
|
if (!claims) {
|
|
54
60
|
throw new Error("The account's token names no tenant.");
|
|
@@ -128,6 +134,25 @@ export async function unlink() {
|
|
|
128
134
|
cached = null;
|
|
129
135
|
await vaultDelete(REFRESH);
|
|
130
136
|
await deleteSetting("account");
|
|
137
|
+
await deleteSetting(TEAM);
|
|
138
|
+
}
|
|
139
|
+
/** The team as the Manage-App last named it: `wizards status` and the menu show it without a round trip. */
|
|
140
|
+
export function knownTeam() {
|
|
141
|
+
return readSetting(TEAM);
|
|
142
|
+
}
|
|
143
|
+
let rememberedTeam = null;
|
|
144
|
+
async function rememberTeam(tenant) {
|
|
145
|
+
const same = `${tenant.id}\n${tenant.name}\n${tenant.balanceCredits}`;
|
|
146
|
+
if (same === rememberedTeam) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
rememberedTeam = same;
|
|
150
|
+
await writeSetting(TEAM, {
|
|
151
|
+
tenantId: tenant.id,
|
|
152
|
+
name: tenant.name,
|
|
153
|
+
balanceCredits: tenant.balanceCredits,
|
|
154
|
+
at: new Date().toISOString(),
|
|
155
|
+
});
|
|
131
156
|
}
|
|
132
157
|
/** Balance and tenants of the linked account, straight from the Manage-App. */
|
|
133
158
|
export async function accountOverview() {
|
|
@@ -139,5 +164,12 @@ export async function accountOverview() {
|
|
|
139
164
|
headers: { authorization: `Bearer ${token}` },
|
|
140
165
|
signal: AbortSignal.timeout(10_000),
|
|
141
166
|
}).catch(() => null);
|
|
142
|
-
|
|
167
|
+
if (!res?.ok) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const overview = (await res.json());
|
|
171
|
+
if (overview?.tenant) {
|
|
172
|
+
await rememberTeam(overview.tenant).catch(() => undefined);
|
|
173
|
+
}
|
|
174
|
+
return overview;
|
|
143
175
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { env } from "../env.js";
|
|
2
|
+
import { discovery, ManageError, tokenRequest } from "../manage.js";
|
|
3
|
+
import { CLIENT, keep, SCOPE } from "./account.js";
|
|
4
|
+
/**
|
|
5
|
+
* Signing in from a terminal (RFC 8628): the Manage-App hands out a code, the person types it
|
|
6
|
+
* on any browser at the account's /device page and allows it, and this process asks the token
|
|
7
|
+
* endpoint until they did. For `wizards setup` and `wizards account sign-in`; the studio keeps
|
|
8
|
+
* the browser flow of account.ts. Both end in the same keep(): the refresh token in the vault,
|
|
9
|
+
* the account in the settings, so the runtime picks it up wherever it runs.
|
|
10
|
+
*
|
|
11
|
+
* POST <account>/api/auth/device/code client_id, scope, resource → the codes
|
|
12
|
+
* POST <token endpoint of the discovery> grant_type device_code, every `interval` seconds,
|
|
13
|
+
* until tokens come, or the code expired or was refused
|
|
14
|
+
*/
|
|
15
|
+
const GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
16
|
+
/** Why a sign-in ended without an account; the message is for the person. */
|
|
17
|
+
export class DeviceSignInError extends Error {
|
|
18
|
+
reason;
|
|
19
|
+
constructor(reason, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Asks the Manage-App for a code to sign this install in with. */
|
|
25
|
+
export async function requestDeviceCode() {
|
|
26
|
+
const base = env.local.accountUrl;
|
|
27
|
+
// The discovery names the endpoint when the Manage-App advertises it.
|
|
28
|
+
const d = (await discovery(base).catch(() => null));
|
|
29
|
+
const res = await fetch(d?.device_authorization_endpoint ?? `${base}/api/auth/device/code`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
32
|
+
body: new URLSearchParams({
|
|
33
|
+
client_id: CLIENT.id,
|
|
34
|
+
scope: SCOPE,
|
|
35
|
+
resource: env.local.cloudUrl,
|
|
36
|
+
}),
|
|
37
|
+
signal: AbortSignal.timeout(15_000),
|
|
38
|
+
}).catch(() => null);
|
|
39
|
+
if (!res) {
|
|
40
|
+
throw new DeviceSignInError("unreachable", `${base} is not reachable right now.`);
|
|
41
|
+
}
|
|
42
|
+
const data = (await res.json().catch(() => ({})));
|
|
43
|
+
if (!(res.ok && data.device_code && data.user_code && data.verification_uri)) {
|
|
44
|
+
throw new DeviceSignInError("refused", data.error_description ||
|
|
45
|
+
(data.error
|
|
46
|
+
? `The account refused the sign-in (${data.error}).`
|
|
47
|
+
: `The account did not hand out a code (${res.status}).`));
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
deviceCode: data.device_code,
|
|
51
|
+
userCode: data.user_code,
|
|
52
|
+
verificationUri: data.verification_uri,
|
|
53
|
+
verificationUriComplete: data.verification_uri_complete ?? null,
|
|
54
|
+
expiresAt: Date.now() + (data.expires_in ?? 1800) * 1000,
|
|
55
|
+
interval: Math.max(1, data.interval ?? 5),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const pause = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
59
|
+
/**
|
|
60
|
+
* Asks the token endpoint until the person allowed the code in the browser, and keeps the
|
|
61
|
+
* tokens as the browser flow does. `authorization_pending` asks again after `interval`;
|
|
62
|
+
* `slow_down` asks five seconds later each time (RFC 8628 §3.5); a network error is tried
|
|
63
|
+
* again a few times. `expired_token` and `access_denied` end it.
|
|
64
|
+
*/
|
|
65
|
+
export async function waitForDeviceSignIn(code, options = {}) {
|
|
66
|
+
const wait = options.wait ?? pause;
|
|
67
|
+
let interval = code.interval;
|
|
68
|
+
let failures = 0;
|
|
69
|
+
for (;;) {
|
|
70
|
+
await wait(interval * 1000);
|
|
71
|
+
if (Date.now() >= code.expiresAt) {
|
|
72
|
+
throw new DeviceSignInError("expired", "The code has expired. Start the sign-in again.");
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const tokens = await tokenRequest({ grant_type: GRANT, device_code: code.deviceCode, resource: env.local.cloudUrl }, CLIENT, env.local.accountUrl);
|
|
76
|
+
return await keep(tokens);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (!(err instanceof ManageError)) {
|
|
80
|
+
if (++failures >= 5) {
|
|
81
|
+
throw new DeviceSignInError("unreachable", `${env.local.accountUrl} stopped answering. Start the sign-in again.`);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
failures = 0;
|
|
86
|
+
switch (err.code) {
|
|
87
|
+
case "authorization_pending":
|
|
88
|
+
continue;
|
|
89
|
+
case "slow_down":
|
|
90
|
+
interval += 5;
|
|
91
|
+
continue;
|
|
92
|
+
case "expired_token":
|
|
93
|
+
throw new DeviceSignInError("expired", "The code has expired. Start the sign-in again.");
|
|
94
|
+
case "access_denied":
|
|
95
|
+
throw new DeviceSignInError("denied", "The sign-in was refused in the browser.");
|
|
96
|
+
default:
|
|
97
|
+
throw new DeviceSignInError("refused", `The account refused the sign-in (${err.code}).`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -273,7 +273,7 @@ export const authRoutes = new Hono()
|
|
|
273
273
|
if (!managed) {
|
|
274
274
|
// The linked account of a runtime that runs alone: the system browser lands here.
|
|
275
275
|
if (!code) {
|
|
276
|
-
return c.html(closePage("
|
|
276
|
+
return c.html(closePage("Sign-in cancelled. You can close this window."), 400);
|
|
277
277
|
}
|
|
278
278
|
try {
|
|
279
279
|
await finishLink(code, rawState);
|
|
@@ -281,11 +281,11 @@ export const authRoutes = new Hono()
|
|
|
281
281
|
void firstSync();
|
|
282
282
|
// The Pro modules the account's plan includes.
|
|
283
283
|
void refreshPro();
|
|
284
|
-
return c.html(closePage("
|
|
284
|
+
return c.html(closePage("Signed in. You can close this window."));
|
|
285
285
|
}
|
|
286
286
|
catch (err) {
|
|
287
287
|
console.error("[account]", err);
|
|
288
|
-
return c.html(closePage("
|
|
288
|
+
return c.html(closePage("The sign-in did not work. Please try again."), 400);
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
const state = unseal(getCookie(c, STATE_COOKIE) ?? "");
|
|
@@ -322,7 +322,7 @@ export const authRoutes = new Hono()
|
|
|
322
322
|
exp: Date.now() + 2 * 60_000,
|
|
323
323
|
});
|
|
324
324
|
const link = `engenty-wizards://signin?code=${encodeURIComponent(code)}`;
|
|
325
|
-
return c.html(`${closePage("
|
|
325
|
+
return c.html(`${closePage("Signed in. Go back to engenty wizards now.")}
|
|
326
326
|
<script>location.href = ${JSON.stringify(link)};</script>`);
|
|
327
327
|
}
|
|
328
328
|
setSessionCookie(c, id);
|