@alexkroman1/aai-cli 5.5.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +16 -7
  2. package/dist/{_agent-DMyOab9_.mjs → _agent-2nVugrN3.mjs} +22 -3
  3. package/dist/_agent.d.ts +3 -1
  4. package/dist/_api-client-B-upMGkc.mjs +104 -0
  5. package/dist/_api-client.d.ts +16 -0
  6. package/dist/_cli-common.d.ts +65 -0
  7. package/dist/{_config-5AEqhh-O.mjs → _config-Y5V-5Krn.mjs} +58 -9
  8. package/dist/_config.d.ts +23 -1
  9. package/dist/_deploy.d.ts +7 -0
  10. package/dist/{_dev-server-vV05Fnki.mjs → _dev-server-BB5N8kdh.mjs} +2 -2
  11. package/dist/{_init-BZ9t_Kz-.mjs → _init-D7JIT-IJ.mjs} +3 -3
  12. package/dist/{_slug-api-DaqQJHk8.mjs → _slug-api-CGJSST9B.mjs} +2 -2
  13. package/dist/_studio-commands.d.ts +73 -0
  14. package/dist/_studio.d.ts +64 -0
  15. package/dist/{_templates-Bv8CR800.mjs → _templates-Bt6u9_68.mjs} +19 -8
  16. package/dist/_templates.d.ts +9 -0
  17. package/dist/{_typecheck-gate-9IHWDnl1.mjs → _typecheck-gate-DvE8S3aQ.mjs} +1 -1
  18. package/dist/{build-D_PgQOD4.mjs → build-BXwDB78d.mjs} +1 -1
  19. package/dist/cli.mjs +264 -32
  20. package/dist/delete-DRNfvczK.mjs +53 -0
  21. package/dist/delete.d.ts +9 -2
  22. package/dist/{deploy-1eaXcfUw.mjs → deploy-Cp-wgME3.mjs} +7 -5
  23. package/dist/deploy.d.ts +2 -0
  24. package/dist/{dev-gVNdGFYY.mjs → dev-C4KyxouE.mjs} +1 -1
  25. package/dist/{init-DoU4_txp.mjs → init-BT-IU9AR.mjs} +15 -14
  26. package/dist/login-AA_UdRI-.mjs +172 -0
  27. package/dist/login.d.ts +33 -20
  28. package/dist/scaffold/package.json +4 -4
  29. package/dist/{secret-CGAIAbUx.mjs → secret-Dr0qnxeb.mjs} +1 -1
  30. package/dist/{storage-CnhOayhm.mjs → storage-CoQB8d-u.mjs} +1 -1
  31. package/dist/studio-sXvYUxr5.mjs +325 -0
  32. package/dist/studio.d.ts +54 -0
  33. package/package.json +4 -4
  34. package/dist/_api-client-MenP4-O7.mjs +0 -49
  35. package/dist/delete-DXilFBb1.mjs +0 -29
  36. package/dist/login-C59ZHzuO.mjs +0 -109
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @alexkroman1/aai-cli
2
2
 
3
- The `aai` command — scaffold, run, test, and deploy aai voice agents.
3
+ The `aai` command — scaffold, run, test, and publish aai voice agents.
4
4
 
5
5
  ```sh
6
6
  npm i -g @alexkroman1/aai-cli # or: npx @alexkroman1/aai-cli@latest
@@ -15,7 +15,10 @@ npm i -g @alexkroman1/aai-cli # or: npx @alexkroman1/aai-cli@latest
15
15
  | `aai dev` | Local dev server: loads `agent.ts`, rebuilds on change, serves the browser client (`--port`) |
16
16
  | `aai test` | Run the project's vitest suite |
17
17
  | `aai build` | Bundle without deploying (type-checks first; `--skipTypecheck` opts out) |
18
- | `aai deploy` | Bundle and deploy to the platform (uploads `.env` as agent secrets) |
18
+ | `aai list` | List your studio projects |
19
+ | `aai pull <project>` | Pull a studio project into a local directory, ready for `aai dev` |
20
+ | `aai push` | Sync this project's source to its studio workspace (fast-forward-checked; `--force` overwrites) |
21
+ | `aai publish` | Push, then deploy to production — the studio's Publish button from the terminal (`.env` syncs as agent secrets) |
19
22
  | `aai delete` | Remove a deployed agent |
20
23
  | `aai secret put\|delete\|list` | Manage a deployed agent's secrets |
21
24
  | `aai storage status\|enable\|disable` | Manage the agent's opt-in SQL database (`ctx.db`) |
@@ -30,16 +33,22 @@ aai init my-agent --template pizza-ordering
30
33
  cd my-agent
31
34
  # put ASSEMBLYAI_API_KEY=... in .env
32
35
  aai dev # talk to it at the printed URL
33
- aai deploy # ship it; .env values become the agent's secrets
36
+ aai publish # ship it; .env values become the agent's secrets
34
37
  ```
35
38
 
36
- Deploys type-check the project, preflight required credentials (a missing
37
- provider key fails the deploy instead of the first call), and print the
38
- agent's public URL.
39
+ Every published agent is also a **studio project**: publishing prints a
40
+ studio link where the same source can be edited in the browser (with the
41
+ coding agent), and `aai pull` brings those edits back to your machine.
42
+ `push`/`pull` are fast-forward-only — an edit made in the studio since your
43
+ last pull surfaces as a conflict instead of being overwritten.
44
+
45
+ Publishes type-check the project locally, then build and deploy inside the
46
+ project's sandbox — byte-for-byte the studio's Publish path — preflight
47
+ required credentials, and print the agent's public URL.
39
48
 
40
49
  ## Notes
41
50
 
42
- - A bare `aai` in a project directory offers to deploy (TTY-confirmed);
51
+ - A bare `aai` in a project directory offers to publish (TTY-confirmed);
43
52
  outside one it runs `init`.
44
53
  - `--server <url>` targets a self-hosted platform; the origin is remembered
45
54
  in your user config after explicit approval.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as readProjectConfig, i as readGlobalConfig, n as ensureApiKey, o as serverOrigin, t as approveServer } from "./_config-5AEqhh-O.mjs";
2
+ import { a as readProjectConfig, i as readGlobalConfig, n as ensureApiKey, o as serverOrigin, t as approveServer } from "./_config-Y5V-5Krn.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -67,6 +67,25 @@ function resolveServerUrl(explicit, configUrl, approvedOrigins = []) {
67
67
  If you do intend to use that server, re-run with --server ${origin} to approve it.`);
68
68
  }
69
69
  /**
70
+ * Reject a repo-supplied slug that isn't the platform's slug shape.
71
+ *
72
+ * Enforced before a slug is ever interpolated into a URL path.
73
+ * `.aai/project.json` is part of the working tree, so a cloned repo controls
74
+ * this value, and callers pair it with the user's API key — `aai publish`
75
+ * hands it to `syncEnvSecrets`, which PUTs the whole `.env` to
76
+ * `${serverUrl}/${slug}/secret`. A hostile `"slug": "x/../admin"` must not
77
+ * steer that request to a path of the repo's choosing.
78
+ *
79
+ * Lives here, at the single point where repo-controlled config becomes a
80
+ * credentialed target, rather than at each call site: the check used to
81
+ * exist only in `getServerInfo` (secret/storage/delete), so `publish` — the
82
+ * command users actually run — had no guard at all.
83
+ */
84
+ function assertValidConfigSlug(slug) {
85
+ if (slug === void 0 || VALID_SLUG_RE.test(slug)) return;
86
+ throw new Error(`Invalid slug in .aai/project.json: ${JSON.stringify(slug)}\n Expected lowercase letters, digits, \`-\`, \`_\` (2-64 chars). Fix or delete the file — \`aai publish\` will create a fresh deployment.`);
87
+ }
88
+ /**
70
89
  * Resolve everything needed to talk to the platform: project config (null if
71
90
  * the project has never been deployed), server URL, and API key.
72
91
  */
@@ -74,6 +93,7 @@ async function resolveDeployTarget(cwd, explicitServer) {
74
93
  const [config, globalConfig] = await Promise.all([readProjectConfig(cwd), readGlobalConfig()]);
75
94
  const serverUrl = resolveServerUrl(explicitServer, config?.serverUrl, globalConfig.approvedServers ?? []);
76
95
  if (explicitServer) await approveServer(serverUrl);
96
+ assertValidConfigSlug(config?.slug);
77
97
  return {
78
98
  config,
79
99
  serverUrl,
@@ -83,8 +103,7 @@ async function resolveDeployTarget(cwd, explicitServer) {
83
103
  /** Like resolveDeployTarget, but requires an existing deployment (project config). */
84
104
  async function getServerInfo(cwd, explicitServer) {
85
105
  const { config, serverUrl, apiKey } = await resolveDeployTarget(cwd, explicitServer);
86
- if (!config) throw new Error("No .aai/project.json found — run `aai deploy` first");
87
- if (!VALID_SLUG_RE.test(config.slug)) throw new Error(`Invalid slug in .aai/project.json: ${JSON.stringify(config.slug)}\n Expected lowercase letters, digits, \`-\`, \`_\` (2-64 chars). Fix the file or run \`aai deploy\` to create a fresh deployment.`);
106
+ if (!config?.slug) throw new Error("This project has no deployed agent — run `aai publish` first");
88
107
  return {
89
108
  serverUrl,
90
109
  slug: config.slug,
package/dist/_agent.d.ts CHANGED
@@ -25,8 +25,10 @@ export declare function resolveServerUrl(explicit?: string, configUrl?: string,
25
25
  */
26
26
  export declare function resolveDeployTarget(cwd: string, explicitServer?: string): Promise<{
27
27
  config: {
28
- slug: string;
28
+ slug?: string | undefined;
29
29
  serverUrl: string;
30
+ studioProject?: string | undefined;
31
+ studioSourceHash?: string | undefined;
30
32
  } | null;
31
33
  serverUrl: string;
32
34
  apiKey: string;
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { FetchError, ofetch } from "ofetch";
3
+ //#region _api-client.ts
4
+ /**
5
+ * Shared HTTP helper for platform API calls (deploy, delete, secrets).
6
+ *
7
+ * Built on ofetch: JSON bodies are serialized (with Content-Type set) and
8
+ * responses parsed automatically, and transient failures (network errors,
9
+ * 5xx/429) are retried before surfacing an error.
10
+ */
11
+ const HINT_INVALID_API_KEY = "Your API key may be invalid. Run `aai` to re-enter your AssemblyAI API key.";
12
+ /** 404 hint for requests scoped to a deployed agent's slug. */
13
+ const HINT_NOT_DEPLOYED = "The agent may not be deployed. Check `.aai/project.json` for the correct slug.";
14
+ /**
15
+ * Send an authenticated request to the platform API and return the parsed
16
+ * JSON response. Throws a descriptive error with status-specific hints on
17
+ * failure (the 401 hint is always included; pass more via `hints`).
18
+ */
19
+ async function apiRequest(url, opts) {
20
+ const client = opts.fetch ? ofetch.create({}, { fetch: opts.fetch }) : ofetch;
21
+ try {
22
+ return await client(url, {
23
+ method: opts.method ?? "GET",
24
+ headers: {
25
+ Authorization: `Bearer ${opts.apiKey}`,
26
+ ...opts.headers
27
+ },
28
+ ...opts.body !== void 0 ? { body: opts.body } : {},
29
+ retry: opts.retry ?? 2,
30
+ retryDelay: opts.retryDelay ?? 300
31
+ });
32
+ } catch (err) {
33
+ if (opts.allow404 && err instanceof FetchError && err.statusCode === 404) return null;
34
+ throw toApiError(err, url, opts);
35
+ }
36
+ }
37
+ /**
38
+ * Collapse `{"error": "..."}` payloads embedded in a message into their own
39
+ * text. Studio Publish runs the real `aai deploy` inside the sandbox, so its
40
+ * failures arrive wrapped twice and stringifying them produced a
41
+ * triple-escaped wall of JSON around one actionable sentence.
42
+ */
43
+ function unwrapEmbeddedErrors(message, depth = 0) {
44
+ if (depth > 3) return message;
45
+ const start = message.indexOf("{\"error\"");
46
+ if (start === -1) return message;
47
+ const json = message.slice(start);
48
+ try {
49
+ const inner = JSON.parse(json).error;
50
+ if (typeof inner !== "string") return message;
51
+ return unwrapEmbeddedErrors(message.slice(0, start) + inner, depth + 1);
52
+ } catch {
53
+ return message;
54
+ }
55
+ }
56
+ /** The messages of a Zod issue tree, deduped and flattened. */
57
+ function zodIssueMessages(value) {
58
+ if (Array.isArray(value)) return value.flatMap(zodIssueMessages);
59
+ if (value === null || typeof value !== "object") return [];
60
+ const node = value;
61
+ const nested = zodIssueMessages(node.issues);
62
+ if (nested.length > 0) return nested;
63
+ return typeof node.message === "string" ? [node.message] : [];
64
+ }
65
+ /**
66
+ * A human-readable one-liner for a server error body.
67
+ *
68
+ * Servers answer with `{ error }`, or with a serialized ZodError whose useful
69
+ * part is buried several levels down. Dumping the raw JSON turned a
70
+ * one-character mistake (`aai secret put MY-KEY`) into a 515-character escaped
71
+ * blob, so the shapes we actually emit are unwrapped here and anything else
72
+ * falls back to the raw body rather than being dropped.
73
+ */
74
+ function describeErrorBody(data) {
75
+ if (typeof data === "string") return data;
76
+ if (data === null || typeof data !== "object") return JSON.stringify(data ?? "");
77
+ const error = data.error;
78
+ if (typeof error === "string") return unwrapEmbeddedErrors(error);
79
+ if (error !== null && typeof error === "object") {
80
+ const { message } = error;
81
+ if (typeof message === "string") {
82
+ try {
83
+ const issues = zodIssueMessages(JSON.parse(message));
84
+ if (issues.length > 0) return [...new Set(issues)].join("; ");
85
+ } catch {}
86
+ return message;
87
+ }
88
+ }
89
+ return JSON.stringify(data);
90
+ }
91
+ /** Format an ofetch failure into a descriptive, action-centric error. */
92
+ function toApiError(err, url, opts) {
93
+ if (err instanceof FetchError && err.statusCode !== void 0) {
94
+ const status = err.statusCode;
95
+ const body = describeErrorBody(err.data);
96
+ const hint = status === 401 ? HINT_INVALID_API_KEY : opts.hints?.[status];
97
+ return /* @__PURE__ */ new Error(`${opts.action} failed (HTTP ${status}): ${body}${hint ? `\n ${hint}` : ""}`);
98
+ }
99
+ const hint = "Check your network connection and verify the server URL is correct.";
100
+ const cause = err instanceof FetchError && err.cause !== void 0 ? err.cause : err;
101
+ return new Error(`${opts.action} failed: could not reach ${url}\n ${hint}`, { cause });
102
+ }
103
+ //#endregion
104
+ export { apiRequest as n, HINT_NOT_DEPLOYED as t };
@@ -28,6 +28,12 @@ export type ApiRequestOptions = {
28
28
  retryDelay?: number;
29
29
  /** Optional fetch implementation for testing. Defaults to globalThis.fetch. */
30
30
  fetch?: typeof globalThis.fetch;
31
+ /**
32
+ * Resolve `null` on a 404 instead of throwing — for existence probes
33
+ * ("is there a studio project with this name?") where absence is an
34
+ * answer, not a failure.
35
+ */
36
+ allow404?: boolean;
31
37
  };
32
38
  /**
33
39
  * Send an authenticated request to the platform API and return the parsed
@@ -35,3 +41,13 @@ export type ApiRequestOptions = {
35
41
  * failure (the 401 hint is always included; pass more via `hints`).
36
42
  */
37
43
  export declare function apiRequest<T = unknown>(url: string, opts: ApiRequestOptions): Promise<T>;
44
+ /**
45
+ * A human-readable one-liner for a server error body.
46
+ *
47
+ * Servers answer with `{ error }`, or with a serialized ZodError whose useful
48
+ * part is buried several levels down. Dumping the raw JSON turned a
49
+ * one-character mistake (`aai secret put MY-KEY`) into a 515-character escaped
50
+ * blob, so the shapes we actually emit are unwrapped here and anything else
51
+ * falls back to the raw body rather than being dropped.
52
+ */
53
+ export declare function describeErrorBody(data: unknown): string;
@@ -0,0 +1,65 @@
1
+ import type { ArgsDef, CommandDef } from "citty";
2
+ import { type CommandResult, type OutputMode } from "./_output.ts";
3
+ /** Shared arg definitions for citty commands. */
4
+ export declare const sharedArgs: {
5
+ readonly server: {
6
+ readonly type: "string";
7
+ readonly alias: "s";
8
+ readonly description: "Platform server URL";
9
+ };
10
+ readonly yes: {
11
+ readonly type: "boolean";
12
+ readonly alias: "y";
13
+ readonly description: "Accept defaults (no prompts)";
14
+ };
15
+ readonly json: {
16
+ readonly type: "boolean";
17
+ readonly description: "Output JSON (auto-detected in non-TTY)";
18
+ };
19
+ };
20
+ /**
21
+ * Flags in `rawArgs` that `argsDef` doesn't declare, in the form the user
22
+ * typed them.
23
+ *
24
+ * citty silently drops an unrecognized flag, so `aai push --serverr=http://x`
25
+ * exited 0 having pushed to the DEFAULT server — production, for an installed
26
+ * CLI — as if the flag had been honoured. Since `--server` is what decides
27
+ * where the API key and secret values are sent, a typo quietly retargeting it
28
+ * is worth failing on.
29
+ */
30
+ export declare function findUnknownFlags(rawArgs: string[], argsDef: ArgsDef): string[];
31
+ /**
32
+ * Any command in the tree, regardless of its args shape — the walk below only
33
+ * reads `subCommands` and `args`, and the concrete generics differ per command.
34
+ */
35
+ type AnyCommandDef = CommandDef<ArgsDef>;
36
+ /**
37
+ * Unknown flags in `argv` for whichever (possibly nested) subcommand it
38
+ * selects — `[]` when everything is declared.
39
+ *
40
+ * Walks the real command tree rather than re-listing flags, so this cannot
41
+ * drift from what the commands accept. An unknown SUBCOMMAND is not reported:
42
+ * citty already answers that with usage text and a non-zero exit.
43
+ */
44
+ export declare function unknownFlagsForArgv(root: AnyCommandDef, argv: string[]): Promise<string[]>;
45
+ /** Shared command setup: resolve cwd, optionally require agent.ts. */
46
+ export declare function setup(opts?: {
47
+ agent?: boolean;
48
+ }): Promise<string>;
49
+ /**
50
+ * Run a command body with standard output-mode resolution, error handling,
51
+ * and result emission.
52
+ *
53
+ * - API key acquisition is owned by `resolveDeployTarget`/`getServerInfo`
54
+ * inside the commands that talk to the platform — after the server-trust
55
+ * check, so an untrusted `serverUrl` is refused without prompting for a
56
+ * key, and commands with no platform traffic never prompt at all.
57
+ *
58
+ * A thrown error and a returned `fail(...)` converge here on one emitter:
59
+ * human mode logs the message, JSON mode writes exactly one result line,
60
+ * and both exit 1.
61
+ */
62
+ export declare function runCommand(args: {
63
+ json?: boolean | undefined;
64
+ }, fn: (mode: OutputMode) => Promise<CommandResult<unknown>>): Promise<void>;
65
+ export {};
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, o as CliError } from "./_ui-8kOEB-JH.mjs";
2
+ import { n as log, o as CliError } from "./_ui-8kOEB-JH.mjs";
3
3
  import { a as errorMessage, c as readJson, d as writeJson } from "./_utils-Ch0J4s6a.mjs";
4
+ import { mkdtempSync } from "node:fs";
4
5
  import path from "node:path";
5
- import * as p from "@clack/prompts";
6
+ import { tmpdir } from "node:os";
6
7
  import envPaths from "env-paths";
7
8
  import { z } from "zod";
8
9
  //#region _config.ts
@@ -18,8 +19,17 @@ import { z } from "zod";
18
19
  * that isn't an approved http(s) origin.
19
20
  */
20
21
  const ProjectConfigSchema = z.object({
21
- slug: z.string(),
22
- serverUrl: z.string()
22
+ /** Deployed agent slug — absent for a pulled project never published. */
23
+ slug: z.string().optional(),
24
+ serverUrl: z.string(),
25
+ /** Studio project this directory is linked to (`aai pull`/`aai push`). */
26
+ studioProject: z.string().optional(),
27
+ /**
28
+ * The workspace files hash at the last pull/push — `aai push` sends it
29
+ * back as the fast-forward token, so an edit made in the studio since
30
+ * then surfaces as a 409 instead of being silently overwritten.
31
+ */
32
+ studioSourceHash: z.string().optional()
23
33
  });
24
34
  /**
25
35
  * Resolve the global config directory (the platform-conventional env-paths
@@ -33,8 +43,19 @@ const ProjectConfigSchema = z.object({
33
43
  function getConfigDir() {
34
44
  const override = process.env.AAI_CONFIG_DIR?.trim();
35
45
  if (override) return override;
46
+ if (process.env.VITEST) return testConfigDir();
36
47
  return envPaths("aai", { suffix: "" }).config;
37
48
  }
49
+ /**
50
+ * Per-process throwaway config dir used only under vitest. Memoized: callers
51
+ * read-modify-write the same config across calls, so a fresh dir per call
52
+ * would silently drop what the previous one wrote.
53
+ */
54
+ let _testConfigDir;
55
+ function testConfigDir() {
56
+ _testConfigDir ??= mkdtempSync(path.join(tmpdir(), "aai-vitest-config-"));
57
+ return _testConfigDir;
58
+ }
38
59
  async function readProjectConfig(agentDir) {
39
60
  const file = path.join(agentDir, ".aai", "project.json");
40
61
  let data;
@@ -53,6 +74,23 @@ async function writeProjectConfig(agentDir, data) {
53
74
  await writeJson(path.join(agentDir, ".aai", "project.json"), data);
54
75
  }
55
76
  /**
77
+ * Merge `patch` into the existing project config rather than replacing the
78
+ * file — a publish recording its `slug` must not drop the studio link
79
+ * fields a pull wrote, and vice versa.
80
+ */
81
+ async function updateProjectConfig(agentDir, patch) {
82
+ let existing = null;
83
+ try {
84
+ existing = await readProjectConfig(agentDir);
85
+ } catch {}
86
+ const merged = {
87
+ ...existing,
88
+ ...patch
89
+ };
90
+ await writeProjectConfig(agentDir, merged);
91
+ return merged;
92
+ }
93
+ /**
56
94
  * Origin of `url`, or `null` when it is not an absolute http(s) URL.
57
95
  *
58
96
  * Non-HTTP schemes are rejected rather than returned: `new URL()` yields the
@@ -106,6 +144,20 @@ async function trySaveApiKey(dir, config, apiKey) {
106
144
  log.warn(`Couldn't save your API key to ${path.join(dir, "config.json")}: ${errorMessage(err)} — you'll be prompted again next run.`);
107
145
  }
108
146
  }
147
+ /**
148
+ * The credential every platform command runs on.
149
+ *
150
+ * Two sources, in order: the key `aai login` saved, then
151
+ * `ASSEMBLYAI_API_KEY` for non-interactive callers (CI, scripts, the eval
152
+ * harnesses).
153
+ *
154
+ * There is deliberately NO "paste a key" prompt. Pasting one produced a
155
+ * half-configured CLI — able to push and publish while linked to no account
156
+ * the user could see in the studio — and it made `aai login`, which is the
157
+ * real onboarding path, optional in practice. It was also the riskier code
158
+ * path: a hidden password prompt reads stdin, so a piped invocation could
159
+ * have its input eaten and persisted as the API key.
160
+ */
109
161
  async function ensureApiKey(configDir) {
110
162
  const dir = configDir ?? getConfigDir();
111
163
  const config = await readGlobalConfig(dir);
@@ -115,10 +167,7 @@ async function ensureApiKey(configDir) {
115
167
  await trySaveApiKey(dir, config, envKey);
116
168
  return envKey;
117
169
  }
118
- if (!process.stdin.isTTY) throw new CliError("no_api_key", "No API key configured and no TTY to prompt for one.", "Set the ASSEMBLYAI_API_KEY environment variable, or run `aai login` interactively once to save a key.");
119
- const apiKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key" }), "Setup cancelled");
120
- await trySaveApiKey(dir, config, apiKey);
121
- return apiKey;
170
+ throw new CliError("not_logged_in", "You're not logged in.", "Run `aai login` to link your account, or set ASSEMBLYAI_API_KEY for non-interactive use.");
122
171
  }
123
172
  //#endregion
124
- export { readProjectConfig as a, writeProjectConfig as c, readGlobalConfig as i, ensureApiKey as n, serverOrigin as o, getConfigDir as r, writeGlobalConfig as s, approveServer as t };
173
+ export { readProjectConfig as a, writeGlobalConfig as c, readGlobalConfig as i, writeProjectConfig as l, ensureApiKey as n, serverOrigin as o, getConfigDir as r, updateProjectConfig as s, approveServer as t };
package/dist/_config.d.ts CHANGED
@@ -11,8 +11,10 @@ import { z } from "zod";
11
11
  * that isn't an approved http(s) origin.
12
12
  */
13
13
  declare const ProjectConfigSchema: z.ZodObject<{
14
- slug: z.ZodString;
14
+ slug: z.ZodOptional<z.ZodString>;
15
15
  serverUrl: z.ZodString;
16
+ studioProject: z.ZodOptional<z.ZodString>;
17
+ studioSourceHash: z.ZodOptional<z.ZodString>;
16
18
  }, z.core.$strip>;
17
19
  /**
18
20
  * Resolve the global config directory (the platform-conventional env-paths
@@ -27,6 +29,12 @@ export declare function getConfigDir(): string;
27
29
  export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
28
30
  export declare function readProjectConfig(agentDir: string): Promise<ProjectConfig | null>;
29
31
  export declare function writeProjectConfig(agentDir: string, data: ProjectConfig): Promise<void>;
32
+ /**
33
+ * Merge `patch` into the existing project config rather than replacing the
34
+ * file — a publish recording its `slug` must not drop the studio link
35
+ * fields a pull wrote, and vice versa.
36
+ */
37
+ export declare function updateProjectConfig(agentDir: string, patch: Partial<ProjectConfig> & Pick<ProjectConfig, "serverUrl">): Promise<ProjectConfig>;
30
38
  export type GlobalConfig = {
31
39
  apiKey?: string;
32
40
  /**
@@ -53,5 +61,19 @@ export declare function serverOrigin(url: string): string | null;
53
61
  export declare function approveServer(url: string, configDir?: string): Promise<void>;
54
62
  export declare function readGlobalConfig(configDir?: string): Promise<GlobalConfig>;
55
63
  export declare function writeGlobalConfig(configDir: string, data: GlobalConfig): Promise<void>;
64
+ /**
65
+ * The credential every platform command runs on.
66
+ *
67
+ * Two sources, in order: the key `aai login` saved, then
68
+ * `ASSEMBLYAI_API_KEY` for non-interactive callers (CI, scripts, the eval
69
+ * harnesses).
70
+ *
71
+ * There is deliberately NO "paste a key" prompt. Pasting one produced a
72
+ * half-configured CLI — able to push and publish while linked to no account
73
+ * the user could see in the studio — and it made `aai login`, which is the
74
+ * real onboarding path, optional in practice. It was also the riskier code
75
+ * path: a hidden password prompt reads stdin, so a piped invocation could
76
+ * have its input eaten and persisted as the API key.
77
+ */
56
78
  export declare function ensureApiKey(configDir?: string): Promise<string>;
57
79
  export {};
package/dist/_deploy.d.ts CHANGED
@@ -15,6 +15,13 @@ export type DeployOpts = {
15
15
  * panel needs a deployed slug to attach secrets to).
16
16
  */
17
17
  allowMissingSecrets?: boolean;
18
+ /**
19
+ * Ask the server to permit a `-preview`-suffixed slug (`aai deploy
20
+ * --allow-preview-slug`). That suffix is reserved for the studio's
21
+ * auto-preview deploys — the server rejects it otherwise — and this is set
22
+ * by the studio's own in-guest deploy, not by ordinary users.
23
+ */
24
+ allowPreviewSlug?: boolean;
18
25
  /** Retry delay override for tests (0 = no real sleeps on retry paths). */
19
26
  retryDelay?: number;
20
27
  /** Optional fetch implementation for testing. Defaults to globalThis.fetch. */
@@ -4,7 +4,7 @@ import { a as errorMessage, r as errorCode } from "./_utils-Ch0J4s6a.mjs";
4
4
  import { n as fallbackHtmlPlugin } from "./client-bundler-yiWoXrgb.mjs";
5
5
  import { buildWorker } from "./worker-bundler.mjs";
6
6
  import { n as createWorkerEvaluator } from "./_bundler-Cjaxa2wi.mjs";
7
- import { n as ensureApiKey } from "./_config-5AEqhh-O.mjs";
7
+ import { n as ensureApiKey } from "./_config-Y5V-5Krn.mjs";
8
8
  import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { existsSync } from "node:fs";
@@ -44,7 +44,7 @@ function agentEnvWarnings(agentDef, env, shellEnv = process.env) {
44
44
  const missing = required.filter((name) => !(env[name] || shellEnv[name]));
45
45
  if (missing.length > 0) warnings.push(`Missing provider credential${s(missing)}: ${missing.join(", ")}. Set ${them(missing)} in .env or the environment.`);
46
46
  const shellOnly = required.filter((name) => !env[name] && shellEnv[name]);
47
- if (shellOnly.length > 0) warnings.push(`${shellOnly.join(", ")} resolved from your shell, not .env — deployed agents won't have ${them(shellOnly)}. Declare ${them(shellOnly)} in .env before \`aai deploy\`.`);
47
+ if (shellOnly.length > 0) warnings.push(`${shellOnly.join(", ")} resolved from your shell, not .env — deployed agents won't have ${them(shellOnly)}. Declare ${them(shellOnly)} in .env before \`aai publish\`.`);
48
48
  const declared = (agentDef.requiredEnv ?? []).filter((name) => !env[name]);
49
49
  if (declared.length > 0) warnings.push(`Missing requiredEnv key${s(declared)} declared by the agent: ${declared.join(", ")}. Set ${them(declared)} in .env — ctx.env will not contain ${them(declared)} otherwise.`);
50
50
  return warnings;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { c as readJson, d as writeJson, s as isEexist } from "./_utils-Ch0J4s6a.mjs";
3
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-DMyOab9_.mjs";
4
- import { REPO_URL, downloadAndMergeTemplate } from "./_templates-Bv8CR800.mjs";
3
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-2nVugrN3.mjs";
4
+ import { REPO_URL, downloadAndMergeTemplate } from "./_templates-Bt6u9_68.mjs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  //#region _init.ts
@@ -15,7 +15,7 @@ A voice agent built with [aai](${REPO_URL}).
15
15
  \`\`\`sh
16
16
  npm install # Install dependencies
17
17
  aai dev # Run locally (opens browser)
18
- aai deploy # Deploy to production
18
+ aai publish # Publish to production (and sync to the studio)
19
19
  \`\`\`
20
20
 
21
21
  ## Secrets
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as getServerInfo } from "./_agent-DMyOab9_.mjs";
3
- import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-MenP4-O7.mjs";
2
+ import { n as getServerInfo } from "./_agent-2nVugrN3.mjs";
3
+ import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B-upMGkc.mjs";
4
4
  //#region _slug-api.ts
5
5
  /**
6
6
  * Authenticated request against a deployed agent's slug-scoped resource
@@ -0,0 +1,73 @@
1
+ export declare const list: import("citty").CommandDef<{
2
+ readonly server: {
3
+ readonly type: "string";
4
+ readonly alias: "s";
5
+ readonly description: "Platform server URL";
6
+ };
7
+ readonly json: {
8
+ readonly type: "boolean";
9
+ readonly description: "Output JSON (auto-detected in non-TTY)";
10
+ };
11
+ }>;
12
+ export declare const pull: import("citty").CommandDef<{
13
+ readonly project: {
14
+ readonly type: "positional";
15
+ readonly description: "Studio project name (see `aai list`)";
16
+ readonly required: true;
17
+ };
18
+ readonly dir: {
19
+ readonly type: "positional";
20
+ readonly description: "Target directory (default: the project name)";
21
+ readonly required: false;
22
+ };
23
+ readonly force: {
24
+ readonly type: "boolean";
25
+ readonly alias: "f";
26
+ readonly description: "Overwrite files in a non-empty directory";
27
+ };
28
+ readonly server: {
29
+ readonly type: "string";
30
+ readonly alias: "s";
31
+ readonly description: "Platform server URL";
32
+ };
33
+ readonly json: {
34
+ readonly type: "boolean";
35
+ readonly description: "Output JSON (auto-detected in non-TTY)";
36
+ };
37
+ }>;
38
+ export declare const push: import("citty").CommandDef<{
39
+ readonly force: {
40
+ readonly type: "boolean";
41
+ readonly alias: "f";
42
+ readonly description: "Overwrite studio-side changes instead of failing the fast-forward check";
43
+ };
44
+ readonly server: {
45
+ readonly type: "string";
46
+ readonly alias: "s";
47
+ readonly description: "Platform server URL";
48
+ };
49
+ readonly json: {
50
+ readonly type: "boolean";
51
+ readonly description: "Output JSON (auto-detected in non-TTY)";
52
+ };
53
+ }>;
54
+ export declare const publish: import("citty").CommandDef<{
55
+ readonly force: {
56
+ readonly type: "boolean";
57
+ readonly alias: "f";
58
+ readonly description: "Overwrite studio-side changes instead of failing the fast-forward check";
59
+ };
60
+ readonly server: {
61
+ readonly type: "string";
62
+ readonly alias: "s";
63
+ readonly description: "Platform server URL";
64
+ };
65
+ readonly json: {
66
+ readonly type: "boolean";
67
+ readonly description: "Output JSON (auto-detected in non-TTY)";
68
+ };
69
+ readonly skipTypecheck: {
70
+ readonly type: "boolean";
71
+ readonly description: "Skip type checking before publishing";
72
+ };
73
+ }>;