wizards 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +28 -14
  2. package/apps/runtime/dist/agents/studio-assistant.js +79 -7
  3. package/apps/runtime/dist/assistant/runs.js +24 -0
  4. package/apps/runtime/dist/auth/account.js +36 -4
  5. package/apps/runtime/dist/auth/device.js +101 -0
  6. package/apps/runtime/dist/auth/index.js +4 -4
  7. package/apps/runtime/dist/cli/account.js +289 -0
  8. package/apps/runtime/dist/cli/install-env.js +33 -0
  9. package/apps/runtime/dist/cli/main.js +17 -3
  10. package/apps/runtime/dist/cli/menu.js +19 -5
  11. package/apps/runtime/dist/cli/setup.js +9 -16
  12. package/apps/runtime/dist/cli/status.js +4 -1
  13. package/apps/runtime/dist/cli/ui.js +14 -0
  14. package/apps/runtime/dist/docs/user/assistant.md +25 -7
  15. package/apps/runtime/dist/docs/user/command-line.md +3 -2
  16. package/apps/runtime/dist/docs/user/create-account.md +47 -0
  17. package/apps/runtime/dist/docs/user/first-start.md +9 -1
  18. package/apps/runtime/dist/docs/user/index.md +16 -12
  19. package/apps/runtime/dist/docs/user/install.md +3 -1
  20. package/apps/runtime/dist/docs/user/local-only.md +38 -0
  21. package/apps/runtime/dist/docs/user/meta.json +3 -1
  22. package/apps/runtime/dist/docs/user/models.md +3 -2
  23. package/apps/runtime/dist/env.js +5 -0
  24. package/apps/runtime/dist/manage.js +16 -4
  25. package/apps/runtime/dist/models.js +6 -0
  26. package/apps/runtime/dist/plugins/pro.js +16 -16
  27. package/apps/runtime/dist/routes/studio.js +140 -40
  28. package/apps/runtime/dist/services/assistant-chats.js +49 -6
  29. package/apps/runtime/dist/services/cloud.js +10 -10
  30. package/apps/runtime/dist/services/onboarding.js +152 -0
  31. package/apps/web/dist/assets/{AboutDialog-C0dN86zd.js → AboutDialog-CPQiN7H9.js} +1 -1
  32. package/apps/web/dist/assets/{ChangelogDialog-Cv81t7tj.js → ChangelogDialog-DynvQuuR.js} +1 -1
  33. package/apps/web/dist/assets/ChatView-CKo_9IZD.js +10 -0
  34. package/apps/web/dist/assets/EditorPage-CDIJXC4T.js +3 -0
  35. package/apps/web/dist/assets/{FlowPreview-D4dp1MWS.js → FlowPreview-BZLnG7GV.js} +1 -1
  36. package/apps/web/dist/assets/{Harness-v7r0e6vK.js → Harness-CNh3nbB_.js} +2 -2
  37. package/apps/web/dist/assets/{NewWizardChannels-Be-7VhBi.js → NewWizardChannels-C1Ss9UMY.js} +1 -1
  38. package/apps/web/dist/assets/{NewWizardPage-C8hPvrK8.js → NewWizardPage-2qLAbvDL.js} +2 -2
  39. package/apps/web/dist/assets/OnboardingPage-C1FJwEsT.js +1 -0
  40. package/apps/web/dist/assets/ProjectPage-CGuLUuOs.js +1 -0
  41. package/apps/web/dist/assets/{PublicRunner-DeCpVfZP.js → PublicRunner-BKw1lOW4.js} +1 -1
  42. package/apps/web/dist/assets/{ReadOnly-B-UCgnqW.js → ReadOnly-DTjv2Jzm.js} +1 -1
  43. package/apps/web/dist/assets/RunDrawer-D1I5xqVf.js +4 -0
  44. package/apps/web/dist/assets/RunnerView-BPPmlv9z.js +1 -0
  45. package/apps/web/dist/assets/SettingsPage-zbomQfGy.js +1 -0
  46. package/apps/web/dist/assets/SetupPage-D3F7M6Pz.js +1 -0
  47. package/apps/web/dist/assets/{SharePage-B778xUFo.js → SharePage-BY49IJgT.js} +1 -1
  48. package/apps/web/dist/assets/{changelog-DCJo2mbx.js → changelog-DYZC2E6q.js} +1 -1
  49. package/apps/web/dist/assets/{circle-alert-DLvC9ePT.js → circle-alert-CVOH4wAv.js} +1 -1
  50. package/apps/web/dist/assets/{esm-BJskogLD.js → esm-BfXdeWaI.js} +1 -1
  51. package/apps/web/dist/assets/index-B9xVkr2X.css +1 -0
  52. package/apps/web/dist/assets/index-BH0PYgov.js +15 -0
  53. package/apps/web/dist/assets/{lock-DR7NakPn.js → lock-DHBh62LA.js} +1 -1
  54. package/apps/web/dist/assets/{phone-BORh3jfv.js → phone-Bt1cyWpY.js} +1 -1
  55. package/apps/web/dist/assets/{settings-menu-DV5D91TD.js → settings-menu-ace1vI3i.js} +1 -1
  56. package/apps/web/dist/assets/{ui-BOCXzzHi.js → ui-D2SqIZeQ.js} +2 -2
  57. package/apps/web/dist/assets/{upload-B3yv8gsk.js → upload-CCEq88l4.js} +1 -1
  58. package/apps/web/dist/index.html +3 -3
  59. package/apps/web/dist/install.sh +17 -7
  60. package/apps/web/dist/mcp/flow.html +3 -3
  61. package/node_modules/@engenty-wizards/shared/dist/assistant.d.ts +62 -1
  62. package/node_modules/@engenty-wizards/shared/dist/assistant.js +10 -0
  63. package/package.json +1 -1
  64. package/apps/web/dist/assets/ChatView-uogJ3nht.js +0 -10
  65. package/apps/web/dist/assets/EditorPage-n8YuTh6j.js +0 -3
  66. package/apps/web/dist/assets/ProjectPage-B686ArYy.js +0 -1
  67. package/apps/web/dist/assets/RunDrawer-C6pzi8AJ.js +0 -4
  68. package/apps/web/dist/assets/RunnerView-Deenymq0.js +0 -1
  69. package/apps/web/dist/assets/SettingsPage-CK6L0yEe.js +0 -1
  70. package/apps/web/dist/assets/SetupPage-9CH3qXdH.js +0 -1
  71. package/apps/web/dist/assets/index-D6hUM8NU.css +0 -1
  72. package/apps/web/dist/assets/index-Df6UxElH.js +0 -12
@@ -0,0 +1,289 @@
1
+ import * as p from "@clack/prompts";
2
+ import { accountOverview, knownTeam, linkedAccount, unlink, } from "../auth/account.js";
3
+ import { DeviceSignInError, requestDeviceCode, waitForDeviceSignIn } from "../auth/device.js";
4
+ import { migrateControlDb } from "../db/client.js";
5
+ import { env } from "../env.js";
6
+ import { featuresFile, fileFeatures, writeFeature } from "../features.js";
7
+ import { command, readInstallNote, writeInstallNote } from "./home.js";
8
+ import { openBrowser, runningRuntime, settings } from "./start.js";
9
+ import { bold, busy, cyan, dim, tilde } from "./ui.js";
10
+ /**
11
+ * The account of this install, from the terminal. `wizards account` says where it stands,
12
+ * `sign-in` runs the device flow (auth/device.ts), `sign-out` ends the link the way the
13
+ * studio does. The setup's first step is the same sign-in. `--local` keeps an install off the
14
+ * account altogether: `account=off` in the install's `.features` hides every account surface
15
+ * (packages/shared/src/feature-flags.ts), and a note in install.json says so.
16
+ *
17
+ * Everything here reads and writes the install's settings and vault through the runtime's own
18
+ * modules, with or without a runtime running (install-env.ts puts this process where the
19
+ * runtime is). A runtime that runs reads the account from the settings as it goes; what it
20
+ * holds from its start is brought in line by a restart, which is offered.
21
+ */
22
+ export const INTRO = "Your team, its space and your wizards live in the account. This computer joins the space.";
23
+ let prepared = null;
24
+ /** The control database as the runtime keeps it: a fresh install has none until something opens it. */
25
+ function ready() {
26
+ prepared ??= migrateControlDb();
27
+ return prepared;
28
+ }
29
+ /** Set up with `--local`: the install's `.features` says `account=off`. */
30
+ export function isLocalOnly(file = featuresFile()) {
31
+ return fileFeatures(file).account === false;
32
+ }
33
+ /** `--local`: the switch into the file, the note into install.json. */
34
+ export function localOnly(paths, file = featuresFile()) {
35
+ writeFeature("account", false, file);
36
+ writeInstallNote({ ...readInstallNote(paths), local: true }, paths);
37
+ }
38
+ /** A sign-in ends local-only: the switch follows the default again, the note goes. */
39
+ export function joinedAccount(paths, file = featuresFile()) {
40
+ if (isLocalOnly(file)) {
41
+ writeFeature("account", null, file);
42
+ }
43
+ const note = readInstallNote(paths);
44
+ if (note.local) {
45
+ const { local: _, ...rest } = note;
46
+ writeInstallNote(rest, paths);
47
+ }
48
+ }
49
+ /** Where the account stands, from this machine alone: no round trip. */
50
+ export async function accountState() {
51
+ if (isLocalOnly()) {
52
+ return { kind: "local" };
53
+ }
54
+ await ready();
55
+ const account = await linkedAccount();
56
+ if (!account) {
57
+ return { kind: "none" };
58
+ }
59
+ return { kind: "linked", account, team: await knownTeam() };
60
+ }
61
+ /** "Matthias · team Platzer Fenster & Türen · 120 credits", as far as it is known. */
62
+ export function described(account, team, credits = true) {
63
+ const parts = [account.name || account.email];
64
+ if (team) {
65
+ parts.push(`team ${team.name}`);
66
+ if (credits) {
67
+ parts.push(`${team.balanceCredits} credits`);
68
+ }
69
+ }
70
+ return parts.join(" · ");
71
+ }
72
+ /** One line for the menu and `wizards status`, after the symbol. */
73
+ export function describeAccount(state) {
74
+ switch (state.kind) {
75
+ case "local":
76
+ return "local only";
77
+ case "none":
78
+ return "not signed in";
79
+ default:
80
+ return described(state.account, state.team, false);
81
+ }
82
+ }
83
+ /** The account's site, as people know it: engenty.ai for account.engenty.ai. */
84
+ const host = () => new URL(env.local.accountUrl).host.replace(/^account\./, "");
85
+ /** `wizards account`: where it stands, with the Manage-App's word when it answers. */
86
+ async function status() {
87
+ const state = await accountState();
88
+ if (state.kind === "local") {
89
+ console.log(`Local only (no account). ${dim(`\`${command()} account sign-in\` connects one.`)}`);
90
+ return 0;
91
+ }
92
+ if (state.kind === "none") {
93
+ console.log(`Not signed in. ${dim(`\`${command()} account sign-in\``)}`);
94
+ return 0;
95
+ }
96
+ const overview = await accountOverview();
97
+ const team = overview?.tenant ?? state.team;
98
+ console.log(`Signed in as ${described(state.account, team)} · ${host()}${overview ? "" : dim(` (${host()} did not answer${team ? "; this is from last time" : ""})`)}`);
99
+ return 0;
100
+ }
101
+ /**
102
+ * The device flow on the terminal: the link and the code, the browser opened where there is
103
+ * one, then a wait while the person allows it. Null when it ended without an account.
104
+ */
105
+ async function deviceSignIn() {
106
+ let code;
107
+ try {
108
+ code = await requestDeviceCode();
109
+ }
110
+ catch (err) {
111
+ p.log.error(err.message);
112
+ return null;
113
+ }
114
+ const link = code.verificationUriComplete ?? code.verificationUri;
115
+ p.log.message(`Open ${cyan(link)} and type ${bold(code.userCode)}`);
116
+ if (openBrowser(link)) {
117
+ p.log.message(dim("The page opens in your browser."));
118
+ }
119
+ const spinner = busy("Waiting for you to allow it in the browser");
120
+ try {
121
+ const account = await waitForDeviceSignIn(code);
122
+ const overview = await accountOverview();
123
+ spinner.stop(`Signed in as ${described(account, overview?.tenant ?? null)}`);
124
+ return account;
125
+ }
126
+ catch (err) {
127
+ spinner.fail(err instanceof DeviceSignInError
128
+ ? err.message
129
+ : `The sign-in did not work. ${err.message}`);
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * A runtime that runs reads the account from the settings as it goes, but holds a few things
135
+ * from its start: the Pro modules it loaded, which source the models run on, what was sent to
136
+ * the cloud. A restart brings it in line; the person is asked, or told how.
137
+ */
138
+ async function pickUp(paths, interactive, why) {
139
+ if (!(await runningRuntime(settings(paths).dataDir))) {
140
+ return;
141
+ }
142
+ const how = `${command()} restart`;
143
+ if (!interactive) {
144
+ p.log.info(`The studio that runs ${why} after ${cyan(how)}.`);
145
+ return;
146
+ }
147
+ const answer = await p.confirm({
148
+ message: `Restart the studio that runs, so it ${why}?`,
149
+ initialValue: true,
150
+ });
151
+ if (p.isCancel(answer) || !answer) {
152
+ p.log.info(dim(`Later: ${how}`));
153
+ return;
154
+ }
155
+ // status.ts shows the account; loaded here when needed, so neither module waits on the other.
156
+ const { restart } = await import("./status.js");
157
+ await restart(paths);
158
+ }
159
+ const SIGNED_IN = "fetches the Pro modules of your plan and sends what is published to the cloud";
160
+ async function signIn(paths, interactive) {
161
+ await ready();
162
+ const linked = await linkedAccount();
163
+ if (linked) {
164
+ p.log.info(`Already signed in as ${described(linked, await knownTeam(), false)}. ${dim(`To switch: \`${command()} account sign-out\` first.`)}`);
165
+ return 0;
166
+ }
167
+ const account = await deviceSignIn();
168
+ if (!account) {
169
+ return 1;
170
+ }
171
+ joinedAccount(paths);
172
+ p.log.message(dim("What you publish here runs in the cloud too."));
173
+ await pickUp(paths, interactive, SIGNED_IN);
174
+ return 0;
175
+ }
176
+ async function signOut(paths, interactive) {
177
+ const state = await accountState();
178
+ if (state.kind !== "linked") {
179
+ console.log(state.kind === "local" ? "Local only: there is no account to sign out." : "Not signed in.");
180
+ return 0;
181
+ }
182
+ const spinner = busy(`Signing out ${state.account.name || state.account.email}`);
183
+ await unlink();
184
+ // The same as the studio's sign-out (DELETE /api/studio/account): what was sent to the cloud
185
+ // is forgotten, the entitlement goes with the account's plan, and models that ran on its
186
+ // credits run on own keys again. This process never loaded Pro modules or models, so only the
187
+ // settings change here; a runtime that runs lets go of its modules on restart.
188
+ const [{ forgetCloud }, { refreshPro }, models] = await Promise.all([
189
+ import("../services/cloud.js"),
190
+ import("../plugins/pro.js"),
191
+ import("../models.js"),
192
+ ]);
193
+ await forgetCloud();
194
+ await refreshPro();
195
+ await models.loadLocalModels();
196
+ if (models.localModelSettings().source === "account") {
197
+ await models.saveLocalModels({ source: "own" });
198
+ }
199
+ spinner.stop("Signed out. Your wizards and results stay on this computer.");
200
+ await pickUp(paths, interactive, "lets go of the account");
201
+ return 0;
202
+ }
203
+ /** `wizards account [sign-in|sign-out]`. */
204
+ export async function accountCommand(paths, value, options) {
205
+ const interactive = !options.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
206
+ switch (value) {
207
+ case "":
208
+ return status();
209
+ case "sign-in":
210
+ case "signin":
211
+ case "login":
212
+ return signIn(paths, interactive);
213
+ case "sign-out":
214
+ case "signout":
215
+ case "logout":
216
+ return signOut(paths, interactive);
217
+ default:
218
+ console.error(`${command()} account [sign-in|sign-out]`);
219
+ return 2;
220
+ }
221
+ }
222
+ /** The menu's "Account": where it stands, and the way in or out. */
223
+ export async function accountMenu(paths) {
224
+ await status();
225
+ const state = await accountState();
226
+ const action = await p.select({
227
+ message: "Account",
228
+ options: [
229
+ state.kind === "linked"
230
+ ? { value: "sign-out", label: "Sign out", hint: "your wizards and results stay here" }
231
+ : { value: "sign-in", label: "Sign in", hint: "with a code, in your browser" },
232
+ { value: "back", label: "Back" },
233
+ ],
234
+ });
235
+ if (p.isCancel(action) || action === "back") {
236
+ return;
237
+ }
238
+ if (action === "sign-in") {
239
+ await signIn(paths, true);
240
+ }
241
+ else {
242
+ await signOut(paths, true);
243
+ }
244
+ }
245
+ /**
246
+ * The setup's first step. `--local` keeps the install off the account; else the person is
247
+ * asked to sign in, unless the setup runs without a terminal (`--yes`), which says how to do
248
+ * it later.
249
+ */
250
+ export async function accountStep(paths, options, interactive) {
251
+ if (options.local) {
252
+ localOnly(paths);
253
+ p.log.warn(`Local only. No account, nothing leaves this computer. Wizards, the space and results stay in ${tilde(settings(paths).dataDir)}. Links answer on this computer only.`);
254
+ p.log.message(dim(`Connect an account any time: ${command()} account`));
255
+ return;
256
+ }
257
+ const state = await accountState();
258
+ if (state.kind === "local") {
259
+ p.log.info(`Local only (no account). ${dim(`\`${command()} account sign-in\` connects one.`)}`);
260
+ return;
261
+ }
262
+ if (state.kind === "linked") {
263
+ const overview = await accountOverview();
264
+ p.log.success(`Signed in as ${described(state.account, overview?.tenant ?? state.team)}`);
265
+ return;
266
+ }
267
+ p.note(INTRO, "Your engenty account");
268
+ if (!interactive) {
269
+ p.log.info(dim(`Sign in later: ${command()} account sign-in`));
270
+ return;
271
+ }
272
+ const answer = await p.confirm({ message: "Sign in now?", initialValue: true });
273
+ if (p.isCancel(answer)) {
274
+ p.cancel(`Setup stopped. Run it again with \`${command()} setup\`.`);
275
+ return "cancelled";
276
+ }
277
+ if (!answer) {
278
+ p.log.info(dim(`Later: ${command()} account`));
279
+ return;
280
+ }
281
+ const account = await deviceSignIn();
282
+ if (!account) {
283
+ p.log.info(dim(`Later: ${command()} account sign-in`));
284
+ return;
285
+ }
286
+ joinedAccount(paths);
287
+ p.log.message(dim("What you publish here runs in the cloud too."));
288
+ await pickUp(paths, interactive, SIGNED_IN);
289
+ }
@@ -0,0 +1,33 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { layout, readEnvFile } from "./home.js";
4
+ /**
5
+ * Imported first by the command: before the runtime's modules load, this process sees the
6
+ * install the way its runtime does. env.ts reads the process once, when it is first imported,
7
+ * and `.env` from the working directory; the runtime starts in the install's folder (start.ts)
8
+ * and so reads the install's `.env`, with its data folder beside it. The command goes there
9
+ * too — else `wizards account` or `wizards status` from another folder would open a `data/`
10
+ * of its own there, with a secret of its own, and a vault the runtime cannot read.
11
+ *
12
+ * What the terminal exports for other projects never reaches the runtime (environment.ts); a
13
+ * few keys that would make this process another runtime are taken from the install's `.env`
14
+ * alone. The rest the runtime inherits, so the terminal's word stands.
15
+ */
16
+ const paths = layout();
17
+ const fileEnv = readEnvFile(paths.envFile);
18
+ /** The install's data folder, as start.ts hands it to the runtime: the terminal's word first. */
19
+ const given = process.env.DATA_DIR?.trim() || fileEnv.DATA_DIR?.trim();
20
+ process.env.DATA_DIR = given ? resolve(paths.home, given) : paths.data;
21
+ for (const key of ["MANAGE_URL", "APP_SECRET", "DATABASE_URL", "DATABASE_AUTH_TOKEN"]) {
22
+ process.env[key] = fileEnv[key] ?? "";
23
+ }
24
+ // Keys and the account's sign-in live in the Keychain on a Mac, as the runtime keeps them.
25
+ if (process.platform === "darwin" &&
26
+ process.env.SECRETS === undefined &&
27
+ fileEnv.SECRETS === undefined) {
28
+ process.env.SECRETS = "keychain";
29
+ }
30
+ /** Where the command was typed: `wizards validate ./my.wizard` means a path from there. */
31
+ export const startedIn = process.cwd();
32
+ mkdirSync(paths.home, { recursive: true });
33
+ process.chdir(paths.home);
@@ -1,6 +1,9 @@
1
+ import "./install-env.js";
2
+ import { accountCommand } from "./account.js";
1
3
  import { apps, connectCommand } from "./connect.js";
2
4
  import { featuresCommand } from "./features.js";
3
5
  import { command, layout, packageVersion, readInstallNote } from "./home.js";
6
+ import { startedIn } from "./install-env.js";
4
7
  import { CLIENTS, nodeIsCurrent } from "./machine.js";
5
8
  import { mcp } from "./mcp.js";
6
9
  import { menu } from "./menu.js";
@@ -16,7 +19,9 @@ const HELP = `engenty wizards ${packageVersion()}
16
19
  (needs a terminal)
17
20
  ${command()} start start it and open the studio; it runs in this terminal until Ctrl-C
18
21
  ${command()} studio open the studio; when it does not run, it starts as a service first
19
- ${command()} setup guided setup: an AI client to think with, ffmpeg, start at login
22
+ ${command()} setup guided setup: your engenty account, an AI client to think with, ffmpeg,
23
+ start at login
24
+ ${command()} account your engenty account: status, sign-in, sign-out
20
25
  ${command()} open let this browser into the running studio (--print: only show the link)
21
26
  ${command()} status what is installed and what runs
22
27
  ${command()} doctor status, and what is wrong
@@ -37,6 +42,7 @@ const HELP = `engenty wizards ${packageVersion()}
37
42
  checked on import
38
43
 
39
44
  --yes, -y ask nothing and install nothing optional
45
+ --local setup without an account: nothing leaves this computer
40
46
  --client <name> install this AI client without asking: ${CLIENTS.map((c) => c.id).join(", ")}
41
47
  --no-open start without opening the browser
42
48
  --service, -s start as a service, detached from this terminal; \`${command()} stop\` stops it
@@ -52,6 +58,7 @@ function parse(argv) {
52
58
  paths: [],
53
59
  json: false,
54
60
  yes: false,
61
+ local: false,
55
62
  open: true,
56
63
  service: false,
57
64
  print: false,
@@ -62,6 +69,9 @@ function parse(argv) {
62
69
  if (arg === "--yes" || arg === "-y") {
63
70
  args.yes = true;
64
71
  }
72
+ else if (arg === "--local") {
73
+ args.local = true;
74
+ }
65
75
  else if (arg === "--no-open") {
66
76
  args.open = false;
67
77
  }
@@ -90,7 +100,7 @@ function parse(argv) {
90
100
  else if (arg.startsWith("-")) {
91
101
  return `Unknown option ${arg}.`;
92
102
  }
93
- else if ((args.command === "autostart" || args.command === "logs") && !args.value) {
103
+ else if (["autostart", "logs", "account"].includes(args.command) && !args.value) {
94
104
  args.value = arg;
95
105
  }
96
106
  else if (["connect", "disconnect", "features"].includes(args.command)) {
@@ -115,7 +125,7 @@ async function main(argv) {
115
125
  return 2;
116
126
  }
117
127
  const paths = layout();
118
- const setupOptions = { yes: args.yes, clients: args.clients };
128
+ const setupOptions = { yes: args.yes, clients: args.clients, local: args.local };
119
129
  if (!args.command) {
120
130
  // Without a command: the menu, which needs a terminal. Starting is \`start\`.
121
131
  if (args.service || !args.open || args.print) {
@@ -168,6 +178,8 @@ async function main(argv) {
168
178
  return update(paths);
169
179
  case "autostart":
170
180
  return autostart(paths, args.value);
181
+ case "account":
182
+ return accountCommand(paths, args.value, { yes: args.yes });
171
183
  case "connect":
172
184
  case "disconnect":
173
185
  return connectCommand(paths, args.apps, args.command === "disconnect");
@@ -176,6 +188,8 @@ async function main(argv) {
176
188
  case "features":
177
189
  return featuresCommand(args.apps);
178
190
  case "validate":
191
+ // The paths are the person's, from where they typed the command (install-env.ts).
192
+ process.chdir(startedIn);
179
193
  return validateCommand(args.paths, args.json);
180
194
  default:
181
195
  console.error(`Unknown command ${args.command}.\n\n${HELP}`);
@@ -1,5 +1,6 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { updateStatus } from "../update.js";
3
+ import { accountMenu, accountState, describeAccount } from "./account.js";
3
4
  import { autostartState, setAutostart } from "./autostart.js";
4
5
  import { connectCommand } from "./connect.js";
5
6
  import { command, packageVersion, readInstallNote } from "./home.js";
@@ -10,14 +11,15 @@ import { badge, cyan, dim, no, ok } from "./ui.js";
10
11
  import { update } from "./update.js";
11
12
  async function look(paths) {
12
13
  const { dataDir } = settings(paths);
13
- const [running, atLogin, release] = await Promise.all([
14
+ const [running, atLogin, release, account] = await Promise.all([
14
15
  runningRuntime(dataDir),
15
16
  autostartState(paths),
16
17
  updateStatus().catch(() => null),
18
+ accountState(),
17
19
  ]);
18
- return { running, atLogin, release };
20
+ return { running, atLogin, release, account };
19
21
  }
20
- function report({ running, atLogin, release }) {
22
+ function report({ running, atLogin, release, account }) {
21
23
  const lines = [
22
24
  running
23
25
  ? `${ok} running ${cyan(running.url)} ${dim(`pid ${running.pid}, since ${new Date(running.startedAt).toLocaleString()}`)}`
@@ -26,12 +28,15 @@ function report({ running, atLogin, release }) {
26
28
  if (!("unavailable" in atLogin)) {
27
29
  lines.push(atLogin.on ? `${ok} starts at login` : `${no} does not start at login`);
28
30
  }
31
+ lines.push(account.kind === "linked"
32
+ ? `${ok} signed in as ${describeAccount(account)}`
33
+ : `${no} ${describeAccount(account)}`);
29
34
  if (release?.newer) {
30
35
  lines.push(`${cyan("↑")} ${release.latest} is out ${dim(`(you have ${release.current})`)}`);
31
36
  }
32
37
  return lines.join("\n");
33
38
  }
34
- function choices({ running, atLogin, release }) {
39
+ function choices({ running, atLogin, release, account }) {
35
40
  const options = running
36
41
  ? [
37
42
  { value: "open", label: "Open the studio", hint: "in your browser" },
@@ -58,7 +63,13 @@ function choices({ running, atLogin, release }) {
58
63
  hint: atLogin.on ? "starts at login" : "does not start at login",
59
64
  });
60
65
  }
61
- options.push({ value: "connect", label: "Connect AI apps", hint: "your wizards in Claude, Cursor, Codex …" }, { value: "status", label: "Status", hint: "what is installed, and what is wrong" }, { value: "setup", label: "Setup", hint: "AI client, ffmpeg, start at login" }, { value: "help", label: "Help", hint: "every command" }, { value: "quit", label: "Quit", hint: running ? "it keeps running" : undefined });
66
+ options.push({ value: "connect", label: "Connect AI apps", hint: "your wizards in Claude, Cursor, Codex …" }, {
67
+ value: "account",
68
+ label: "Account",
69
+ hint: account.kind === "linked"
70
+ ? `signed in as ${describeAccount(account)}`
71
+ : describeAccount(account),
72
+ }, { value: "status", label: "Status", hint: "what is installed, and what is wrong" }, { value: "setup", label: "Setup", hint: "account, AI client, ffmpeg, start at login" }, { value: "help", label: "Help", hint: "every command" }, { value: "quit", label: "Quit", hint: running ? "it keeps running" : undefined });
62
73
  return options;
63
74
  }
64
75
  /** How the service runs: whether it starts at login. Esc goes back to the menu. */
@@ -153,6 +164,9 @@ export async function menu(paths, setupOptions, help) {
153
164
  case "connect":
154
165
  await connectCommand(paths, [], false);
155
166
  break;
167
+ case "account":
168
+ await accountMenu(paths);
169
+ break;
156
170
  case "status":
157
171
  await status(paths, true);
158
172
  break;
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { delimiter, join } from "node:path";
4
4
  import * as p from "@clack/prompts";
5
+ import { accountStep } from "./account.js";
5
6
  import { autostartState, setAutostart } from "./autostart.js";
6
7
  import { installClient, installCommand } from "./clients.js";
7
8
  import { appStates, connectApps } from "./connect.js";
@@ -9,21 +10,9 @@ import { runLogged } from "./exec.js";
9
10
  import { command, installedByScript, isCheckout, packageVersion, readInstallNote, writeInstallNote, } from "./home.js";
10
11
  import { CLIENTS, detectClients, findChrome, findFfmpeg, findOnPath, suggestedClient, vendorApp, } from "./machine.js";
11
12
  import { runningRuntime, settings } from "./start.js";
12
- import { badge, cyan, dim, tilde } from "./ui.js";
13
+ import { badge, busy, cyan, dim, tilde } from "./ui.js";
13
14
  /** The studio as an app of its own: Chrome (or Edge) installs it from the page. */
14
15
  const CHROME_APP = "Tip: in Chrome or Edge the studio installs as an app, in a window of its own with its own icon. The studio offers it at the top; or use the install icon in the address bar.";
15
- /** A spinner on a terminal; where nobody watches, only what came of it. */
16
- function busy(message) {
17
- if (!process.stdout.isTTY) {
18
- return { stop: (text) => p.log.step(text), fail: (text) => p.log.warn(text) };
19
- }
20
- const spinner = p.spinner();
21
- spinner.start(message);
22
- return {
23
- stop: (text) => spinner.stop(text),
24
- fail: (text) => spinner.error(text),
25
- };
26
- }
27
16
  function cancelled(value) {
28
17
  if (p.isCancel(value)) {
29
18
  p.cancel(`Setup stopped. Run it again with \`${command()} setup\`.`);
@@ -298,9 +287,10 @@ async function howToUse(paths) {
298
287
  ].join("\n"));
299
288
  }
300
289
  /**
301
- * The guided setup: what this machine has, what is missing, and what to install — an AI client
302
- * to think with, ffmpeg, starting at login. It starts nothing; it ends with what runs and how to
303
- * start, stop and check it. Returns false when it was stopped.
290
+ * The guided setup: the account this computer joins, then what this machine has, what is
291
+ * missing, and what to install — an AI client to think with, ffmpeg, starting at login. It
292
+ * starts nothing; it ends with what runs and how to start, stop and check it. Returns false
293
+ * when it was stopped.
304
294
  */
305
295
  export async function setup(paths, options) {
306
296
  const interactive = !options.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -310,6 +300,9 @@ export async function setup(paths, options) {
310
300
  `Install ${tilde(paths.home)}${installedByScript() ? "" : dim(` (data; the code runs from ${isCheckout() ? "this checkout" : "npm"})`)}`,
311
301
  `Node ${process.version} ${dim(tilde(process.execPath))}`,
312
302
  ].join("\n"));
303
+ if ((await accountStep(paths, options, interactive)) === "cancelled") {
304
+ return false;
305
+ }
313
306
  if ((await clientsStep(paths, options, interactive)) === "cancelled") {
314
307
  return false;
315
308
  }
@@ -1,5 +1,6 @@
1
1
  import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { accountState, describeAccount } from "./account.js";
3
4
  import { autostartState, setAutostart } from "./autostart.js";
4
5
  import { installedByScript, isCheckout, packageRoot, packageVersion } from "./home.js";
5
6
  import { detectClients, findChrome, findFfmpeg, nodeIsCurrent } from "./machine.js";
@@ -11,11 +12,12 @@ import { bad, badge, cyan, dim, no, ok, tilde } from "./ui.js";
11
12
  */
12
13
  export async function status(paths, checks) {
13
14
  const { dataDir } = settings(paths);
14
- const [running, clients, ffmpeg, atLogin] = await Promise.all([
15
+ const [running, clients, ffmpeg, atLogin, account] = await Promise.all([
15
16
  runningRuntime(dataDir),
16
17
  detectClients(paths),
17
18
  findFfmpeg(),
18
19
  autostartState(paths),
20
+ accountState(),
19
21
  ]);
20
22
  const chrome = findChrome();
21
23
  const row = (label, value) => console.log(` ${label.padEnd(9)} ${value}`);
@@ -31,6 +33,7 @@ export async function status(paths, checks) {
31
33
  if (!("unavailable" in atLogin)) {
32
34
  row("At login", atLogin.on ? `${ok} starts ${dim(`(${atLogin.how})`)}` : `${no} does not start`);
33
35
  }
36
+ row("Account", `${account.kind === "linked" ? ok : no} ${describeAccount(account)}`);
34
37
  for (const { client, path, version } of clients) {
35
38
  row(client.name.split(" ")[0], path ? `${ok} ${version} ${dim(tilde(path))}` : `${no} not installed`);
36
39
  }
@@ -1,12 +1,26 @@
1
1
  import { homedir } from "node:os";
2
2
  import { styleText } from "node:util";
3
+ import * as p from "@clack/prompts";
3
4
  const colours = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
4
5
  /** The name on engenty's orange. */
5
6
  export const badge = (text) => colours ? `\x1b[48;2;224;83;27m\x1b[97m\x1b[1m ${text} \x1b[0m` : text;
6
7
  export const dim = (text) => styleText("dim", text);
8
+ export const bold = (text) => styleText("bold", text);
7
9
  export const cyan = (text) => styleText("cyan", text);
8
10
  export const ok = styleText("green", "✓");
9
11
  export const no = styleText("yellow", "–");
10
12
  export const bad = styleText("red", "✗");
11
13
  /** A path as the person would write it. */
12
14
  export const tilde = (path) => path.startsWith(homedir()) ? `~${path.slice(homedir().length)}` : path;
15
+ /** A spinner on a terminal; where nobody watches, only what came of it. */
16
+ export function busy(message) {
17
+ if (!process.stdout.isTTY) {
18
+ return { stop: (text) => p.log.step(text), fail: (text) => p.log.warn(text) };
19
+ }
20
+ const spinner = p.spinner();
21
+ spinner.start(message);
22
+ return {
23
+ stop: (text) => spinner.stop(text),
24
+ fail: (text) => spinner.error(text),
25
+ };
26
+ }
@@ -1,10 +1,10 @@
1
1
  ---
2
- title: The assistant
2
+ title: Goose, the assistant
3
3
  description: The chat at the bottom of every studio page. It knows the app, sees the page you are on, shows you where things are and fills the space in.
4
- version: 0.3.2
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
- The assistant stands at the bottom of every page of the studio but the editor, which has a
7
+ Goose, the assistant, stands at the bottom of every page of the studio but the editor, which has a
8
8
  conversation of its own. It thinks on the High class of Text & reasoning: your subscription,
9
9
  your keys or credits ([Models and account](./models.md)).
10
10
 
@@ -26,20 +26,38 @@ a value into a field for you; you check it and save. It does not press buttons.
26
26
  | | |
27
27
  |---|---|
28
28
  | At the bottom | The default. Click the field or the engenty, and the chat rises above it |
29
- | Small | The engenty at the lower right; a click opens the chat beside it. Data and Results keep it small |
29
+ | Closed | ✕ in the header: the engenty at the lower right; a click opens the chat where it last stood. Data and Results start closed |
30
30
  | Sidebar | Beside the page, on a window 1,520 pixels wide or more |
31
31
 
32
32
  The buttons in the chat's header switch between them; the studio remembers your choice.
33
33
 
34
+ At the bottom, the chat moves out of the way of what it covers: drag it by the grip before its
35
+ name (or the engenty, while it is folded). It stays where you put it. Near the bottom it snaps to
36
+ the middle or the right; near the right side, higher up, it becomes the sidebar, and the
37
+ sidebar's grip pulls it out again. A dashed outline shows where it will snap. The handle on top of
38
+ the chat sets how tall the thread is; a click on it folds the chat away.
39
+
40
+ ## Writing while it works
41
+
42
+ You can write on while Goose answers. What you send waits above the field:
43
+
44
+ | | |
45
+ |---|---|
46
+ | Enter | Goose reads it before its next step, and keeps what you asked before unless the message changes it |
47
+ | ⌘↩ (Ctrl+↩) | It goes out as the next question once the answer is done |
48
+ | Send now | Stops the answer and sends it at once |
49
+
50
+ The stop button stands where Send does while the field is empty.
51
+
34
52
  ## One chat
35
53
 
36
54
  You have one chat, the same on every page and in every space, after a reload and on another
37
55
  device. Nobody else in the team sees it.
38
56
 
39
- - **New chat** (the menu at the chat's name) closes the chat with a short summary and starts an
40
- empty one. The assistant keeps the summaries of your last chats in mind.
57
+ - **New chat** (the pen in the header, or the menu at the chat's name) clears the chat at once and
58
+ sums the old one up meanwhile. Goose keeps the summaries of your last chats in mind.
41
59
  - **Earlier chats** lists the closed ones to read. Ask about something you discussed before, and
42
- the assistant searches them.
60
+ Goose searches them.
43
61
 
44
62
  ## Live chat
45
63
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Command line
3
3
  description: The wizards command — start, setup, status, update — and the install's settings file.
4
- version: 0.3.2
4
+ version: 0.3.4
5
5
  ---
6
6
 
7
7
  ## Commands
@@ -11,7 +11,8 @@ version: 0.3.2
11
11
  | `wizards` | What runs, and a menu: open the studio, restart and stop while it runs, "Start as service" or "Start here" (in this terminal) while it does not; update, which restarts what runs into the new version; "Service settings" (start at login); connect AI apps, status, setup, help. The first time, the setup runs first. Needs a terminal |
12
12
  | `wizards start` | Starts it and opens the studio. It runs in that terminal; Ctrl-C stops it. The first time, the setup runs first |
13
13
  | `wizards studio` | Opens the studio in your browser. When it does not run, it starts as a service first |
14
- | `wizards setup` | The guided setup again: an AI client to think with, the browser and ffmpeg, start at login, your AI apps |
14
+ | `wizards setup` | The guided setup again: your account, an AI client to think with, the browser and ffmpeg, start at login, your AI apps. `--local` leaves the account out ([Local only](./local-only.md)) |
15
+ | `wizards account` | How the install stands to your account: signed in as whom, which team, the credits. `sign-in` shows a code and opens the browser; `sign-out` disconnects the install |
15
16
  | `wizards open` | Lets this browser into the running studio. `--print` only shows the link; it works once, within a minute |
16
17
  | `wizards status` | What is installed and what runs, with the browser the app uses for PDF, PNG and websites |
17
18
  | `wizards doctor` | Status, and what is wrong |