@layers/amba 4.0.3 → 4.1.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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `amba ship` config — the typed, hand-validated launch config.
3
+ *
4
+ * One file per repo (default `amba.ship.json`) describes how to take this Expo
5
+ * app from code-complete to live on the App Store and Google Play. It carries
6
+ * NON-SECRET identifiers and the NAME of the env var holding the Expo token —
7
+ * never secret values (the App Store Connect `.p8` and Play service-account
8
+ * JSON live in `eas.json`, owned by the app repo, exactly where the build runs).
9
+ *
10
+ * Validation is hand-rolled (the CLI deliberately ships no `zod` — see
11
+ * `_internal/shared.ts` for the same return-all-errors-at-once pattern). The
12
+ * field names are the canonical contract; `validateShipConfig` reports every
13
+ * problem in one pass and enforces the cross-field invariants.
14
+ *
15
+ * Monetization is NOT described here: the App Store / Play product catalog is
16
+ * Amba's monetization control plane (`amba monetization`, the durable Track H
17
+ * apply against the provider). `ship` only flips `monetization.enabled` to
18
+ * decide whether to verify that catalog is in sync before building.
19
+ */
20
+ /** The ordered launch phases. `ship` runs them in this sequence. */
21
+ export declare const SHIP_PHASES: readonly ["preflight", "monetization", "build", "submit", "metadata", "release"];
22
+ export type Phase = (typeof SHIP_PHASES)[number];
23
+ export declare const SHIP_PLATFORMS: readonly ["ios", "android"];
24
+ export type Platform = (typeof SHIP_PLATFORMS)[number];
25
+ /** Google Play release tracks `ship` can submit to. */
26
+ export declare const PLAY_TRACKS: readonly ["internal", "alpha", "beta", "production"];
27
+ export type PlayTrack = (typeof PLAY_TRACKS)[number];
28
+ /** Default config filename when no `--config <path>` is passed. */
29
+ export declare const DEFAULT_SHIP_CONFIG_FILE = "amba.ship.json";
30
+ export interface ShipConfig {
31
+ /** Schema version of this config file (forward-compatible migrations). */
32
+ version: number;
33
+ app: {
34
+ /** Lowercase key used to scope the per-app launch state file. */
35
+ slug: string;
36
+ /** Human-facing app name (App Store / Play display). */
37
+ name: string;
38
+ /** Marketing version MAJOR.MINOR.PATCH. */
39
+ version: string;
40
+ /** iOS bundle identifier (reverse-DNS). */
41
+ bundleId: string;
42
+ /** Android application id (reverse-DNS); may equal bundleId. */
43
+ androidPackage: string;
44
+ };
45
+ build: {
46
+ /** EAS build profile (a `build.<profile>` key in eas.json), e.g. "production". */
47
+ profile: string;
48
+ /** Platforms to build/submit/release. At least one. */
49
+ platforms: Platform[];
50
+ };
51
+ ios: {
52
+ /**
53
+ * App Store Connect numeric app id. `null` until the app record exists.
54
+ * EAS Submit cannot create the record (and under --non-interactive cannot
55
+ * prompt for it, eas-cli #2418), so `ship` treats a null value as a manual
56
+ * gate on the submit phase.
57
+ */
58
+ ascAppId: string | null;
59
+ };
60
+ android: {
61
+ /** Track the submit phase lands on; release promotes it to production. */
62
+ track: PlayTrack;
63
+ };
64
+ monetization: {
65
+ /**
66
+ * When true, `ship` verifies the App Store / Play product catalog is in
67
+ * sync with the Amba monetization control plane before building, and gates
68
+ * if it has drifted (run `amba monetization apply`). When false (the
69
+ * default for apps that don't sell anything), the monetization phase is a
70
+ * no-op skip.
71
+ */
72
+ enabled: boolean;
73
+ };
74
+ secrets: {
75
+ /** NAME of the env var holding the Expo token for non-interactive EAS auth. */
76
+ expoTokenEnvVar: string;
77
+ };
78
+ }
79
+ export type ValidateResult = {
80
+ ok: true;
81
+ config: ShipConfig;
82
+ } | {
83
+ ok: false;
84
+ errors: string[];
85
+ };
86
+ /**
87
+ * Validate an untrusted parsed JSON value into a typed `ShipConfig`,
88
+ * collecting EVERY problem (not just the first) so a misconfigured file
89
+ * surfaces all its issues in one run. Returns the typed config on success.
90
+ */
91
+ export declare function validateShipConfig(raw: unknown): ValidateResult;
92
+ /**
93
+ * A starter config written by `amba ship init`. Placeholders are intentionally
94
+ * obvious so the validator's invariants don't pass on an unedited file's IDs
95
+ * (the reverse-DNS / semver placeholders DO validate so `--phase preflight`
96
+ * works immediately; the operator swaps in their real values).
97
+ */
98
+ export declare function shipConfigTemplate(slug: string): ShipConfig;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The external-command boundary for `amba ship`.
3
+ *
4
+ * Every shell-out (always `eas`) goes through an `Executor`, so the phase
5
+ * logic — which args get built, how `--json` output is parsed, which failures
6
+ * map to which manual gate — is unit-tested against a fake executor with NO
7
+ * live cloud, no EAS account, and no Apple/Google credentials. The default
8
+ * `createNodeExecutor()` is a thin, shell-free wrapper around
9
+ * `node:child_process.spawn` (mirrors the launch toolkit's `lib/exec`).
10
+ */
11
+ export interface ExecRunOptions {
12
+ /** Extra environment variables merged over process.env. */
13
+ env?: NodeJS.ProcessEnv;
14
+ /** Working directory for the child. Defaults to process.cwd(). */
15
+ cwd?: string;
16
+ /** Hard timeout in ms. On expiry the child is SIGKILLed and the call rejects. */
17
+ timeoutMs?: number;
18
+ /** If true, stream child stdout/stderr live to this process. Default false. */
19
+ stream?: boolean;
20
+ /** If true, log the command and return a synthetic success WITHOUT running. */
21
+ dryRun?: boolean;
22
+ }
23
+ export interface ExecResult {
24
+ /** The redacted command line, for logging / state. */
25
+ command: string;
26
+ code: number;
27
+ stdout: string;
28
+ stderr: string;
29
+ /** True when the call was skipped because of dry-run. */
30
+ dryRun: boolean;
31
+ }
32
+ export interface Executor {
33
+ /** Run a command. Resolves on exit 0; REJECTS (with an actionable message) otherwise. */
34
+ run(cmd: string, args: string[], opts?: ExecRunOptions): Promise<ExecResult>;
35
+ }
36
+ /**
37
+ * Redact secret-shaped tokens from a string before it is logged. A backstop,
38
+ * not a license — `ship` never puts secret values on a command line (EAS reads
39
+ * its token from the env and its store creds from eas.json). Covers Expo /
40
+ * common access-token shapes and `Bearer`/`sk_` prefixes.
41
+ */
42
+ export declare function redactSecrets(text: string): string;
43
+ /** A backing executor over `node:child_process.spawn`. Never invokes a shell. */
44
+ export declare function createNodeExecutor(globalDryRun?: boolean): Executor;
45
+ /**
46
+ * Run a command and report success/failure WITHOUT throwing — for read-only
47
+ * "is this CLI installed/authed?" probes where a non-zero exit is data, not a
48
+ * fatal error.
49
+ */
50
+ export declare function probe(exec: Executor, cmd: string, args: string[], opts?: ExecRunOptions): Promise<{
51
+ ok: boolean;
52
+ result?: ExecResult;
53
+ }>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The `amba ship` orchestrator.
3
+ *
4
+ * Sequences the requested phases in canonical order, honoring per-app
5
+ * idempotency state (skip phases already `done` unless `--force`), propagating
6
+ * a global `--dry-run` to every phase, and stopping the chain at the first gate
7
+ * or failure (later phases depend on earlier ones — the "clear a gate → re-run"
8
+ * loop). It owns ALL state writes and the clock; the phase runners stay pure of
9
+ * both. Returns structured rows for the command layer to render.
10
+ */
11
+ import type { Phase, Platform, ShipConfig } from './config.js';
12
+ import type { Executor } from './exec.js';
13
+ import { type PhaseRunner } from './phases.js';
14
+ export type RowStatus = 'done' | 'skipped' | 'gate' | 'failed' | 'pending' | 'dry-run';
15
+ export interface ShipRow {
16
+ phase: Phase;
17
+ status: RowStatus;
18
+ detail: string;
19
+ gates?: string[];
20
+ }
21
+ export interface ShipResult {
22
+ rows: ShipRow[];
23
+ /** A gate stopped the chain (human store action needed) — not an error. */
24
+ blocked: boolean;
25
+ /** A phase errored. */
26
+ failed: boolean;
27
+ }
28
+ export interface ShipRunOptions {
29
+ config: ShipConfig;
30
+ /** Which phases to run (any subset of SHIP_PHASES). */
31
+ phases: Phase[];
32
+ platforms: Platform[];
33
+ exec: Executor;
34
+ dryRun: boolean;
35
+ force: boolean;
36
+ stateBaseDir?: string;
37
+ env?: NodeJS.ProcessEnv;
38
+ now?: () => string;
39
+ log?: (line: string) => void;
40
+ sleep?: (ms: number) => Promise<void>;
41
+ /** Resolve the linked Amba project id (default: from .env.local). */
42
+ resolveProjectId?: () => Promise<string | null>;
43
+ /** Admin GET (default: the real api-client). Injected in tests. */
44
+ adminGet?: <T>(path: string) => Promise<{
45
+ data: T;
46
+ }>;
47
+ /** Phase runners (default: the real registry). Injected in tests. */
48
+ runners?: Record<Phase, PhaseRunner>;
49
+ releasePollMs?: number;
50
+ releaseMaxWaitMs?: number;
51
+ }
52
+ export declare function runShip(opts: ShipRunOptions): Promise<ShipResult>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The six `amba ship` phases.
3
+ *
4
+ * Each runner receives a `ShipContext` (config + an injected `Executor` for
5
+ * `eas`, an injected admin-API `adminGet`, a project resolver, and state
6
+ * accessors) and returns a `PhaseOutcome`. The cloud-touching work is all
7
+ * behind those injection points, so the arg-building, `--json` parsing, and
8
+ * failure→manual-gate mapping below are unit-tested with no live cloud.
9
+ *
10
+ * Honest gates: several launch steps are NOT API-automatable (create the App
11
+ * Store Connect / Play app record, the Apple Paid-Apps agreement, screenshots,
12
+ * store review). A runner DETECTS such a step and returns `status: 'gate'` with
13
+ * the exact action + console URL, rather than pretending it is automated.
14
+ */
15
+ import type { Executor } from './exec.js';
16
+ import type { Phase, Platform, ShipConfig } from './config.js';
17
+ import type { PhaseStatus } from './state.js';
18
+ export interface ShipContext {
19
+ config: ShipConfig;
20
+ exec: Executor;
21
+ platforms: Platform[];
22
+ dryRun: boolean;
23
+ force: boolean;
24
+ env: NodeJS.ProcessEnv;
25
+ now: () => string;
26
+ log: (line: string) => void;
27
+ /** Read a phase's persisted note (a JSON string) or null. */
28
+ getNote: (phase: Phase) => string | null;
29
+ /** Checkpoint a phase's status + note mid-run. No-op under dryRun. */
30
+ checkpoint: (phase: Phase, status: PhaseStatus, note: string) => void;
31
+ /** Resolve the linked Amba project id, or null when no project is linked. */
32
+ resolveProjectId: () => Promise<string | null>;
33
+ /** GET an admin API path → `{ data }`. */
34
+ adminGet: <T>(path: string) => Promise<{
35
+ data: T;
36
+ }>;
37
+ /** Sleep helper (injected so release-polling is instant in tests). */
38
+ sleep: (ms: number) => Promise<void>;
39
+ /** Build-completion poll cadence (release phase). Small values in tests. */
40
+ releasePollMs: number;
41
+ releaseMaxWaitMs: number;
42
+ }
43
+ export interface PhaseOutcome {
44
+ status: 'done' | 'gate' | 'failed' | 'skipped';
45
+ detail: string;
46
+ /** Structured JSON note to persist (build/submit/release maps). */
47
+ note?: string;
48
+ /** Human-action items when status === 'gate' (printed prominently). */
49
+ gates?: string[];
50
+ }
51
+ export type PhaseRunner = (ctx: ShipContext) => Promise<PhaseOutcome>;
52
+ export declare function easBuildArgs(platform: Platform, profile: string): string[];
53
+ export declare function easSubmitArgs(input: {
54
+ platform: Platform;
55
+ profile: string;
56
+ buildId: string | null;
57
+ track?: string;
58
+ }): string[];
59
+ export declare function easBuildViewArgs(buildId: string): string[];
60
+ export declare function easMetadataArgs(profile: string): string[];
61
+ export interface EasBuild {
62
+ id: string;
63
+ status?: string;
64
+ platform?: string;
65
+ appVersion?: string;
66
+ artifacts?: {
67
+ applicationArchiveUrl?: string;
68
+ buildUrl?: string;
69
+ };
70
+ buildDetailsPageUrl?: string;
71
+ }
72
+ export interface EasSubmission {
73
+ id?: string;
74
+ status?: string;
75
+ platform?: string;
76
+ buildId?: string;
77
+ submissionDetailsPageUrl?: string;
78
+ }
79
+ /** Pull the build object for `platform` out of `eas build --json` stdout. */
80
+ export declare function extractBuild(raw: string, platform: Platform): EasBuild;
81
+ export declare function artifactUrlOf(b: EasBuild): string | null;
82
+ /** Parse the submission object for `platform` out of `eas submit --json` stdout. */
83
+ export declare function parseSubmission(raw: string, platform: Platform): EasSubmission;
84
+ export declare function diagnoseBuildFailure(message: string): string;
85
+ /** A submit failure that needs a human store action, or null for a plain error. */
86
+ export declare function diagnoseSubmitGate(platform: Platform, message: string): string | null;
87
+ export declare const PHASE_RUNNERS: Record<Phase, PhaseRunner>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `amba ship` per-app idempotency state.
3
+ *
4
+ * Each app's launch progress is a JSON file at `.amba/ship-state/<slug>.json`
5
+ * recording, per phase, whether it is done / failed / blocked-by-a-human-gate /
6
+ * in-progress, when, and an optional structured `note` (e.g. the build ids a
7
+ * later phase reads back). The orchestrator uses it to skip already-done phases
8
+ * and to resume the "clear a gate → re-run" loop.
9
+ *
10
+ * PURITY RULE: this module NEVER reads the clock itself — callers pass an
11
+ * ISO-8601 timestamp into every mutation so it stays deterministically
12
+ * testable (the orchestrator owns the clock). The file is purely an
13
+ * optimization/audit log: deleting it is safe because every phase re-detects
14
+ * real cloud state before mutating.
15
+ */
16
+ import type { Phase } from './config.js';
17
+ export type PhaseStatus = 'pending' | 'in-progress' | 'done' | 'failed' | 'gate';
18
+ export interface PhaseRecord {
19
+ status: PhaseStatus;
20
+ /** ISO timestamp of the last status change (supplied by the caller). */
21
+ updatedAt: string;
22
+ /** Free-form note: build ids, a short reason, etc. NEVER secrets. */
23
+ note?: string;
24
+ }
25
+ export interface ShipState {
26
+ slug: string;
27
+ /** Schema version of this state file (forward-compatible migrations). */
28
+ stateVersion: 1;
29
+ /**
30
+ * The app marketing version the recorded phases shipped. When it changes, the
31
+ * orchestrator invalidates the binary-scoped phases (build → release) so a
32
+ * version bump rebuilds instead of skipping a stale `done`.
33
+ */
34
+ appVersion?: string;
35
+ phases: Partial<Record<Phase, PhaseRecord>>;
36
+ }
37
+ /** Path to the state file for a slug, under `<baseDir>/.amba/ship-state/`. */
38
+ export declare function shipStatePath(slug: string, baseDir?: string): string;
39
+ /** Load state for a slug, or a fresh empty state if none exists. Pure read. */
40
+ export declare function loadShipState(slug: string, baseDir?: string): ShipState;
41
+ /** Write state atomically (write temp, then rename) to avoid partial files. */
42
+ export declare function saveShipState(state: ShipState, baseDir?: string): void;
43
+ /**
44
+ * Return a NEW state with one phase updated. Pure: does not touch disk and does
45
+ * not read the clock — the caller passes `timestamp`. Use `saveShipState` to
46
+ * persist.
47
+ */
48
+ export declare function withPhase(state: ShipState, phase: Phase, status: PhaseStatus, timestamp: string, note?: string): ShipState;
49
+ /** True if a phase is recorded as done. Used by the orchestrator for idempotency. */
50
+ export declare function isPhaseDone(state: ShipState, phase: Phase): boolean;
51
+ /** The record for a phase, or a synthetic pending record if never run. */
52
+ export declare function phaseRecord(state: ShipState, phase: Phase): PhaseRecord;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.0.3",
4
- "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites — one CLI to spin up your project and ship to production. `npx @layers/amba init` to start.",
3
+ "version": "4.1.0",
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": {
7
7
  "amba": "dist/index.js"
@@ -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-mcp": "4.0.2",
53
- "@layers/amba-shared": "4.0.2"
52
+ "@layers/amba-shared": "4.0.4",
53
+ "@layers/amba-mcp": "4.0.6"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -71,6 +71,8 @@ Check whether a project exists in this directory:
71
71
 
72
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.
73
73
  >
74
+ > **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
+ >
74
76
  > Do not pause to ask the user about restarting. You have **three** working paths for admin operations in this session, in order of preference:
75
77
  >
76
78
  > 1. **POST JSON-RPC directly to `https://mcp.amba.dev/mcp`** (recommended — full 178-tool surface, no client wiring needed). Amba's MCP is plain HTTP, not stdio. Read the PAT from `~/.amba/credentials.json` (`.pat` field), then `curl` (or `fetch`) the endpoint:
@@ -15,7 +15,7 @@ A collection is a schema-first table inside the project's isolated tenant databa
15
15
  | `amba_collections_create` / `amba_create_collection` | Create a typed collection. | `{ project_id, name: "todos", columns: [{ name: "title", type: "text", nullable: false }, { name: "done", type: "boolean", nullable: false, default: false }, { name: "due_at", type: "timestamptz", nullable: true }] }` |
16
16
  | `amba_collections_list` / `amba_list_collections` | List collections in this project. | `{ project_id }` |
17
17
  | `amba_collections_get` / `amba_get_collection` | Read one collection's schema. | `{ project_id, collection_name: "todos" }` |
18
- | `amba_collections_alter` / `amba_alter_collection` | Add / drop columns, add / drop indexes. | `{ project_id, collection_name: "todos", add_columns: [{ name: "priority", type: "int", nullable: true }] }` |
18
+ | `amba_collections_alter` / `amba_alter_collection` | Add / drop columns, add / drop indexes. | `{ project_id, collection_name: "todos", add_columns: [{ name: "priority", type: "integer", nullable: true }] }` |
19
19
  | `amba_collections_delete` / `amba_delete_collection` | Drop the table (destructive). | `{ project_id, collection_name }` |
20
20
  | `amba_admin_insert_row` | Insert a row as the developer (bypasses user-scope). Useful for seed data. | `{ project_id, collection: "todos", row: { title: "Sample todo", done: false } }` |
21
21
  | `amba_admin_list_rows` | Read rows as the developer (bypasses user-scope — sees every user's rows). | `{ project_id, collection: "todos", limit: 100 }` |
@@ -28,7 +28,7 @@ A collection is a schema-first table inside the project's isolated tenant databa
28
28
  | `amba_client_find_rows` | Filter / sort / paginate rows (end-user). | `{ project_id, api_key, session_token, collection, filter: {...}, order_by: [...], limit: 50 }` |
29
29
  | `amba_client_find_nearest_rows` | Vector-similarity search (rows with a `vector(<dim>)` column). | `{ project_id, api_key, session_token, collection, vector_column: "embedding", query_vector: [...], k: 10 }` |
30
30
 
31
- Column types: `text`, `int`, `bigint`, `float`, `boolean`, `timestamptz`, `date`, `json`, `jsonb`, `uuid`, `vector(<dim>)` (e.g. `vector(1536)` for OpenAI embeddings).
31
+ Column types: `text`, `integer`, `bigint`, `numeric`, `boolean`, `timestamptz`, `date`, `jsonb`, `uuid`, `vector(<dim>)` (e.g. `vector(1536)` for OpenAI embeddings), plus array forms `text[]`, `integer[]`, `bigint[]`, `numeric[]`, `boolean[]`, `uuid[]`. Use `integer` (not `int`), `numeric` (not `float`/`real`/`double`), and `jsonb` (not `json`) — the validator rejects the aliases.
32
32
 
33
33
  ### Functions (serverless code)
34
34
 
@@ -48,16 +48,21 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
48
48
 
49
49
  ### AI prompts
50
50
 
51
- Managed LLM templates: stored prompt with model + system message + variables, callable by name from the SDK. The actual LLM call is rewritten per-tenant — the customer's API keys (Anthropic / OpenAI) live in the tenant secrets, never on the device.
51
+ Managed LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant — the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.
52
+
53
+ **Two steps, in order:** register the provider key with `amba_ai_providers_set`, then create prompts against it. A prompt registered before its provider has a key still saves, but invocations fail with `provider_not_configured` (424) until the key is set. The provider key is **not** a function secret — `amba_secrets_set` writes function-scoped Worker secrets the AI gateway never reads; provider keys live in a separate gateway-owned store set only via `amba_ai_providers_set`.
52
54
 
53
55
  | Tool | Purpose | Example args |
54
56
  | --- | --- | --- |
55
- | `amba_ai_prompts_create` / `amba_create_ai_prompt` | Create a prompt template. | `{ project_id, key: "summarize", model: "claude-opus-4-5", system: "Summarize the user's text in 2 sentences.", variables: ["text"] }` |
57
+ | `amba_ai_providers_set` | Register / rotate the upstream provider API key. **Do this first.** | `{ project_id, provider: "anthropic", api_key: "sk-ant-..." }` |
58
+ | `amba_ai_providers_list` | List registered providers (`configured` = key set). | `{ project_id }` |
59
+ | `amba_ai_providers_delete` | Revoke a provider key (fails if prompts still reference it). | `{ project_id, provider: "anthropic" }` |
60
+ | `amba_ai_prompts_create` / `amba_create_ai_prompt` | Create a prompt template. | `{ project_id, name: "summarize", provider: "anthropic", model: "claude-opus-4-5", system_prompt: "Summarize the user's text in 2 sentences.", client_invokable: true }` |
56
61
  | `amba_ai_prompts_list` / `amba_list_ai_prompts` | List prompts. | `{ project_id }` |
57
- | `amba_ai_prompts_get` / `amba_get_ai_prompt` | Read one prompt. | `{ project_id, key }` |
58
- | `amba_ai_prompts_update` / `amba_update_ai_prompt` | Edit a prompt (model swap, system message change). | `{ project_id, key, system: "..." }` |
59
- | `amba_ai_prompts_invoke` / `amba_invoke_ai_prompt` | Invoke a prompt server-side (no client involvement — admin testing). | `{ project_id, key, variables: { text: "..." } }` |
60
- | `amba_ai_prompts_delete` / `amba_delete_ai_prompt` | Delete. | `{ project_id, key }` |
62
+ | `amba_ai_prompts_get` / `amba_get_ai_prompt` | Read one prompt. | `{ project_id, name }` |
63
+ | `amba_ai_prompts_update` / `amba_update_ai_prompt` | Edit a prompt (replaces all fields; bumps version). | `{ project_id, name, provider, model, system_prompt: "..." }` |
64
+ | `amba_ai_prompts_invoke` / `amba_invoke_ai_prompt` | Invoke a prompt server-side (admin testing). `messages` is a provider-shaped array. | `{ project_id, name, messages: [{ role: "user", content: "..." }] }` |
65
+ | `amba_ai_prompts_delete` / `amba_delete_ai_prompt` | Delete. | `{ project_id, name }` |
61
66
 
62
67
  ### Analytics + events + sessions
63
68
 
@@ -75,7 +80,7 @@ Managed LLM templates: stored prompt with model + system message + variables, ca
75
80
 
76
81
  | Tool | Purpose | Example args |
77
82
  | --- | --- | --- |
78
- | `amba_secrets_set` / `amba_set_secret` | Set a tenant secret (encrypted at rest). Use for third-party API keys called from functions. | `{ project_id, name: "OPENAI_API_KEY", value: "sk-..." }` |
83
+ | `amba_secrets_set` / `amba_set_secret` | Set a **function-scoped** secret (encrypted at rest; bound on the next deploy). For third-party API keys called from your functions — NOT AI provider keys (use `amba_ai_providers_set` for those). | `{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }` |
79
84
  | `amba_secrets_get` / `amba_get_secret` | Read a secret (returns `"<redacted>"` unless explicitly requested). | `{ project_id, name }` |
80
85
  | `amba_secrets_list` / `amba_list_secrets` | List secret names. | `{ project_id }` |
81
86
  | `amba_secrets_delete` / `amba_delete_secret` | Delete. | `{ project_id, name }` |
@@ -138,9 +143,9 @@ const newTodo = await Amba.collections.insert('todos', {
138
143
  await Amba.collections.update('todos', newTodo.id, { done: true });
139
144
  await Amba.collections.delete('todos', newTodo.id);
140
145
 
141
- // AI — call a managed prompt
146
+ // AI — call a managed prompt (prompt_slug names the registered prompt)
142
147
  const response = await Amba.ai.anthropic.messages.create({
143
- prompt_key: 'summarize',
148
+ prompt_slug: 'summarize',
144
149
  variables: { text: 'A long article about backend services …' },
145
150
  });
146
151
  // response.content — the model's reply
@@ -230,8 +235,7 @@ try await Amba.events.track("app_opened", properties: ["source": "deep_link"])
230
235
 
231
236
  // AI
232
237
  let reply = try await Amba.ai.anthropic.messages.create(
233
- promptKey: "summarize",
234
- variables: ["text": "A long article..."]
238
+ request: AiMessageRequest(promptSlug: "summarize", variables: ["text": "A long article..."])
235
239
  )
236
240
  ```
237
241
 
@@ -248,8 +252,7 @@ val showBeta = Amba.flags.get("beta_feature")
248
252
  Amba.events.track("app_opened", mapOf("source" to "deep_link"))
249
253
 
250
254
  val reply = Amba.ai.anthropic.messages.create(
251
- promptKey = "summarize",
252
- variables = mapOf("text" to "A long article…")
255
+ AiMessageRequest(promptSlug = "summarize", variables = mapOf("text" to "A long article…"))
253
256
  )
254
257
  ```
255
258
 
@@ -268,7 +271,7 @@ final showBeta = await Amba.flags.get('beta_feature');
268
271
  await Amba.events.track('app_opened', {'source': 'deep_link'});
269
272
 
270
273
  final reply = await Amba.ai.anthropic.messages.create(
271
- promptKey: 'summarize',
274
+ promptSlug: 'summarize',
272
275
  variables: {'text': 'A long article…'},
273
276
  );
274
277
  ```
@@ -333,7 +336,7 @@ Batch.
333
336
  2. Before creating:
334
337
  - `amba_collections_list` — match on `name`. Collisions: never silently recreate (data loss). Offer `amba_collections_alter` to add new columns instead.
335
338
  - `amba_functions_list` — match on `name`. Collisions: ask to redeploy (with the new source) or skip.
336
- - `amba_ai_prompts_list` — match on `key`. Same.
339
+ - `amba_ai_prompts_list` — match on `name`. Same. (And `amba_ai_providers_list` — match on `provider`; re-running `amba_ai_providers_set` rotates the key in place.)
337
340
  - `amba_integrations_list` — match on `provider`. Same.
338
341
  - `amba_configs_list` — match on `key`. Same.
339
342