busabase-sdk 0.18.0 → 0.19.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.
package/dist/airapp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as BusabaseClient } from "./client-CbUceGzy.js";
1
+ import { t as BusabaseClient } from "./client-slzL_ils.js";
2
2
  //#region src/airapp.d.ts
3
3
  type NodeChangeRequestInput = Parameters<BusabaseClient["nodes"]["createChangeRequest"]>[0];
4
4
  type NodeOperationInput = NodeChangeRequestInput["operations"][number];
@@ -87,6 +87,15 @@ interface AirAppResourceConfig {
87
87
  /**
88
88
  * The ownership stamp written into `node.metadata`.
89
89
  *
90
+ * Structurally identical to (and kept in lockstep with) the contract package's
91
+ * `AppResourceOwnership`, which `busabase-package`'s installer writes for the
92
+ * SAME resources when a user installs the app from the Template Center instead
93
+ * of running its `setup.mjs`. The two writers only recognise each other's work
94
+ * by this shape — drift means a user who installed through the UI and then ran
95
+ * the skill in their shell hits `SETUP_CONFLICT` on their own data. The
96
+ * assertion below is what makes that drift a compile error rather than a
97
+ * support ticket.
98
+ *
90
99
  * A type alias rather than an `interface` on purpose: `nodes.updateMetadata`
91
100
  * and the create operations take `Record<string, unknown>`, and an interface —
92
101
  * being open to declaration merging — is not assignable to an index signature.
package/dist/airapp.js CHANGED
@@ -1,5 +1,155 @@
1
+ import { z } from "zod";
2
+ const TemplateAirAppRefSchema = z.object({
3
+ /** Slug of the `content/<dir>` holding the AirApp. */
4
+ slug: z.string().min(1),
5
+ role: z.enum([
6
+ "primary",
7
+ "admin",
8
+ "public",
9
+ "tool"
10
+ ]),
11
+ label: z.string().optional()
12
+ });
13
+ /**
14
+ * Secrets the app expects to find in the Vault.
15
+ *
16
+ * DECLARED, never created: the package format has no slot for secret values and
17
+ * must not grow one (the same "you cannot leak what the format cannot express"
18
+ * rule the whole format is built on). Install surfaces these as a post-install
19
+ * prompt; the user fills them in the Vault themselves.
20
+ */
21
+ const TemplateSecretSchema = z.object({
22
+ key: z.string().min(1),
23
+ description: z.string().default(""),
24
+ required: z.boolean().default(true)
25
+ });
26
+ z.object({
27
+ /** Template Center category, e.g. `"crm"`, `"email"`, `"content"`. */
28
+ category: z.string().min(1),
29
+ tags: z.array(z.string()).default([]),
30
+ /** Card/detail screenshots, package-relative (`assets/screenshots/overview.webp`). */
31
+ screenshots: z.array(z.string()).default([]),
32
+ /**
33
+ * Ready-made prompts shown after install ("Ask agent" prefills the first).
34
+ *
35
+ * They are the difference between a folder of tables and something a user can
36
+ * *use*: the point of a template is that the agent already knows the job, and
37
+ * these are how that is made visible rather than left for the user to guess.
38
+ */
39
+ agentPrompts: z.array(z.string()).default([]),
40
+ /** Single-AirApp shorthand. Mutually exclusive with `airapps`. */
41
+ airapp: z.string().optional(),
42
+ /** Multi-AirApp form. Exactly one entry must have `role: "primary"`. */
43
+ airapps: z.array(TemplateAirAppRefSchema).optional(),
44
+ /**
45
+ * Bumped by the author when the declared resource shape changes.
46
+ *
47
+ * Part of the ownership stamp, so BOTH doors must agree on it: the installer
48
+ * writes it, and a skill's own `setup.mjs` compares against it to decide
49
+ * whether a node it finds is its own current shape or an older one to repair.
50
+ * Defaulted rather than required so an author who never versions their app
51
+ * still gets a stamp both sides recognise.
52
+ */
53
+ schemaVersion: z.number().int().nonnegative().default(1),
54
+ vaultNamespace: z.string().optional(),
55
+ secrets: z.array(TemplateSecretSchema).default([]),
56
+ requires: z.object({ airapp: z.boolean().optional() }).default({})
57
+ });
58
+ /**
59
+ * `metadata.busabase` inside the root `SKILL.md`'s YAML frontmatter.
60
+ *
61
+ * `template: true` is an EXPLICIT opt-in, not an inference from "this skill
62
+ * happens to contain a package". Publishing a template means accepting that
63
+ * installers will run its AirApp code and feed its SKILL.md to their agent; that
64
+ * deserves a deliberate flag rather than a side effect of directory shape.
65
+ */
66
+ const SkillBusabaseMetadataSchema = z.object({
67
+ template: z.boolean().default(false),
68
+ folderSlug: z.string().optional(),
69
+ /** Resource keys the manual talks about; each must exist under `content/`. */
70
+ resources: z.array(z.string()).default([]),
71
+ risk: z.string().optional()
72
+ });
73
+ z.object({
74
+ name: z.string().min(1),
75
+ description: z.string().default(""),
76
+ metadata: z.object({ busabase: SkillBusabaseMetadataSchema.optional() }).passthrough().optional()
77
+ });
78
+ /** Stamp on every resource node (Base, Drive, AirApp, …) an app owns. */
79
+ const AppResourceOwnershipSchema = z.object({
80
+ appId: z.string().min(1),
81
+ /** Stable internal handle (`"contacts"`), NOT the installed slug. */
82
+ resourceKey: z.string().min(1),
83
+ schemaVersion: z.number().int().nonnegative()
84
+ });
85
+ /**
86
+ * The `resourceKey` reserved for an app's root Folder.
87
+ *
88
+ * `busabase-sdk` recognises an app's own Folder by looking for exactly this
89
+ * value (`ownsAppRoot`), so the installer must write it too — a Folder stamped
90
+ * with anything else reads as a stranger's, and the skill's own `setup.mjs`
91
+ * then refuses to touch its own workspace with `SETUP_CONFLICT`. Exported so
92
+ * neither side carries the string literal privately.
93
+ */
94
+ const APP_ROOT_RESOURCE_KEY = "app-root";
95
+ AppResourceOwnershipSchema.extend({
96
+ resourceKey: z.literal(APP_ROOT_RESOURCE_KEY),
97
+ version: z.string().optional(),
98
+ source: z.object({
99
+ repo: z.string().optional(),
100
+ ref: z.string().optional(),
101
+ subdir: z.string().optional()
102
+ }).optional(),
103
+ installedAt: z.string().optional()
104
+ });
105
+ z.object({
106
+ appId: z.string().min(1),
107
+ ["isTemplateSkill"]: z.literal(true)
108
+ });
109
+ //#endregion
1
110
  //#region src/airapp.ts
2
111
  /**
112
+ * AirApp resource provisioning — how an app claims (or creates) the Folder and
113
+ * Bases it declares, exactly once, without ever taking over someone else's.
114
+ *
115
+ * Every App-in-Skill shipped a byte-identical copy of this module (280 lines ×
116
+ * 65 apps, two spellings). That is the wrong place for it: the rules encoded
117
+ * here are not app preferences, they are the safety boundary that keeps an app
118
+ * from adopting a Folder a human created for something else. A third party
119
+ * re-deriving them from scratch gets the happy path right and the conflict
120
+ * cases wrong, and the failure is silent — the app happily writes into data it
121
+ * does not own.
122
+ *
123
+ * The contract, in one line: **an app owns a node only if it stamped it.**
124
+ * Ownership lives in `node.metadata` as `{ appId, resourceKey, schemaVersion }`.
125
+ * Anything else is either a legacy node this app plausibly created before
126
+ * stamping existed (claimable *only* after a full structural fingerprint match)
127
+ * or someone else's (never touched, always a `SETUP_CONFLICT`).
128
+ *
129
+ * This module is isomorphic — browser and Node both — and holds no I/O beyond
130
+ * the passed-in client.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * import { createBusabaseClient } from "busabase-sdk";
135
+ * import { inspectProvisionedResources, provisionDeclaredResources } from "busabase-sdk/airapp";
136
+ *
137
+ * const client = createBusabaseClient({ baseUrl: window.location.origin });
138
+ * const config = {
139
+ * appId: "kelly-crm",
140
+ * appName: "Kelly CRM",
141
+ * schemaVersion: 1,
142
+ * folder: { slug: "kelly-crm", name: "Kelly CRM", description: "CRM workspace" },
143
+ * bases: [{ key: "contacts", slug: "kelly-crm-contacts-v1", name: "Contacts", fields: [...] }],
144
+ * };
145
+ *
146
+ * let resources = await inspectProvisionedResources(client, config);
147
+ * if (!resources.folder || resources.missing.length) {
148
+ * resources = await provisionDeclaredResources(client, config); // one idempotent ChangeRequest
149
+ * }
150
+ * ```
151
+ */
152
+ /**
3
153
  * A setup failure carrying its state as a `code`.
4
154
  *
5
155
  * `message` is deliberately kept in the historical `"CODE: detail"` shape: the
@@ -66,7 +216,7 @@ function resolveProvisionedFolder(folder, config) {
66
216
  airApp: null
67
217
  };
68
218
  if (folder.node?.type !== "folder" || folder.node?.slug !== config.folder.slug) throw setupError("SETUP_CONFLICT", `A different Folder already uses the slug ${config.folder.slug}; nothing was changed`);
69
- const rootOwned = hasResourceIdentity(folder.node, config.appId, "app-root");
219
+ const rootOwned = hasResourceIdentity(folder.node, config.appId, APP_ROOT_RESOURCE_KEY);
70
220
  const legacyRoot = hasEmptyMetadata(folder.node) && matchesDeclaration(folder.node, config.folder, "folder");
71
221
  if (!rootOwned && !legacyRoot) throw setupError("SETUP_CONFLICT", `The Folder ${config.folder.slug} does not belong to this app; nothing was changed`);
72
222
  const bases = [];
@@ -74,8 +224,8 @@ function resolveProvisionedFolder(folder, config) {
74
224
  const repairs = [];
75
225
  if (!ownsAppRoot(folder.node, config.appId, config.schemaVersion)) repairs.push({
76
226
  nodeId: folder.node.id,
77
- resourceKey: "app-root",
78
- metadata: resourceMetadata(config, "app-root")
227
+ resourceKey: APP_ROOT_RESOURCE_KEY,
228
+ metadata: resourceMetadata(config, APP_ROOT_RESOURCE_KEY)
79
229
  });
80
230
  for (const base of config.bases) {
81
231
  const matches = (folder.children ?? []).filter((node) => node.slug === base.slug);
@@ -139,7 +289,7 @@ function buildProvisionOperations(config, folder, missingBases) {
139
289
  slug: config.folder.slug,
140
290
  name: config.folder.name,
141
291
  description: config.folder.description ?? "",
142
- metadata: resourceMetadata(config, "app-root")
292
+ metadata: resourceMetadata(config, APP_ROOT_RESOURCE_KEY)
143
293
  });
144
294
  for (const base of missingBases) operations.push({
145
295
  kind: "create",
@@ -10635,6 +10635,7 @@ declare const cloudContract: {
10635
10635
  workspace: "workspace";
10636
10636
  }>>>;
10637
10637
  version: z.ZodDefault<z.ZodOptional<z.ZodString>>;
10638
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
10638
10639
  files: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
10639
10640
  path: z.ZodString;
10640
10641
  assetId: z.ZodString;
@@ -13565,6 +13566,7 @@ declare const cloudContract: {
13565
13566
  }>;
13566
13567
  version: z.ZodDefault<z.ZodNullable<z.ZodString>>;
13567
13568
  available: z.ZodDefault<z.ZodBoolean>;
13569
+ comingSoon: z.ZodDefault<z.ZodBoolean>;
13568
13570
  unavailableReason: z.ZodDefault<z.ZodNullable<z.ZodString>>;
13569
13571
  connectionRequired: z.ZodDefault<z.ZodBoolean>;
13570
13572
  connectedAgentName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
@@ -14336,6 +14338,41 @@ declare const cloudContract: {
14336
14338
  warnings: z.ZodDefault<z.ZodArray<z.ZodString>>;
14337
14339
  }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
14338
14340
  };
14341
+ templates: {
14342
+ list: import("@orpc/contract").ContractProcedure<z.ZodDefault<z.ZodOptional<z.ZodObject<{
14343
+ refresh: z.ZodOptional<z.ZodBoolean>;
14344
+ }, z.core.$strip>>>, z.ZodObject<{
14345
+ templates: z.ZodArray<z.ZodObject<{
14346
+ id: z.ZodString;
14347
+ name: z.ZodString;
14348
+ description: z.ZodString;
14349
+ category: z.ZodString;
14350
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
14351
+ screenshots: z.ZodDefault<z.ZodArray<z.ZodString>>;
14352
+ agentPrompts: z.ZodDefault<z.ZodArray<z.ZodString>>;
14353
+ version: z.ZodOptional<z.ZodString>;
14354
+ author: z.ZodOptional<z.ZodString>;
14355
+ license: z.ZodOptional<z.ZodString>;
14356
+ stats: z.ZodObject<{
14357
+ folders: z.ZodNumber;
14358
+ docs: z.ZodNumber;
14359
+ bases: z.ZodNumber;
14360
+ records: z.ZodNumber;
14361
+ files: z.ZodNumber;
14362
+ airapps: z.ZodNumber;
14363
+ skill: z.ZodBoolean;
14364
+ }, z.core.$strip>;
14365
+ install: z.ZodObject<{
14366
+ repoUrl: z.ZodString;
14367
+ intoFolder: z.ZodString;
14368
+ }, z.core.$strip>;
14369
+ sourceUrl: z.ZodString;
14370
+ }, z.core.$strip>>;
14371
+ repo: z.ZodString;
14372
+ ref: z.ZodString;
14373
+ error: z.ZodOptional<z.ZodString>;
14374
+ }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
14375
+ };
14339
14376
  changeRequests: {
14340
14377
  list: import("@orpc/contract").ContractProcedure<z.ZodDefault<z.ZodOptional<z.ZodObject<{
14341
14378
  limit: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createBusabaseClient, c as cloudContract, d as NodeIconSchema, f as CREATABLE_NODE_TYPES, h as OperationKind, i as ResolvedConfig, l as NodeOutput, m as NodeType, n as BusabaseConfig, o as resolveConfig, p as CreatableNodeType, r as DEFAULT_BASE_URL, s as CloudContract, t as BusabaseClient, u as NodeIcon } from "./client-CbUceGzy.js";
1
+ import { a as createBusabaseClient, c as cloudContract, d as NodeIconSchema, f as CREATABLE_NODE_TYPES, h as OperationKind, i as ResolvedConfig, l as NodeOutput, m as NodeType, n as BusabaseConfig, o as resolveConfig, p as CreatableNodeType, r as DEFAULT_BASE_URL, s as CloudContract, t as BusabaseClient, u as NodeIcon } from "./client-slzL_ils.js";
2
2
  import { z } from "zod";
3
3
  //#region src/url.d.ts
4
4
  /**
package/dist/index.js CHANGED
@@ -86,6 +86,8 @@ const AgentCatalogEntryVOSchema = z.object({
86
86
  version: z.string().nullable().default(null),
87
87
  /** Whether this entry can be launched right now (binary present / URL configured). */
88
88
  available: z.boolean().default(false),
89
+ /** Whether this integration is listed for discovery but not available yet. */
90
+ comingSoon: z.boolean().default(false),
89
91
  /** Human-readable reason when `available` is false — never a bare "failed". */
90
92
  unavailableReason: z.string().nullable().default(null),
91
93
  connectionRequired: z.boolean().default(false),
@@ -1616,6 +1618,17 @@ const createFileTreeInputSchema = z.object({
1616
1618
  "public"
1617
1619
  ]).optional().default("private"),
1618
1620
  version: z.string().optional().default("0.1.0"),
1621
+ /**
1622
+ * Extra node metadata, stored alongside the server-owned keys.
1623
+ *
1624
+ * Rides along the change request on the review-first path, so it lands when a
1625
+ * human merges. That is the whole point: an ownership stamp applied only
1626
+ * after an immediate create would silently never be applied to a node that
1627
+ * was proposed instead — leaving the app unable to recognise its own
1628
+ * resources on the DEFAULT install path. Server-owned keys (`visibility`,
1629
+ * `version`) always win, so a caller cannot use this to rewrite them.
1630
+ */
1631
+ metadata: z.record(z.string(), z.unknown()).optional(),
1619
1632
  files: z.array(z.union([assetFileInputSchema, textFileInputSchema])).optional().default([]),
1620
1633
  autoMerge: z.boolean().optional(),
1621
1634
  mergeMode: z.enum(["merge", "replace"]).optional().default("merge")
@@ -3146,6 +3159,92 @@ const installContract = {
3146
3159
  }).input(InstallFromGithubDTOSchema).output(InstallResultVOSchema)
3147
3160
  };
3148
3161
  //#endregion
3162
+ //#region ../../packages/busabase-contract/src/domains/templates/types.ts
3163
+ /**
3164
+ * Template Center catalog types (pure zod, client-safe).
3165
+ *
3166
+ * The catalog is the file `busabase-cli index` builds from a skills repository
3167
+ * — see `busabase-package/index-build`. It is re-declared here rather than
3168
+ * imported because that module is Node-only (it reads packages), and these
3169
+ * shapes are rendered in a browser.
3170
+ *
3171
+ * Spec: `apps/busabase/content/spec/template-center.md` §6.4.
3172
+ */
3173
+ const TemplateStatsVOSchema = z.object({
3174
+ folders: z.number().int(),
3175
+ docs: z.number().int(),
3176
+ bases: z.number().int(),
3177
+ records: z.number().int(),
3178
+ files: z.number().int(),
3179
+ airapps: z.number().int(),
3180
+ skill: z.boolean()
3181
+ });
3182
+ const TemplateCardVOSchema = z.object({
3183
+ /** Stable across a catalog: `<repo>/<subdir>`. What a route keys on. */
3184
+ id: z.string(),
3185
+ name: z.string(),
3186
+ description: z.string(),
3187
+ category: z.string(),
3188
+ tags: z.array(z.string()).default([]),
3189
+ /**
3190
+ * Absolute URLs, resolved server-side.
3191
+ *
3192
+ * The catalog stores package-relative paths; turning them into URLs needs to
3193
+ * know the repo and ref, which the server already has and the browser would
3194
+ * otherwise have to re-derive. Doing it once here also means a card cannot
3195
+ * accidentally point at a different ref than the one it installs.
3196
+ */
3197
+ screenshots: z.array(z.string()).default([]),
3198
+ agentPrompts: z.array(z.string()).default([]),
3199
+ version: z.string().optional(),
3200
+ author: z.string().optional(),
3201
+ license: z.string().optional(),
3202
+ stats: TemplateStatsVOSchema,
3203
+ /** Exactly what the install dialog needs — no URL assembly in the client. */
3204
+ install: z.object({
3205
+ repoUrl: z.string(),
3206
+ intoFolder: z.string()
3207
+ }),
3208
+ /** Where a curious user goes to read it before installing. */
3209
+ sourceUrl: z.string()
3210
+ });
3211
+ const TemplateCatalogVOSchema = z.object({
3212
+ templates: z.array(TemplateCardVOSchema),
3213
+ /** `owner/repo` and ref the catalog was built from — shown as provenance. */
3214
+ repo: z.string(),
3215
+ ref: z.string(),
3216
+ /**
3217
+ * Why the catalog is empty or stale, in the server's own words.
3218
+ *
3219
+ * A gallery that silently shows nothing is indistinguishable from one that is
3220
+ * broken, and the difference matters: "the catalog could not be fetched" is a
3221
+ * thing a user can act on, "no templates" is not.
3222
+ */
3223
+ error: z.string().optional()
3224
+ });
3225
+ const ListTemplatesDTOSchema = z.object({
3226
+ /** Bypass the cache — the refresh button. */
3227
+ refresh: z.boolean().optional() }).optional().default({});
3228
+ //#endregion
3229
+ //#region ../../packages/busabase-contract/src/domains/templates/contract.ts
3230
+ /**
3231
+ * Template Center — the catalog a user browses before installing.
3232
+ *
3233
+ * Read-only and server-side on purpose. The catalog lives in a GitHub
3234
+ * repository, and a browser fetching it directly would hit CORS, would have no
3235
+ * cache shared between users, and would let the page decide which host to trust.
3236
+ * Installing is NOT here: a card's button hands its URL to the existing
3237
+ * `install.*` routes, so browsing and installing cannot disagree about what a
3238
+ * package is or who is allowed to install it.
3239
+ */
3240
+ const templatesContract = { list: oc.route({
3241
+ method: "GET",
3242
+ path: "/templates",
3243
+ tags: ["Templates"],
3244
+ summary: "List the Template Center catalog",
3245
+ successDescription: "The templates this server's configured catalog publishes, with provenance and per-template stats. `error` is set when the catalog could not be fetched, so an empty gallery can say why."
3246
+ }).input(ListTemplatesDTOSchema).output(TemplateCatalogVOSchema) };
3247
+ //#endregion
3149
3248
  //#region ../../packages/busabase-contract/src/domains/vault/types.ts
3150
3249
  const VaultItemKeySchema = z.string().trim().min(1).max(128).regex(/^[A-Z_][A-Z0-9_]*$/, "Use uppercase letters, numbers, and underscores");
3151
3250
  const VaultItemValueSchema = z.string().max(8192);
@@ -4210,6 +4309,7 @@ const busabaseContractRoutes = {
4210
4309
  webhooks: webhookContract,
4211
4310
  dump: dumpContract,
4212
4311
  install: installContract,
4312
+ templates: templatesContract,
4213
4313
  changeRequests: {
4214
4314
  list: oc.route({
4215
4315
  method: "GET",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busabase-sdk",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Typed TypeScript/JavaScript SDK for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server` (or Busabase Cloud).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-sdk",
@@ -63,9 +63,9 @@
63
63
  "tsx": "^4.20.5",
64
64
  "typescript": "^7.0.2",
65
65
  "vitest": "^2.1.8",
66
- "busabase-contract": "0.18.0",
67
- "open-domains": "0.0.2",
68
- "openlib": "0.1.1"
66
+ "busabase-contract": "0.19.0",
67
+ "openlib": "0.1.1",
68
+ "open-domains": "0.0.2"
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=24.18.0"