@layers/amba 4.1.8 → 4.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,21 @@ fetches the canonical prompt from
42
42
  falls back to an inlined snapshot when offline. Use `--no-skills` to
43
43
  skip the skill install.
44
44
 
45
+ ### Invite-only signups
46
+
47
+ While new accounts are invite-only, pass your invite code:
48
+
49
+ ```bash
50
+ npx @layers/amba init --invite <code-from-your-invite-email>
51
+ ```
52
+
53
+ `AMBA_INVITE_CODE` works too; the flag wins when both are set. Without
54
+ a valid code, init stops with `INVITE_REQUIRED`, `INVITE_INVALID` or
55
+ `SIGNUPS_CLOSED` and prints where to request access
56
+ (<https://app.amba.dev/request-access>). The invite code arrives by
57
+ email. With `--json`, the failure is a JSON document on stdout carrying
58
+ `error.code`.
59
+
45
60
  ## Configure + first call
46
61
 
47
62
  ```bash
@@ -23,6 +23,37 @@
23
23
  * Project is resolved via `loadProjectConfig` (AMBA_PROJECT_ID env, then
24
24
  * .env / .env.local in the cwd) — same pattern as `amba secrets *`.
25
25
  */
26
+ /** Database limits block (free-tier compute + storage caps). */
27
+ interface DatabaseLimits {
28
+ limits_enforced: boolean;
29
+ source?: 'tier' | 'override' | 'unmanaged';
30
+ compute: {
31
+ max_cu: number;
32
+ used_cu_hours: number | null;
33
+ limit_cu_hours: number | null;
34
+ pct: number | null;
35
+ };
36
+ storage: {
37
+ used_mb: number | null;
38
+ limit_mb: number | null;
39
+ pct: number | null;
40
+ };
41
+ period: {
42
+ start: string;
43
+ resets_at: string;
44
+ } | null;
45
+ limit_reached: ('database_compute' | 'database_storage')[];
46
+ observed_at: string | null;
47
+ upgrade?: {
48
+ cli?: string;
49
+ mcp_tool?: string;
50
+ } | null;
51
+ }
52
+ /**
53
+ * Print the database limits block. Exported for `amba status`'s one-line
54
+ * summary + tests.
55
+ */
56
+ export declare function formatDatabaseLimits(db: DatabaseLimits): string[];
26
57
  export declare function billingStatusCommand(): Promise<void>;
27
58
  export declare function billingUpgradeCommand(input: {
28
59
  tier: 'pro' | 'scale';
@@ -32,3 +63,4 @@ export declare function billingPortalCommand(): Promise<void>;
32
63
  export declare function billingSetCeilingCommand(input: {
33
64
  ceiling: number | null;
34
65
  }): Promise<void>;
66
+ export {};
@@ -21,6 +21,12 @@ export interface InitOptions {
21
21
  sandbox?: boolean;
22
22
  /** Override the auto-generated sandbox email. Only meaningful with `--sandbox`. */
23
23
  sandboxEmail?: string;
24
+ /**
25
+ * Platform invite code (`--invite`) sent with the sandbox signup when
26
+ * new accounts are invite-only. Falls back to `AMBA_INVITE_CODE`.
27
+ * Passing the flag selects the headless signup path.
28
+ */
29
+ inviteCode?: string;
24
30
  /** Skip MCP-client-config writes. Mostly for testing. */
25
31
  noMcpConfig?: boolean;
26
32
  /**
@@ -38,6 +44,8 @@ export interface InitOptions {
38
44
  export declare function initCommand(options?: InitOptions): Promise<void>;
39
45
  export declare function runSandboxInit(cwd: string, options: {
40
46
  sandboxEmail?: string;
47
+ /** Platform invite code for a fresh signup; falls back to `AMBA_INVITE_CODE`. */
48
+ inviteCode?: string;
41
49
  noMcpConfig?: boolean;
42
50
  /**
43
51
  * Skip the project-local skill install. Default is to install. Set
@@ -52,6 +60,11 @@ export declare function runSandboxInit(cwd: string, options: {
52
60
  */
53
61
  homeDir?: string;
54
62
  }): Promise<SandboxResult>;
63
+ /**
64
+ * `--json` failure document. A gated signup carries its 403 code plus
65
+ * where to request access; every other failure reports `INIT_FAILED`.
66
+ */
67
+ export declare function sandboxErrorToJson(err: unknown): Record<string, unknown>;
55
68
  /**
56
69
  * Build the plaintext (no ANSI) success-output block printed at the
57
70
  * end of `amba init --sandbox`. Pure function — exported for the
@@ -148,6 +148,11 @@ export interface EnsureDeveloperOptions {
148
148
  * identity is reused.
149
149
  */
150
150
  sandboxEmail?: string;
151
+ /**
152
+ * Platform invite code for the sandbox signup (`--invite`). Falls back
153
+ * to `AMBA_INVITE_CODE`. Ignored when an existing identity is reused.
154
+ */
155
+ inviteCode?: string;
151
156
  }
152
157
  export interface EnsureDeveloperResult {
153
158
  credentials: DeveloperCredentials;
package/dist/index.js CHANGED
@@ -1326,6 +1326,64 @@ function generateSandboxPassword() {
1326
1326
  }
1327
1327
  /** Request header carrying AMBA_SIGNUP_TOKEN on the signup call. */
1328
1328
  const SIGNUP_TOKEN_HEADER = "X-Amba-Signup-Token";
1329
+ /** Environment variable read when `--invite` is absent. */
1330
+ const INVITE_CODE_ENV = "AMBA_INVITE_CODE";
1331
+ /** The 403 codes signup answers with when the platform is gated. */
1332
+ const SIGNUP_ACCESS_ERROR_CODES = [
1333
+ "SIGNUPS_CLOSED",
1334
+ "INVITE_REQUIRED",
1335
+ "INVITE_INVALID"
1336
+ ];
1337
+ const DEFAULT_REQUEST_ACCESS_URL = `${CONSOLE_URL}/request-access`;
1338
+ const REQUEST_ACCESS_ENDPOINT = `POST /v1/auth/developer/waitlist`;
1339
+ const RERUN_WITH_INVITE = "npx @layers/amba init --invite <code>";
1340
+ function isSignupAccessErrorCode(code) {
1341
+ return SIGNUP_ACCESS_ERROR_CODES.includes(code);
1342
+ }
1343
+ /**
1344
+ * The flag value when set, else `AMBA_INVITE_CODE`, else undefined. Blank
1345
+ * values count as absent so an empty env var never sends `invite_code: ""`.
1346
+ */
1347
+ function resolveInviteCode(flag) {
1348
+ const fromFlag = flag?.trim();
1349
+ if (fromFlag) return fromFlag;
1350
+ return process.env["AMBA_INVITE_CODE"]?.trim() || void 0;
1351
+ }
1352
+ /**
1353
+ * Ordered next steps for a gated signup, phrased for whoever runs the CLI
1354
+ * (a developer or their coding agent).
1355
+ */
1356
+ function signupAccessNextSteps(code, requestAccessUrl) {
1357
+ const requestAccess = `Request access at ${requestAccessUrl}; the invite code arrives by email.`;
1358
+ switch (code) {
1359
+ case "INVITE_REQUIRED": return [`Have an invite code? Rerun \`${RERUN_WITH_INVITE}\` (or set ${INVITE_CODE_ENV}).`, `No code yet? ${requestAccess}`];
1360
+ case "INVITE_INVALID": return [`Check the code and rerun \`${RERUN_WITH_INVITE}\` (or correct ${INVITE_CODE_ENV}).`, `Need a fresh code? ${requestAccess}`];
1361
+ case "SIGNUPS_CLOSED": return [`${requestAccess} Rerun \`${RERUN_WITH_INVITE}\` once it lands.`];
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Thrown when signup answers 403 SIGNUPS_CLOSED / INVITE_REQUIRED /
1366
+ * INVITE_INVALID. `message` carries the server's sentence plus the next
1367
+ * steps; the structured fields feed `amba init --json`.
1368
+ */
1369
+ var SignupAccessError = class extends Error {
1370
+ code;
1371
+ serverMessage;
1372
+ requestAccessUrl;
1373
+ requestAccessEndpoint;
1374
+ nextSteps;
1375
+ constructor(input) {
1376
+ const requestAccessUrl = input.requestAccessUrl ?? DEFAULT_REQUEST_ACCESS_URL;
1377
+ const nextSteps = signupAccessNextSteps(input.code, requestAccessUrl);
1378
+ super([`Sandbox signup failed (${input.code}): ${input.serverMessage}`, ...nextSteps].join("\n "));
1379
+ this.name = "SignupAccessError";
1380
+ this.code = input.code;
1381
+ this.serverMessage = input.serverMessage;
1382
+ this.requestAccessUrl = requestAccessUrl;
1383
+ this.requestAccessEndpoint = input.requestAccessEndpoint ?? REQUEST_ACCESS_ENDPOINT;
1384
+ this.nextSteps = nextSteps;
1385
+ }
1386
+ };
1329
1387
  /**
1330
1388
  * POST the synthesized credentials at the public signup endpoint.
1331
1389
  *
@@ -1337,7 +1395,8 @@ const SIGNUP_TOKEN_HEADER = "X-Amba-Signup-Token";
1337
1395
  * Returns the unwrapped, flattened shape consumed by the rest of the
1338
1396
  * sandbox flow. Throws with a human-readable message on any non-2xx so
1339
1397
  * the CLI's `runAction` wrapper can surface it without crashing on a
1340
- * generic 'fetch failed'.
1398
+ * generic 'fetch failed'. A gated platform (403 SIGNUPS_CLOSED /
1399
+ * INVITE_REQUIRED / INVITE_INVALID) throws `SignupAccessError`.
1341
1400
  */
1342
1401
  async function performSandboxSignup(req, options = {}) {
1343
1402
  const apiUrl = options.apiUrl ?? process.env["AMBA_API_URL"] ?? "https://api.amba.dev";
@@ -1349,21 +1408,32 @@ async function performSandboxSignup(req, options = {}) {
1349
1408
  };
1350
1409
  const signupToken = process.env["AMBA_SIGNUP_TOKEN"]?.trim();
1351
1410
  if (signupToken) headers[SIGNUP_TOKEN_HEADER] = signupToken;
1411
+ const body = {
1412
+ email: req.email,
1413
+ password: req.password,
1414
+ name: req.name ?? "amba-sandbox-cli"
1415
+ };
1416
+ const inviteCode = resolveInviteCode(req.inviteCode);
1417
+ if (inviteCode) body["invite_code"] = inviteCode;
1352
1418
  const res = await fetchImpl(url, {
1353
1419
  method: "POST",
1354
1420
  headers,
1355
- body: JSON.stringify({
1356
- email: req.email,
1357
- password: req.password,
1358
- name: req.name ?? "amba-sandbox-cli"
1359
- })
1421
+ body: JSON.stringify(body)
1360
1422
  });
1361
1423
  if (!res.ok) {
1362
1424
  let detail = `${res.status} ${res.statusText}`;
1425
+ let errorBody = {};
1363
1426
  try {
1364
- const body = await res.json();
1365
- if (body.error?.message) detail = body.error.message;
1427
+ errorBody = await res.json();
1428
+ if (errorBody.error?.message) detail = errorBody.error.message;
1366
1429
  } catch {}
1430
+ const code = errorBody.error?.code;
1431
+ if (res.status === 403 && isSignupAccessErrorCode(code)) throw new SignupAccessError({
1432
+ code,
1433
+ serverMessage: detail,
1434
+ requestAccessUrl: errorBody.error?.details?.request_access_url,
1435
+ requestAccessEndpoint: errorBody.error?.details?.request_access_endpoint
1436
+ });
1367
1437
  throw new Error(`Sandbox signup failed: ${detail}`);
1368
1438
  }
1369
1439
  let raw;
@@ -1963,7 +2033,8 @@ async function ensureDeveloperIdentity(options = {}) {
1963
2033
  if (options.signupOnMissing === false) throw new Error(`No verified Amba identity at ${developerCredentialsPath(options.homeDir)} and signup-on-missing is disabled. Run \`amba login\` to authenticate.`);
1964
2034
  const signup = await performSandboxSignup({
1965
2035
  email: options.sandboxEmail?.trim() || generateSandboxEmail(),
1966
- password: generateSandboxPassword()
2036
+ password: generateSandboxPassword(),
2037
+ inviteCode: options.inviteCode
1967
2038
  }, {
1968
2039
  apiUrl,
1969
2040
  fetchImpl
@@ -2716,6 +2787,8 @@ Tell the user where their credentials live:
2716
2787
  - \`project_id\`, \`client_key\` — the values they paste into their app's \`.env.local\` / \`.env\`.
2717
2788
  - \`server_key\` — never ship to user devices; only into a server \`.env\` or a secret manager. The \`amb_dev_sk_\` / \`amb_live_sk_\` prefix is the marker.
2718
2789
 
2790
+ **Signup refused with 403 \`INVITE_REQUIRED\` or \`INVITE_INVALID\`?** New accounts need an invite code right now. Ask the developer for theirs (it starts with \`AMBA-\` and arrives in their invite email) and call \`amba_developer_signup\` again with the same arguments plus \`invite_code\`. If they have no code, or the answer is \`SIGNUPS_CLOSED\`, offer to request access for them: \`POST https://api.amba.dev/v1/auth/developer/waitlist\` with \`{ "email": "<their own email>", "note": "<optional>" }\`, or send them to the \`error.details.request_access_url\` page. Tell them the invite code arrives by email, then stop. Retry only with a code the developer hands you; never retry in a loop, invent codes, or try other email addresses. From a terminal, the code goes to \`npx @layers/amba init --invite <code>\` (or the \`AMBA_INVITE_CODE\` env var).
2791
+
2719
2792
  **Already have a PAT?** Skip the signup. Call \`amba_developer_me({})\` to verify the Bearer; if it succeeds, either reuse the most recent project (\`amba_projects_list\`) or call \`amba_projects_create({ name: "<app-name>", platform: "all" })\` and then \`amba_api_keys_create\` twice to mint client + server keys for \`environment: "development"\`.
2720
2793
 
2721
2794
  **Can't reach \`amba_developer_signup\` — it's not in your tool list, or your harness blocks the call?** Do NOT try to work around it by adding the MCP server yourself, running an installer, or editing your own settings/permissions — refusing to self-modify or widen your own permissions is the correct call, and you should not circumvent it. Instead, hand the developer **one** command to run in **their own** terminal (which is not subject to your sandbox):
@@ -2869,8 +2942,8 @@ Be specific. List resources by key, not "some achievements". If something needs
2869
2942
  ## Get credentials (cheat sheet)
2870
2943
 
2871
2944
  - No terminal, in an MCP client: call \`amba_developer_signup\` (no Bearer required) — this guide's Step 0.
2872
- - With a terminal: \`npx -y @layers/amba init\` signs up, mints a project + client/server keys, writes \`.env.local\` + \`AMBA.md\`, installs the \`/amba\` skill, and wires \`mcpServers.amba\` into every detected MCP-client config in one command. Auto-detects non-TTY invocations (the coding-agent bash-tool case) and runs headlessly.
2873
- - Bind the sandbox account to a real email later: \`npx @layers/amba claim me@example.com\`. The backend emails a one-click magic link; clicking it lifts the sandbox cap to the Free tier.
2945
+ - With a terminal: \`npx -y @layers/amba init\` signs up, mints a project + client/server keys, writes \`.env.local\` + \`AMBA.md\`, installs the \`/amba\` skill, and wires \`mcpServers.amba\` into every detected MCP-client config in one command. Auto-detects non-TTY invocations (the coding-agent bash-tool case) and runs headlessly. While signups are invite-only, add \`--invite <code>\` (or set \`AMBA_INVITE_CODE\`).
2946
+ - Bind the sandbox account to its human owner as soon as you know who that is: \`amba_developer_claim\` with their email (or \`npx @layers/amba claim me@example.com\`). The backend emails a one-click magic link; clicking it lifts the sandbox cap to the Free tier. Each free project of an unclaimed sandbox runs its own clock from when it was created: a claim notice from day 7, a hold on day 14, and an archive on day 90 after a full hold (archiving deletes nothing); claiming lifts all of it.
2874
2947
  - Hosted MCP endpoint: \`https://mcp.amba.dev/mcp\` (Streamable HTTP, Bearer auth).
2875
2948
 
2876
2949
  ## SDKs
@@ -3882,14 +3955,21 @@ Docs: https://docs.amba.dev
3882
3955
  async function initCommand(options = {}) {
3883
3956
  const cwd = process.cwd();
3884
3957
  const isNonTTY = process.stdin.isTTY !== true;
3885
- if (options.sandbox === true || options.json === true || isNonTTY) {
3886
- const result = await runSandboxInit(cwd, {
3887
- sandboxEmail: options.sandboxEmail,
3888
- noMcpConfig: options.noMcpConfig,
3889
- noSkills: options.noSkills,
3890
- json: options.json,
3891
- homeDir: options.homeDir
3892
- });
3958
+ if (options.sandbox === true || options.json === true || Boolean(options.inviteCode?.trim()) || isNonTTY) {
3959
+ let result;
3960
+ try {
3961
+ result = await runSandboxInit(cwd, {
3962
+ sandboxEmail: options.sandboxEmail,
3963
+ inviteCode: options.inviteCode,
3964
+ noMcpConfig: options.noMcpConfig,
3965
+ noSkills: options.noSkills,
3966
+ json: options.json,
3967
+ homeDir: options.homeDir
3968
+ });
3969
+ } catch (err) {
3970
+ if (options.json) process.stdout.write(JSON.stringify(sandboxErrorToJson(err), null, 2) + "\n");
3971
+ throw err;
3972
+ }
3893
3973
  if (options.json) process.stdout.write(JSON.stringify(sandboxResultToJson(result), null, 2) + "\n");
3894
3974
  else printSandboxNextSteps(result);
3895
3975
  return;
@@ -4134,7 +4214,8 @@ async function runSandboxInit(cwd, options) {
4134
4214
  };
4135
4215
  } else identity = await ensureDeveloperIdentity({
4136
4216
  homeDir: options.homeDir,
4137
- sandboxEmail: options.sandboxEmail
4217
+ sandboxEmail: options.sandboxEmail,
4218
+ inviteCode: options.inviteCode
4138
4219
  });
4139
4220
  setBearerOverride(identity.credentials.pat);
4140
4221
  const framework = await detectFramework(cwd);
@@ -4203,6 +4284,31 @@ async function runSandboxInit(cwd, options) {
4203
4284
  setupTargets
4204
4285
  };
4205
4286
  }
4287
+ /**
4288
+ * `--json` failure document. A gated signup carries its 403 code plus
4289
+ * where to request access; every other failure reports `INIT_FAILED`.
4290
+ */
4291
+ function sandboxErrorToJson(err) {
4292
+ if (err instanceof SignupAccessError) return {
4293
+ ok: false,
4294
+ mode: "sandbox",
4295
+ error: {
4296
+ code: err.code,
4297
+ message: err.serverMessage,
4298
+ request_access_url: err.requestAccessUrl,
4299
+ request_access_endpoint: err.requestAccessEndpoint
4300
+ },
4301
+ next_steps: err.nextSteps
4302
+ };
4303
+ return {
4304
+ ok: false,
4305
+ mode: "sandbox",
4306
+ error: {
4307
+ code: "INIT_FAILED",
4308
+ message: err instanceof Error ? err.message : String(err)
4309
+ }
4310
+ };
4311
+ }
4206
4312
  function sandboxResultToJson(r) {
4207
4313
  return {
4208
4314
  ok: true,
@@ -6861,6 +6967,28 @@ async function adminWrite$1(method, path, body) {
6861
6967
  }
6862
6968
  return await res.json();
6863
6969
  }
6970
+ function pctLabel(pct) {
6971
+ return pct === null ? "" : pc.dim(` (${Math.round(pct * 100)}%)`);
6972
+ }
6973
+ /**
6974
+ * Print the database limits block. Exported for `amba status`'s one-line
6975
+ * summary + tests.
6976
+ */
6977
+ function formatDatabaseLimits(db) {
6978
+ const lines = [];
6979
+ const computeUsed = db.compute.used_cu_hours === null ? "—" : `${db.compute.used_cu_hours}`;
6980
+ const computeLimit = db.compute.limit_cu_hours === null ? "no cap" : `${db.compute.limit_cu_hours} CU-hours`;
6981
+ lines.push(` ${pc.dim("Database compute".padEnd(22))} ${computeUsed} / ${computeLimit}${pctLabel(db.compute.pct)}` + pc.dim(` max ${db.compute.max_cu} CU`));
6982
+ const storageUsed = db.storage.used_mb === null ? "—" : `${db.storage.used_mb} MB`;
6983
+ const storageLimit = db.storage.limit_mb === null ? "no cap" : `${db.storage.limit_mb} MB`;
6984
+ lines.push(` ${pc.dim("Database size".padEnd(22))} ${storageUsed} / ${storageLimit}${pctLabel(db.storage.pct)}`);
6985
+ if (db.period && db.compute.limit_cu_hours !== null) lines.push(` ${pc.dim("Compute resets".padEnd(22))} ${new Date(db.period.resets_at).toLocaleDateString(void 0, { timeZone: "UTC" })} UTC`);
6986
+ const upgrade = db.upgrade?.cli ?? "amba billing upgrade --tier pro";
6987
+ if (db.limit_reached.includes("database_compute")) lines.push(pc.red(" Database paused: ") + `compute limit reached; database calls return 402 FREE_TIER_LIMIT_REACHED until the period resets. Upgrade now: ${pc.bold(upgrade)}`);
6988
+ if (db.limit_reached.includes("database_storage")) lines.push(pc.red(" Database full: ") + `storage limit reached; writes that add data return 402. Delete data or upgrade: ${pc.bold(upgrade)}`);
6989
+ else if (!db.limit_reached.includes("database_compute") && db.compute.pct !== null && db.compute.pct >= .8) lines.push(pc.yellow(" Near the compute limit: ") + `upgrade before the database pauses: ${pc.bold(upgrade)}`);
6990
+ return lines;
6991
+ }
6864
6992
  /** Append "· $X.XX" to a usage line when the meter accrued cost this period. */
6865
6993
  function costSuffix(cost) {
6866
6994
  if (!cost || cost.cost_usd <= 0) return "";
@@ -6909,6 +7037,11 @@ async function billingStatusCommand() {
6909
7037
  console.log(formatAxis("Database storage", s.headroom.db_storage_mb, true) + costSuffix(cost.db_storage_mb));
6910
7038
  console.log(formatAxis("Media storage", s.headroom.media_storage_mb, true) + costSuffix(cost.media_storage_mb));
6911
7039
  if (s.headroom.app_mcp_tool_calls) console.log(formatAxis("Agent tool calls", s.headroom.app_mcp_tool_calls, false) + costSuffix(cost.app_mcp_tool_calls));
7040
+ if (s.database) {
7041
+ console.log();
7042
+ console.log(pc.bold(" Database limits") + (s.database.observed_at ? pc.dim(` — measured ${new Date(s.database.observed_at).toLocaleString()}`) : ""));
7043
+ for (const line of formatDatabaseLimits(s.database)) console.log(line);
7044
+ }
6912
7045
  console.log();
6913
7046
  if (s.human_action_required !== "none") {
6914
7047
  const action = s.human_action_required.replaceAll("_", " ");
@@ -6939,6 +7072,7 @@ async function billingUpgradeCommand(input) {
6939
7072
  console.log(` ${pc.cyan(res.data.url)}`);
6940
7073
  console.log();
6941
7074
  console.log(pc.dim(" Subscription status updates automatically once Stripe confirms (a few seconds)."));
7075
+ console.log(pc.dim(" Paid plans lift the free-tier database compute cap; a paused database resumes within seconds."));
6942
7076
  console.log();
6943
7077
  } catch (err) {
6944
7078
  handleError$1(err);
@@ -9811,7 +9945,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9811
9945
  //#endregion
9812
9946
  //#region src/index.ts
9813
9947
  const program = new Command();
9814
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.8");
9948
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.9");
9815
9949
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9816
9950
  program.hook("preAction", (thisCommand) => {
9817
9951
  setBearerOverride(resolveTokenSource({
@@ -9826,7 +9960,7 @@ function runAction(fn) {
9826
9960
  process.exit(1);
9827
9961
  });
9828
9962
  }
9829
- program.command("init").description("Initialize Amba in the current project (mints a personal dev project by default)").option("--with-example", "Scaffold a sample app.tsx + README snippet into the current directory").option("--env <env>", "'development' (default) or 'production'").option("--sandbox", "Headless agentic mode: auto-signup, write .env.local + AMBA.md, auto-wire MCP client configs. No prompts.").option("--email <email>", "Override the auto-generated sandbox email (requires --sandbox)").option("--no-mcp-config", "Skip writing MCP client config files (rare; mostly for testing)").option("--no-skills", "Skip installing the project-local /amba-build Claude Code skill (default: install during --sandbox)").option("--json", "Emit a machine-readable JSON summary on stdout instead of human-readable lines").action(async (opts) => {
9963
+ program.command("init").description("Initialize Amba in the current project (mints a personal dev project by default)").option("--with-example", "Scaffold a sample app.tsx + README snippet into the current directory").option("--env <env>", "'development' (default) or 'production'").option("--sandbox", "Headless agentic mode: auto-signup, write .env.local + AMBA.md, auto-wire MCP client configs. No prompts.").option("--email <email>", "Override the auto-generated sandbox email (requires --sandbox)").option("--invite <code>", "Invite code for creating a new account while signups are invite-only (implies --sandbox; also read from AMBA_INVITE_CODE)").option("--no-mcp-config", "Skip writing MCP client config files (rare; mostly for testing)").option("--no-skills", "Skip installing the project-local /amba-build Claude Code skill (default: install during --sandbox)").option("--json", "Emit a machine-readable JSON summary on stdout instead of human-readable lines").action(async (opts) => {
9830
9964
  let env;
9831
9965
  if (opts.env === "development" || opts.env === "dev") env = "development";
9832
9966
  else if (opts.env === "production" || opts.env === "prod") env = "production";
@@ -9834,27 +9968,29 @@ program.command("init").description("Initialize Amba in the current project (min
9834
9968
  console.error(`Error: --env must be 'development' or 'production' (got '${opts.env}').`);
9835
9969
  process.exit(1);
9836
9970
  }
9837
- if (opts.email && !opts.sandbox) {
9971
+ const sandbox = opts.sandbox === true || Boolean(opts.invite?.trim());
9972
+ if (opts.email && !sandbox) {
9838
9973
  console.error("Error: --email is only valid with --sandbox.");
9839
9974
  process.exit(1);
9840
9975
  }
9841
- if (opts.json && !opts.sandbox) {
9976
+ if (opts.json && !sandbox) {
9842
9977
  console.error("Error: --json is only valid with --sandbox.");
9843
9978
  process.exit(1);
9844
9979
  }
9845
- if (opts.mcpConfig === false && !opts.sandbox) {
9980
+ if (opts.mcpConfig === false && !sandbox) {
9846
9981
  console.error("Error: --no-mcp-config is only valid with --sandbox.");
9847
9982
  process.exit(1);
9848
9983
  }
9849
- if (opts.skills === false && !opts.sandbox) {
9984
+ if (opts.skills === false && !sandbox) {
9850
9985
  console.error("Error: --no-skills is only valid with --sandbox.");
9851
9986
  process.exit(1);
9852
9987
  }
9853
9988
  await runAction(() => initCommand({
9854
9989
  withExample: opts.withExample,
9855
9990
  env,
9856
- sandbox: opts.sandbox,
9991
+ sandbox,
9857
9992
  sandboxEmail: opts.email,
9993
+ inviteCode: opts.invite,
9858
9994
  noMcpConfig: opts.mcpConfig === false,
9859
9995
  noSkills: opts.skills === false,
9860
9996
  json: opts.json
package/dist/sandbox.d.ts CHANGED
@@ -43,6 +43,12 @@ export interface SandboxSignupRequest {
43
43
  email: string;
44
44
  password: string;
45
45
  name?: string;
46
+ /**
47
+ * Platform invite code (`--invite`). Falls back to `AMBA_INVITE_CODE`
48
+ * when absent. Sent as `invite_code`; the server ignores case, spaces
49
+ * and dashes.
50
+ */
51
+ inviteCode?: string;
46
52
  }
47
53
  /**
48
54
  * Shape returned by `POST /v1/auth/developer/signup`. Only the fields the
@@ -148,6 +154,34 @@ export declare function generateSandboxEmail(): string;
148
154
  export declare function generateSandboxPassword(): string;
149
155
  /** Request header carrying AMBA_SIGNUP_TOKEN on the signup call. */
150
156
  export declare const SIGNUP_TOKEN_HEADER = "X-Amba-Signup-Token";
157
+ /** Environment variable read when `--invite` is absent. */
158
+ export declare const INVITE_CODE_ENV = "AMBA_INVITE_CODE";
159
+ /** The 403 codes signup answers with when the platform is gated. */
160
+ export declare const SIGNUP_ACCESS_ERROR_CODES: readonly ["SIGNUPS_CLOSED", "INVITE_REQUIRED", "INVITE_INVALID"];
161
+ export type SignupAccessErrorCode = (typeof SIGNUP_ACCESS_ERROR_CODES)[number];
162
+ /**
163
+ * The flag value when set, else `AMBA_INVITE_CODE`, else undefined. Blank
164
+ * values count as absent so an empty env var never sends `invite_code: ""`.
165
+ */
166
+ export declare function resolveInviteCode(flag?: string): string | undefined;
167
+ /**
168
+ * Thrown when signup answers 403 SIGNUPS_CLOSED / INVITE_REQUIRED /
169
+ * INVITE_INVALID. `message` carries the server's sentence plus the next
170
+ * steps; the structured fields feed `amba init --json`.
171
+ */
172
+ export declare class SignupAccessError extends Error {
173
+ readonly code: SignupAccessErrorCode;
174
+ readonly serverMessage: string;
175
+ readonly requestAccessUrl: string;
176
+ readonly requestAccessEndpoint: string;
177
+ readonly nextSteps: string[];
178
+ constructor(input: {
179
+ code: SignupAccessErrorCode;
180
+ serverMessage: string;
181
+ requestAccessUrl?: string;
182
+ requestAccessEndpoint?: string;
183
+ });
184
+ }
151
185
  /**
152
186
  * POST the synthesized credentials at the public signup endpoint.
153
187
  *
@@ -159,7 +193,8 @@ export declare const SIGNUP_TOKEN_HEADER = "X-Amba-Signup-Token";
159
193
  * Returns the unwrapped, flattened shape consumed by the rest of the
160
194
  * sandbox flow. Throws with a human-readable message on any non-2xx so
161
195
  * the CLI's `runAction` wrapper can surface it without crashing on a
162
- * generic 'fetch failed'.
196
+ * generic 'fetch failed'. A gated platform (403 SIGNUPS_CLOSED /
197
+ * INVITE_REQUIRED / INVITE_INVALID) throws `SignupAccessError`.
163
198
  */
164
199
  export declare function performSandboxSignup(req: SandboxSignupRequest, options?: {
165
200
  apiUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.8",
3
+ "version": "4.1.9",
4
4
  "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites, and `amba ship` to take an Expo app live on the App Store + Google Play. `npx @layers/amba init` to start.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,8 @@
49
49
  "tsdown": "^0.12.5",
50
50
  "typescript": "^5.8.3",
51
51
  "vitest": "^3.2.4",
52
- "@layers/amba-shared": "4.0.6",
53
- "@layers/amba-mcp": "4.0.11"
52
+ "@layers/amba-mcp": "4.0.12",
53
+ "@layers/amba-shared": "4.0.6"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -69,7 +69,9 @@ Check whether a project exists in this directory:
69
69
 
70
70
  The project status is `"provisioning"` immediately after signup; the per-tenant DB activates within ~5 seconds. Don't block — your next SDK or MCP call may briefly retry, that's fine. If you want to be polite, call `amba_projects_get_provisioning_status({ project_id })` once and proceed when it returns `"active"` (or after 15s, whichever first).
71
71
 
72
- > **If you don't see Amba MCP tools available** (no `mcp__amba__*` in your tool list), Amba's MCP server isn't wired into your agent's tool surface this session. Run `npx -y @layers/amba init` via your bash tool — it provisions the account/project, writes `.env.local`, installs this skill, and wires the MCP entry into the user's agent config so the typed tools light up on the next agent launch.
72
+ **Signup refused with 403 `INVITE_REQUIRED` or `INVITE_INVALID`?** New accounts need an invite code right now. Ask the developer for theirs (it starts with `AMBA-` and arrives in their invite email) and call `amba_developer_signup` again with the same arguments plus `invite_code` (or rerun `npx -y @layers/amba init --invite <code>`; `AMBA_INVITE_CODE` works too). If they have no code, or the answer is `SIGNUPS_CLOSED`, offer to request access for them: `POST https://api.amba.dev/v1/auth/developer/waitlist` with `{ "email": "<their own email>", "note": "<optional>" }`, or send them to the `error.details.request_access_url` page. Tell them the invite code arrives by email, then stop. Retry only with a code the developer hands you; never retry in a loop, invent codes, or try other email addresses.
73
+
74
+ > **If you don't see Amba MCP tools available** (no `mcp__amba__*` in your tool list), Amba's MCP server isn't wired into your agent's tool surface this session. Run `npx -y @layers/amba init` via your bash tool — it provisions the account/project, writes `.env.local`, installs this skill, and wires the MCP entry into the user's agent config so the typed tools light up on the next agent launch. While signups are invite-only, add `--invite <code>` with the developer's invite code; without one, init stops and prints where to request access.
73
75
  >
74
76
  > **If your harness ALSO blocks that bash command** (a locked-down agent: no installer, no network egress, no settings edits) — do NOT try to add the MCP server yourself, run the installer another way, or edit your own settings/permissions to authorize it. Refusing to self-modify or widen your own permissions is correct; don't circumvent it. Instead, ask the developer to run **one** command in **their own** terminal (not subject to your sandbox): `npx @layers/amba init`. It creates the account with no browser and writes `AMBA_PROJECT_ID` + `AMBA_CLIENT_KEY` to `.env.local`. Ask them to run it, say when it's done, then resume from Step 0 by reading `.env.local`. Pasting an `amb_ck_…` client key is the last-resort lite path — enough to wire `Amba.configure()` + verify events land, but a client key alone can't do admin provisioning (achievements, collections, segments), which needs the PAT/server key.
75
77
  >