busabase-sdk 0.30.0 → 0.40.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as FileTreeReadFileVO, A as SearchResultKind, At as AttachmentRef, B as VaultRuntimeEnv, C as NodeSearchResultVO, Ct as VIEW_FIELD_MIN_WIDTH, D as ReviewVO, Dt as ViewSortVO, E as OperationVO, Et as ViewFilterVO, F as VaultAccessPolicy, Ft as CREATABLE_NODE_TYPES, G as AssetDetailVO, H as VaultSettingsVO, I as VaultEnvironment, It as CreatableNodeType, J as AssetVO, K as AssetTextStatus, L as VaultItemInput, Lt as NodeType, M as SourceAttributionVO, Mt as cloudContract, N as UserRefVO, Nt as NodeIcon, O as ReviewVerdict, Ot as ViewType, P as UpdateVaultSettingsDTO, Pt as NodeIconSchema, Q as FileTreeNodeVO, R as VaultItemKind, Rt as OperationKind, S as LookupRollup, St as VIEW_FIELD_MAX_WIDTH, T as OperationStatus, Tt as ViewFilterOperator, U as FileNodeMetadata, V as VaultScopeType, W as FileNodeVO, X as GrepResultVO, Y as GrepInputDTO, Z as FileTreeFileVO, _ as ChangeRequestVO, _t as GalleryCardSize, a as createBusabaseClient, at as FormPageSourceVO, b as CommitVO, bt as RecordLinkVO, c as AuditAction, ct as FormThemeVO, d as ChangeRequestBatchFailureVO, dt as ListFormsVO, et as NodeDetailVO, f as ChangeRequestCountsVO, ft as SubmitFormDTO, g as ChangeRequestTargetType, gt as BaseVO, h as ChangeRequestStatus, ht as BaseFieldVO, i as ResolvedConfig, it as FormFieldBindingVO, j as SearchResultVO, jt as CloudContract, k as SearchResponseVO, kt as ViewVO, l as AuditEventVO, lt as FormVO, m as ChangeRequestReviewBatchResultVO, mt as AssetAttachmentRef, n as BusabaseConfig, nt as CreateFormDTO, o as resolveConfig, ot as FormShareVO, p as ChangeRequestMergeBatchResultVO, pt as UpdateFormDTO, q as AssetUsageVO, r as DEFAULT_BASE_URL, rt as FormBoundFieldVO, s as AgentTaskVO, st as FormSubmitResultVO, t as BusabaseClient, tt as ActivityItemVO, u as BusabaseSourceChannel, ut as ListFormsDTO, v as CommentSubjectType, vt as GalleryCoverFit, w as NodeVO, wt as ViewConfigVO, x as FieldType, xt as RecordVO, y as CommentVO, yt as GanttScale, z as VaultItemVO } from "./client-CVz2pqrF.js";
1
+ import { $ as FileTreeReadFileVO, A as SearchResultKind, At as AttachmentRef, B as VaultRuntimeEnv, C as NodeSearchResultVO, Ct as VIEW_FIELD_MIN_WIDTH, D as ReviewVO, Dt as ViewSortVO, E as OperationVO, Et as ViewFilterVO, F as VaultAccessPolicy, Ft as CREATABLE_NODE_TYPES, G as AssetDetailVO, H as VaultSettingsVO, I as VaultEnvironment, It as CreatableNodeType, J as AssetVO, K as AssetTextStatus, L as VaultItemInput, Lt as NodeType, M as SourceAttributionVO, Mt as cloudContract, N as UserRefVO, Nt as NodeIcon, O as ReviewVerdict, Ot as ViewType, P as UpdateVaultSettingsDTO, Pt as NodeIconSchema, Q as FileTreeNodeVO, R as VaultItemKind, Rt as OperationKind, S as LookupRollup, St as VIEW_FIELD_MAX_WIDTH, T as OperationStatus, Tt as ViewFilterOperator, U as FileNodeMetadata, V as VaultScopeType, W as FileNodeVO, X as GrepResultVO, Y as GrepInputDTO, Z as FileTreeFileVO, _ as ChangeRequestVO, _t as GalleryCardSize, a as createBusabaseClient, at as FormPageSourceVO, b as CommitVO, bt as RecordLinkVO, c as AuditAction, ct as FormThemeVO, d as ChangeRequestBatchFailureVO, dt as ListFormsVO, et as NodeDetailVO, f as ChangeRequestCountsVO, ft as SubmitFormDTO, g as ChangeRequestTargetType, gt as BaseVO, h as ChangeRequestStatus, ht as BaseFieldVO, i as ResolvedConfig, it as FormFieldBindingVO, j as SearchResultVO, jt as CloudContract, k as SearchResponseVO, kt as ViewVO, l as AuditEventVO, lt as FormVO, m as ChangeRequestReviewBatchResultVO, mt as AssetAttachmentRef, n as BusabaseConfig, nt as CreateFormDTO, o as resolveConfig, ot as FormShareVO, p as ChangeRequestMergeBatchResultVO, pt as UpdateFormDTO, q as AssetUsageVO, r as DEFAULT_BASE_URL, rt as FormBoundFieldVO, s as AgentTaskVO, st as FormSubmitResultVO, t as BusabaseClient, tt as ActivityItemVO, u as BusabaseSourceChannel, ut as ListFormsDTO, v as CommentSubjectType, vt as GalleryCoverFit, w as NodeVO, wt as ViewConfigVO, x as FieldType, xt as RecordVO, y as CommentVO, yt as GanttScale, z as VaultItemVO } from "./client-CWlHll4H.js";
2
2
  import { z } from "zod";
3
3
  //#region src/url.d.ts
4
4
  /**
package/dist/index.js CHANGED
@@ -354,7 +354,7 @@ const LocaleSchema = z.enum({
354
354
  * - ZH: isIString 接受 JSON 字符串并尝试解析为按语言分布的对象;纯字符串将返回 false。
355
355
  */
356
356
  const iStringRecordSchema = z.partialRecord(LocaleSchema, z.string());
357
- z.union([z.string(), iStringRecordSchema]).describe("i18n string");
357
+ const iStringSchema = z.union([z.string(), iStringRecordSchema]).describe("i18n string");
358
358
  //#endregion
359
359
  //#region ../../packages/busabase-contract/src/contract/auto-merge.ts
360
360
  /**
@@ -500,7 +500,8 @@ const baseSchema = z.object({
500
500
  requiredApprovals: z.number()
501
501
  }),
502
502
  createdAt: z.string(),
503
- fields: z.array(baseFieldSchema)
503
+ fields: z.array(baseFieldSchema),
504
+ metadata: z.record(z.string(), z.unknown()).default({})
504
505
  });
505
506
  const createBaseInputSchema = z.object({
506
507
  parentNodeId: z.string().optional().describe("Parent node id. Must be a folder or the space root; container-incapable node types (Base, Doc, AirApp, etc.) cannot hold children."),
@@ -1081,6 +1082,81 @@ const NodeIconSchema = z.discriminatedUnion("type", [z.object({
1081
1082
  }).optional()
1082
1083
  })]);
1083
1084
  //#endregion
1085
+ //#region ../../packages/busabase-contract/src/contract/node-agent-prompt-schemas.ts
1086
+ /**
1087
+ * Per-node custom scenario prompts (`node.metadata.agentPrompts`) — the single
1088
+ * schema shared by BOTH write and read paths, per
1089
+ * `apps/busabase-cloud/content/spec/node-agent-prompts-v2.md` §7.3:
1090
+ *
1091
+ * - Write: `busabase-cli nodes set-agent-prompts` validates a file against this
1092
+ * schema client-side, before calling `nodes.updateMetadata` — a malformed file
1093
+ * never reaches the network.
1094
+ * - Read: `buildNodeAgentPrompts` (`packages/busabase-core/.../node-agent-prompts.ts`)
1095
+ * `.safeParse`s whatever is stored in `metadata.agentPrompts` and falls back to
1096
+ * the node type's default scenarios on failure, so a bad write (or a manual
1097
+ * jsonb edit) can never crash the Agent Prompts dialog.
1098
+ *
1099
+ * One schema, one place to edit the limits — do not duplicate this validation on
1100
+ * either side.
1101
+ */
1102
+ /** Recommended limits from the spec (§7.3) — kept as named constants so a limit
1103
+ * change has exactly one place to edit, and so error messages can cite them. */
1104
+ const CUSTOM_AGENT_PROMPT_LIMITS = {
1105
+ /** Max custom prompts per node. */
1106
+ maxPrompts: 50,
1107
+ /** Max characters per localized `label` value. */
1108
+ maxLabelChars: 80,
1109
+ /** Max bytes (UTF-8) per localized `body` value. */
1110
+ maxBodyBytes: 8192
1111
+ };
1112
+ /** Same intent vocabulary `PromptDef.intent` uses — `change` is the router-side
1113
+ * default when omitted, matching the existing curated-prompt behavior. */
1114
+ const customPromptIntentSchema = z.enum(["read-only", "change"]);
1115
+ const iStringLocaleValues = (value) => typeof value === "string" ? [value] : Object.values(value);
1116
+ /** UTF-8 byte length — `body` includes CJK/JA text, whose per-character byte
1117
+ * cost is not 1, so a character-count limit alone would under-enforce §7.3's
1118
+ * "8 KiB per localized body" budget. */
1119
+ const utf8ByteLength = (value) => new TextEncoder().encode(value).length;
1120
+ const customPromptLabelSchema = iStringSchema.refine((value) => iStringLocaleValues(value).every((v) => v.length <= CUSTOM_AGENT_PROMPT_LIMITS.maxLabelChars), { message: `label must be at most ${CUSTOM_AGENT_PROMPT_LIMITS.maxLabelChars} characters per locale` });
1121
+ const customPromptBodySchema = iStringSchema.refine((value) => iStringLocaleValues(value).every((v) => utf8ByteLength(v) <= CUSTOM_AGENT_PROMPT_LIMITS.maxBodyBytes), { message: `body must be at most ${CUSTOM_AGENT_PROMPT_LIMITS.maxBodyBytes} bytes (UTF-8) per locale` });
1122
+ /**
1123
+ * One custom scenario prompt. `body`'s `{target}` placeholder is substituted at
1124
+ * render time with the same target string `PromptDef.body(target)` receives
1125
+ * today (see `node-agent-prompts.ts`) — this schema does not interpolate it.
1126
+ */
1127
+ const customPromptDefSchema = z.object({
1128
+ /** Stable id, unique within this node's custom list. */
1129
+ key: z.string().trim().min(1, { message: "key must not be empty" }),
1130
+ /** Defaults to `change` (same default the curated prompts use) so a prompt
1131
+ * cannot silently bypass the approval-first policy by omission. */
1132
+ intent: customPromptIntentSchema.optional(),
1133
+ /** Short title shown in the dialog's left list. */
1134
+ label: customPromptLabelSchema,
1135
+ /** Template text; "{target}" is substituted with the rendered target line. */
1136
+ body: customPromptBodySchema
1137
+ });
1138
+ /**
1139
+ * The full `metadata.agentPrompts` array. `.max` bounds the list; the
1140
+ * `superRefine` catches duplicate keys with an issue path pointing at the
1141
+ * offending entry, so a CLI/server validation error names which entry and
1142
+ * which key collided instead of a bare "keys must be unique".
1143
+ */
1144
+ const customAgentPromptsSchema = z.array(customPromptDefSchema).max(CUSTOM_AGENT_PROMPT_LIMITS.maxPrompts, { message: `at most ${CUSTOM_AGENT_PROMPT_LIMITS.maxPrompts} custom prompts per node` }).superRefine((prompts, ctx) => {
1145
+ const firstIndexByKey = /* @__PURE__ */ new Map();
1146
+ prompts.forEach((prompt, index) => {
1147
+ const firstIndex = firstIndexByKey.get(prompt.key);
1148
+ if (firstIndex === void 0) {
1149
+ firstIndexByKey.set(prompt.key, index);
1150
+ return;
1151
+ }
1152
+ ctx.addIssue({
1153
+ code: "custom",
1154
+ path: [index, "key"],
1155
+ message: `duplicate key "${prompt.key}" — already used at entry ${firstIndex + 1}`
1156
+ });
1157
+ });
1158
+ });
1159
+ //#endregion
1084
1160
  //#region ../../packages/busabase-contract/src/contract/schemas.ts
1085
1161
  const nodeSchema = z.lazy(() => z.object({
1086
1162
  id: z.string(),
@@ -1090,6 +1166,7 @@ const nodeSchema = z.lazy(() => z.object({
1090
1166
  name: z.string(),
1091
1167
  description: z.string(),
1092
1168
  metadata: z.object({ version: z.string().optional() }).catchall(z.unknown()).default({}),
1169
+ settings: nodeSettingsSchema.default({}),
1093
1170
  explicitVisibility: z.enum([
1094
1171
  "private",
1095
1172
  "workspace",
@@ -1173,6 +1250,65 @@ const updateNodeMetadataInputSchema = z.object({
1173
1250
  nodeId: z.string(),
1174
1251
  metadata: z.record(z.string(), z.unknown())
1175
1252
  });
1253
+ /**
1254
+ * System settings Busabase itself acts on, per node.
1255
+ *
1256
+ * A CLOSED object, unlike `metadata`'s `z.record(z.string(), z.unknown())`.
1257
+ * That difference is the point: `metadata` is the open bag callers put their
1258
+ * own keys in, and its endpoint merges whatever it is handed. Anything the
1259
+ * product *reads and acts on* must not live there — `metadata.visibility` had
1260
+ * to be pulled out into its own column precisely because the generic endpoint
1261
+ * handed a `write`-level actor a door into an ACL input.
1262
+ *
1263
+ * Adding a key here is a deliberate change to this schema, which is what keeps
1264
+ * `PATCH /nodes/{nodeId}/settings` from becoming a second free-form bag.
1265
+ *
1266
+ * `strictObject`, not `object`: zod's default is to STRIP an unknown key, which
1267
+ * would hand a client that misspelled `airappEngine` a 200 and no effect —
1268
+ * exactly the silent failure a closed schema exists to prevent. Verified
1269
+ * against a running server, where the permissive version returned 200.
1270
+ */
1271
+ const nodeSettingsSchema = z.strictObject({
1272
+ /**
1273
+ * Which engine an AirApp runs on, when a human has chosen.
1274
+ *
1275
+ * `undefined` means "follow the app" — `airapp.json`'s `preferredEngine`
1276
+ * decides, or the default does. A value means somebody overrode it in the
1277
+ * node settings dialog, and it outranks the manifest from then on. So absence
1278
+ * must stay distinguishable from any particular value; `null` clears an
1279
+ * override and returns the node to following the app.
1280
+ */
1281
+ airappEngine: z.enum([
1282
+ "browser",
1283
+ "local",
1284
+ "remote"
1285
+ ]).nullish() });
1286
+ const updateNodeSettingsInputSchema = z.object({
1287
+ nodeId: z.string(),
1288
+ settings: nodeSettingsSchema
1289
+ });
1290
+ /**
1291
+ * This node's custom scenario prompts.
1292
+ *
1293
+ * A separate read from `nodes.get`/`nodes.list` on purpose: the list is capped
1294
+ * at 50 prompts x 8 KiB of body per locale, so carrying it on every node of
1295
+ * every sidebar load is exactly what its own column exists to avoid. The dialog
1296
+ * asks for it when it opens.
1297
+ *
1298
+ * `agentPrompts: null` means "this node has never set any" — distinct from `[]`,
1299
+ * which means "set, and deliberately empty". Both fall back to the node type's
1300
+ * default scenarios in the UI, but only the first is a node nobody has touched.
1301
+ */
1302
+ const getNodeAgentPromptsInputSchema = z.object({ nodeId: z.string() });
1303
+ const nodeAgentPromptsSchema = z.object({
1304
+ nodeId: z.string(),
1305
+ agentPrompts: customAgentPromptsSchema.nullable()
1306
+ });
1307
+ const updateNodeAgentPromptsInputSchema = z.object({
1308
+ nodeId: z.string(),
1309
+ /** Replaces the whole list — this is not a merge. Send `null` to clear. */
1310
+ agentPrompts: customAgentPromptsSchema.nullable()
1311
+ });
1176
1312
  const searchNodesByNameInputSchema = z.object({
1177
1313
  query: z.string().min(1),
1178
1314
  limit: z.coerce.number().int().min(1).max(50).optional().default(20)
@@ -1368,7 +1504,8 @@ const liveEventSchema = z.object({
1368
1504
  "change_request.reviewed",
1369
1505
  "change_request.merged",
1370
1506
  "change_request.pending_review",
1371
- "node.metadata_updated"
1507
+ "node.metadata_updated",
1508
+ "node.settings_updated"
1372
1509
  ]),
1373
1510
  spaceId: z.string(),
1374
1511
  actorId: z.string(),
@@ -1399,6 +1536,8 @@ const auditActionSchema = z.enum([
1399
1536
  "asset.text_written",
1400
1537
  "asset.text_marked_none",
1401
1538
  "node.metadata_updated",
1539
+ "node.settings_updated",
1540
+ "node.agent_prompts_updated",
1402
1541
  "node.purged"
1403
1542
  ]);
1404
1543
  const auditEventSchema = z.object({
@@ -1814,16 +1953,17 @@ const airAppRunLocalInputSchema = z.object({
1814
1953
  * The in-browser Nodepod engine never uses this field: it hands raw bytes to
1815
1954
  * `Nodepod.boot({ files })` directly and skips the base64 round trip. */
1816
1955
  binaryFiles: z.record(z.string(), z.string()).optional().default({}),
1817
- /** Server-side execution mode. `"local"` spawns a bare host Node.js
1818
- * process (previewable, data bridge via reverse proxy, NOT OS-isolated);
1819
- * `"srt"` wraps the same commands in the OS sandbox (isolated execution,
1820
- * but live preview is unreachable). `"nodepod"` never calls this endpoint —
1821
- * it runs entirely in-browser. */
1822
- engine: z.enum([
1823
- "local",
1824
- "srt",
1825
- "sandock"
1826
- ]).default("local")
1956
+ /** Where the server should run it. `"local"` spawns a bare process on the
1957
+ * Busabase host (previewable, data bridge via reverse proxy, NOT isolated);
1958
+ * `"remote"` runs the same lifecycle on a provisioned machine elsewhere.
1959
+ * `"browser"` never reaches this endpoint — it runs entirely in the tab.
1960
+ *
1961
+ * Required, deliberately. This used to default to `"local"`, so a call that
1962
+ * simply omitted the field asked the server to spawn a host process — the
1963
+ * most privileged of the two options, reached by saying nothing. Naming the
1964
+ * engine is now the caller's job, and the handler independently refuses one
1965
+ * this deployment does not offer. */
1966
+ engine: z.enum(["local", "remote"])
1827
1967
  });
1828
1968
  const airAppRuntimeEventSchema = z.discriminatedUnion("type", [
1829
1969
  z.object({
@@ -2414,6 +2554,41 @@ const listRecordsResponseSchema = z.object({
2414
2554
  const listRecordsPageInputSchema = z.object({
2415
2555
  baseId: z.string().min(1),
2416
2556
  viewId: z.string().min(1).optional(),
2557
+ /**
2558
+ * Extra conditions ANDed with the View's own filters — "this View, further
2559
+ * narrowed". The motivating case is one board column: the saved View's
2560
+ * filters plus `stackField equals <choice>`, paged independently of the
2561
+ * other columns.
2562
+ *
2563
+ * Unlike `records.list`'s `filters` (a SUPERSET push-down the client then
2564
+ * narrows), these are applied with the same authority as a saved View's:
2565
+ * every returned page is exactly what the client's own matcher would keep.
2566
+ * That distinction is the whole point — a *superset* page can be missing
2567
+ * records, and a board column that silently drops cards reads as data loss.
2568
+ */
2569
+ filters: z.array(listRecordsFilterSchema).optional(),
2570
+ /**
2571
+ * Scope the page to records whose `date`/`created_time`/`updated_time` field
2572
+ * falls in `[gte, lt)` — an absolute UTC instant range, not a `filters`
2573
+ * condition. It is deliberately NOT an operator on `listRecordsFilterSchema`:
2574
+ * that model mirrors the client's label-based view-filter matching (see
2575
+ * `recordMatchesViewFilter`), which for a date renders via
2576
+ * `toLocaleDateString()` — meaningless without knowing the viewer's
2577
+ * timezone, which the server never has. A UTC instant range has no such
2578
+ * ambiguity, so it is resolved once here, by the caller (who DOES know the
2579
+ * viewer's timezone), and applied as a real timestamp comparison.
2580
+ *
2581
+ * The motivating case is a Calendar month grid: the client computes the UTC
2582
+ * bounds of its own local 42-day grid and asks for only that slice, instead
2583
+ * of every record in the Base.
2584
+ */
2585
+ dateRange: z.object({
2586
+ fieldSlug: z.string().min(1),
2587
+ /** Inclusive lower bound, ISO 8601 UTC instant. */
2588
+ gte: z.string(),
2589
+ /** Exclusive upper bound, ISO 8601 UTC instant. */
2590
+ lt: z.string()
2591
+ }).optional(),
2417
2592
  page: z.coerce.number().int().min(1).optional().default(1),
2418
2593
  pageSize: z.coerce.number().int().min(1).max(100).optional().default(50)
2419
2594
  });
@@ -2458,6 +2633,42 @@ const countRecordsInputSchema = z.object({
2458
2633
  const countRecordsResponseSchema = z.object({
2459
2634
  /** Total active records in the space (optionally scoped to a base). */
2460
2635
  total: z.number().int().nonnegative() });
2636
+ const groupRecordsInputSchema = z.object({
2637
+ /** Group within exactly one Base — a field slug is only unambiguous there. */
2638
+ baseId: z.string().min(1),
2639
+ /**
2640
+ * The field to group by. Restricted to `select` and `checkbox`: their stored
2641
+ * value IS the grouping key (a choice id / a boolean), so a SQL GROUP BY
2642
+ * returns exactly the buckets a client would build. Text/number keys would
2643
+ * be truncated at the projection limit, and date keys would bucket by the
2644
+ * server's timezone rather than the viewer's — both would report a
2645
+ * confidently wrong split, so they're rejected instead of approximated.
2646
+ */
2647
+ fieldSlug: z.string().min(1),
2648
+ /** Group only the rows a saved View would display (its filters; sort ignored). */
2649
+ viewId: z.string().min(1).optional(),
2650
+ /** Ad-hoc filters, ANDed with the View's own when both are given. */
2651
+ filters: z.array(listRecordsFilterSchema).optional()
2652
+ });
2653
+ const groupRecordsResponseSchema = z.object({
2654
+ groups: z.array(z.object({
2655
+ /**
2656
+ * The raw stored key: a `select` choice id, or `"true"`/`"false"` for a
2657
+ * checkbox. For a select, `null` is the bucket of records with no value
2658
+ * (what a Kanban board shows as its "Uncategorized" column). A checkbox
2659
+ * never reports `null` — an unset checkbox counts as `"false"`, matching
2660
+ * how view filters already treat it (`is_false` covers null/undefined).
2661
+ *
2662
+ * Choice LABELS are deliberately not resolved here — the client already
2663
+ * holds the Base's field definitions and renders labels itself, and
2664
+ * returning ids keeps this response stable across a choice rename.
2665
+ */
2666
+ value: z.string().nullable(),
2667
+ count: z.number().int().nonnegative()
2668
+ })),
2669
+ /** Sum of every group's count — the same number `records.count` would return. */
2670
+ total: z.number().int().nonnegative()
2671
+ });
2461
2672
  const createChangeRequestInputSchema = z.object({
2462
2673
  fields: z.record(z.string(), z.unknown()).describe("Record field values keyed by field slug. The base's PRIMARY field (its first field) becomes the record's display name and the change request title everywhere — always give it a short, human-readable value, never an id or placeholder."),
2463
2674
  message: z.string().optional().default("Initial change request").describe("Explanation shown to the human reviewer. Write a conventional-commit style subject — imperative verb + what + why, e.g. \"Add Acme Corp — qualified lead from the June webinar\"."),
@@ -2657,7 +2868,7 @@ const recordContract = {
2657
2868
  path: "/records/page",
2658
2869
  tags: ["Records"],
2659
2870
  summary: "List a numbered record page",
2660
- successDescription: "A random-access page of active records. When viewId is supplied, the saved view is authoritatively filtered and sorted before total and page slicing are calculated."
2871
+ successDescription: "A random-access page of active records. When viewId is supplied, the saved view is authoritatively filtered and sorted before total and page slicing are calculated. `dateRange` additionally scopes to a `[gte, lt)` UTC instant window on a date/created_time/updated_time field."
2661
2872
  }).input(listRecordsPageInputSchema).output(listRecordsPageResponseSchema),
2662
2873
  count: oc.route({
2663
2874
  method: "GET",
@@ -2667,6 +2878,23 @@ const recordContract = {
2667
2878
  description: "A real SQL COUNT — always the exact total, never a partial or capped number, so it's safe to render as a canonical figure (e.g. a dashboard summary tile). Plain `baseId` scoping is always cheap. Adding `viewId` and/or `filters` is exact too — provably-exact conditions (e.g. text equals/contains, not_empty/is_empty, checkbox is_true/is_false) stay a cheap SQL COUNT; everything else falls back to evaluating every matching row server-side, which is exact but not free on a large Base. Both `viewId` and `filters` require `baseId`.",
2668
2879
  successDescription: "Total active records matching the scope: the whole space, one Base, a saved View, an ad-hoc filter set, or a combination."
2669
2880
  }).input(countRecordsInputSchema).output(countRecordsResponseSchema),
2881
+ groupBy: oc.route({
2882
+ method: "GET",
2883
+ path: "/records/group-by",
2884
+ tags: ["Records"],
2885
+ summary: "Count records per group",
2886
+ description: "One SQL GROUP BY returning every bucket's exact count — the split a board column header or a summary tile needs, without reading the records themselves. `fieldSlug` must name a `select` or `checkbox` field: their stored value IS the grouping key, so the buckets are exactly the ones a client would build. Grouping by a text, number or date field is rejected rather than approximated (text keys are truncated at the projection limit; date keys would bucket by the server's timezone, not the viewer's). `viewId` and `filters` narrow the set first, with the same exactness rules as `records.count`: provably-exact conditions stay a cheap SQL aggregate, anything else falls back to evaluating every matching row server-side — exact, but not free on a large Base. Groups come back keyed by raw choice id (or `\"true\"`/`\"false\"`), with `null` for records that have no value; labels are the client's to render.",
2887
+ successDescription: "Every group's exact count, plus the total across all groups. Groups with zero records are omitted — a Base's full choice list lives in its field definition, so the client already knows which buckets to render empty."
2888
+ }).errors({
2889
+ BAD_REQUEST: {
2890
+ status: 400,
2891
+ message: "Field is not groupable"
2892
+ },
2893
+ NOT_FOUND: {
2894
+ status: 404,
2895
+ message: "Base or field not found"
2896
+ }
2897
+ }).input(groupRecordsInputSchema).output(groupRecordsResponseSchema),
2670
2898
  get: oc.route({
2671
2899
  method: "GET",
2672
2900
  path: "/records/get",
@@ -2840,19 +3068,55 @@ const ExportAssetTextVOSchema = z.object({
2840
3068
  textContentHash: z.string().nullable(),
2841
3069
  byteCount: z.number().int().nonnegative()
2842
3070
  });
2843
- const ImportBeginInputSchema = z.object({
3071
+ const ExportDocBodiesInputSchema = z.object({ nodeIds: z.array(z.string()).min(1).max(25) });
3072
+ const ExportDocBodiesVOSchema = z.object({
2844
3073
  /**
2845
- * The space id the archive was ORIGINALLY exported from (`manifest.spaceId`
2846
- * in the `.bbdump`, already integrity-verified before this is called).
2847
- * `importTableRows`'s "nodes" handling needs this to recognize the
2848
- * archive's own root-node row deterministically (`rootNodeIdForSpace`)
2849
- * scanning each batch's rows for "the one with a null `parentId`" only
2850
- * works when that row happens to land in the SAME batch as its children,
2851
- * which cursor pagination (id-ordered, not tree-ordered) does not
2852
- * guarantee once a space has more nodes than one page. See the matching
2853
- * comment in `import-logic.ts`.
3074
+ * One entry per requested node that is a Doc in this space. A node id that
3075
+ * does not resolve is simply absent (not an error): the caller asked for a
3076
+ * batch, and one bad id must not cost it the other 24. A Doc that exists but
3077
+ * has no body object yet yields `markdown: ""`, matching what a read through
3078
+ * the Doc domain would return.
2854
3079
  */
2855
- sourceSpaceId: z.string() });
3080
+ bodies: z.array(z.object({
3081
+ nodeId: z.string(),
3082
+ markdown: z.string()
3083
+ })) });
3084
+ const ImportBeginInputSchema = z.object({
3085
+ /**
3086
+ * The space id the archive was ORIGINALLY exported from (`manifest.spaceId`
3087
+ * in the `.bbdump`, already integrity-verified before this is called).
3088
+ * `importTableRows`'s "nodes" handling needs this to recognize the
3089
+ * archive's own root-node row deterministically (`rootNodeIdForSpace`) —
3090
+ * scanning each batch's rows for "the one with a null `parentId`" only
3091
+ * works when that row happens to land in the SAME batch as its children,
3092
+ * which cursor pagination (id-ordered, not tree-ordered) does not
3093
+ * guarantee once a space has more nodes than one page. See the matching
3094
+ * comment in `import-logic.ts`.
3095
+ */
3096
+ sourceSpaceId: z.string(),
3097
+ /**
3098
+ * Continue a restore that was interrupted partway through, instead of
3099
+ * requiring an empty space.
3100
+ *
3101
+ * A restore that FAILS rolls itself back (`importAbort`), so the target is
3102
+ * left clean and a plain re-run works. What cannot roll itself back is a
3103
+ * restore whose process died — Ctrl-C, OOM, a dropped connection, the
3104
+ * machine rebooting. That leaves the space holding however many of the
3105
+ * archive's rows had landed, and every subsequent attempt is refused
3106
+ * ("requires an empty target space") with no way forward except wiping it.
3107
+ *
3108
+ * In this mode the empty-space guard is skipped and inserts become
3109
+ * `ON CONFLICT DO NOTHING`, so replaying the same archive re-lands only what
3110
+ * is missing. Blobs and doc bodies are content-addressed writes to object
3111
+ * storage and were already idempotent.
3112
+ *
3113
+ * DANGEROUS if pointed at the wrong space: rows that collide are silently
3114
+ * skipped rather than reported, so restoring archive A into a space holding
3115
+ * archive B's data would interleave the two instead of refusing. Only pass
3116
+ * it to continue the SAME archive into the SAME space.
3117
+ */
3118
+ resume: z.boolean().optional().default(false)
3119
+ });
2856
3120
  const ImportBeginVOSchema = z.object({ sessionId: z.string() });
2857
3121
  /**
2858
3122
  * `docBodies`, `attachmentBlobs` and `assetTextBlobs` are pseudo-tables: doc
@@ -2910,6 +3174,13 @@ const dumpContract = {
2910
3174
  summary: "Resolve the download URL for one asset's extracted-text object",
2911
3175
  successDescription: "A resolved download URL for the asset's DERIVED text blob (`asset-texts/blobs/sha256/…`) plus its `textStorageKey` and `textContentHash`, so a backup can archive the exact bytes and verify them. `downloadUrl` is null when the row owns no separate object (auto-registered text-kind rows point at their attachment's own key, already covered by the attachment blobs; `status: \"none\"` rows have no text at all)."
2912
3176
  }).input(ExportAssetTextInputSchema).output(ExportAssetTextVOSchema),
3177
+ exportDocBodies: oc.route({
3178
+ method: "POST",
3179
+ path: "/dump/export/doc-bodies",
3180
+ tags: ["Dump"],
3181
+ summary: "Read the raw markdown for a batch of Doc nodes",
3182
+ successDescription: "The raw body behind each requested Doc, read straight from object storage — archived Docs included, which the ordinary `nodes.get` deliberately refuses. Ids that do not resolve to a Doc in this space are omitted rather than failing the batch."
3183
+ }).input(ExportDocBodiesInputSchema).output(ExportDocBodiesVOSchema),
2913
3184
  importBegin: oc.route({
2914
3185
  method: "POST",
2915
3186
  path: "/dump/import/begin",
@@ -4462,6 +4733,27 @@ const busabaseContractRoutes = {
4462
4733
  summary: "Update node metadata",
4463
4734
  successDescription: "Shallow-merged the supplied top-level keys into the active node's existing metadata. Requires write access on the node. Node CONTENT (a Doc body, or a whiteboard/workflow/html document) does not go through here — use PUT /nodes/{nodeId}/content instead."
4464
4735
  }).input(updateNodeMetadataInputSchema).output(nodeSchema),
4736
+ updateSettings: oc.route({
4737
+ method: "PATCH",
4738
+ path: "/nodes/{nodeId}/settings",
4739
+ tags: ["Nodes"],
4740
+ summary: "Update node system settings",
4741
+ successDescription: "Replaced the node's system settings. Unlike metadata this is a closed set of keys Busabase itself acts on, so an unknown key is rejected rather than stored. Send a key as null to clear it — for an AirApp's engine that returns the node to following its airapp.json. Requires write access on the node."
4742
+ }).input(updateNodeSettingsInputSchema).output(nodeSchema),
4743
+ getAgentPrompts: oc.route({
4744
+ method: "GET",
4745
+ path: "/nodes/{nodeId}/agent-prompts",
4746
+ tags: ["Nodes"],
4747
+ summary: "Get node custom agent prompts",
4748
+ successDescription: "This node's custom scenario prompts, which replace the node type's default prompts in the Ask-agent dialog. `null` means the node has never had any set, which is not the same as an empty list. Read separately from the node itself because the list is large enough (50 prompts x 8 KiB per locale) that carrying it on every node listing would be its own problem. Requires read access on the node."
4749
+ }).input(getNodeAgentPromptsInputSchema).output(nodeAgentPromptsSchema),
4750
+ updateAgentPrompts: oc.route({
4751
+ method: "PUT",
4752
+ path: "/nodes/{nodeId}/agent-prompts",
4753
+ tags: ["Nodes"],
4754
+ summary: "Replace node custom agent prompts",
4755
+ successDescription: "Replaced this node's custom scenario prompts — the whole list, not a merge. Send `null` to clear them and return the node to its type's default prompts. Requires write access on the node."
4756
+ }).input(updateNodeAgentPromptsInputSchema).output(nodeAgentPromptsSchema),
4465
4757
  updateContent: oc.route({
4466
4758
  method: "PUT",
4467
4759
  path: "/nodes/{nodeId}/content",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busabase-sdk",
3
- "version": "0.30.0",
3
+ "version": "0.40.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",
@@ -67,9 +67,9 @@
67
67
  "tsx": "^4.20.5",
68
68
  "typescript": "^7.0.2",
69
69
  "vitest": "^4.1.11",
70
- "busabase-contract": "0.30.0",
71
- "open-domains": "0.0.2",
72
- "openlib": "0.1.1"
70
+ "busabase-contract": "0.40.0",
71
+ "openlib": "0.1.1",
72
+ "open-domains": "0.0.2"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=24.18.0"