busabase-sdk 0.10.2 → 0.11.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.js CHANGED
@@ -1,9 +1,34 @@
1
+ import { normalizeBaseUrl } from './chunk-5NYQX65A.js';
2
+ export { normalizeBaseUrl } from './chunk-5NYQX65A.js';
1
3
  import { createORPCClient, ORPCError } from '@orpc/client';
2
4
  import { OpenAPILink } from '@orpc/openapi-client/fetch';
3
5
  import { oc, eventIterator } from '@orpc/contract';
4
6
  import { z } from 'zod';
5
7
 
6
- // src/client.ts
8
+ // src/asset-grep.ts
9
+ var toUnifiedFilesGrepInput = (input) => ({
10
+ pattern: input.pattern,
11
+ flags: input.flags,
12
+ sources: ["files"],
13
+ scope: input.scope ? { files: input.scope } : void 0,
14
+ maxMatches: input.maxMatches,
15
+ contextLines: input.contextLines
16
+ });
17
+ var toFilesOnlyGrepResult = (result) => ({
18
+ matches: result.matches.flatMap((match) => {
19
+ if (match.source !== "files") return [];
20
+ const { source: _source, ...fileMatch } = match;
21
+ return [fileMatch];
22
+ }),
23
+ filesScanned: result.coverage.files.scanned,
24
+ missing: result.coverage.files.missing,
25
+ stale: result.coverage.files.stale,
26
+ unsearchable: result.coverage.files.unsearchable,
27
+ errored: result.coverage.files.errored,
28
+ notReached: result.coverage.files.notReached,
29
+ truncated: result.truncated
30
+ });
31
+ var grepAssets = async (client, input) => toFilesOnlyGrepResult(await client.grep(toUnifiedFilesGrepInput(input)));
7
32
  var i18n = {
8
33
  locales: ["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr", "es", "pt"]};
9
34
  var LocaleSchema = z.enum(i18n.locales);
@@ -536,7 +561,33 @@ var listNodesInputSchema = z.object({
536
561
  * soft-archived nodes for the Trash view — no `parentId`/`depth` walk, since
537
562
  * archived nodes are shown as a list, not a tree.
538
563
  */
539
- status: z.enum(["active", "archived"]).optional().default("active")
564
+ status: z.enum(["active", "archived"]).optional().default("active"),
565
+ /**
566
+ * Narrow to specific node types and return a FLAT list of lightweight node
567
+ * summaries (`children: []`) instead of walking the tree. This is what
568
+ * replaced the four retired narrow listings (`GET /docs`, `/files`,
569
+ * `/folders`, `/file-trees`); file-trees are selected with their real
570
+ * discriminators `skill` / `drive` / `airapp`, since there is no synthetic
571
+ * "file-tree" node type.
572
+ *
573
+ * Omitting `types` leaves every existing caller on exactly today's
574
+ * behaviour (full tree, or a `parentId`/`depth`-bounded walk, or the
575
+ * archived flat list) — the two modes never interfere.
576
+ *
577
+ * NOTE — no `projection` parameter, deliberately. The consolidation roadmap
578
+ * sketched `?projection=summary`, but it also rules out adding
579
+ * `projection=detail` in this batch (the retired detail lists were the
580
+ * N+1 payloads this change exists to remove). That would leave a parameter
581
+ * with exactly one legal value, which is noise in OpenAPI/MCP/CLI rather
582
+ * than a decision a caller gets to make. Detail is `GET /nodes/{nodeId}`.
583
+ *
584
+ * A GET query param that occurs exactly once (`?types=doc`) arrives as a
585
+ * bare string, not a 1-element array — only a REPEATED occurrence
586
+ * (`?types=doc&types=file`) becomes an array. Accept both and normalize.
587
+ */
588
+ types: z.union([z.array(z.enum(NODE_TYPES)), z.enum(NODE_TYPES)]).transform((value) => Array.isArray(value) ? value : [value]).optional().describe(
589
+ "Return a flat list of lightweight summaries for these node types instead of the tree. Read one node's full detail with GET /nodes/{nodeId}."
590
+ )
540
591
  }).optional();
541
592
  var isDescendantInputSchema = z.object({
542
593
  nodeId: z.string(),
@@ -704,11 +755,19 @@ var liveEventSchema = z.object({
704
755
  // via the audit funnel, but nothing needs reviewing). Consumed by
705
756
  // `use-live-sync.ts` to pop a desktop Notification, and by
706
757
  // busabase-cloud's host hook to persist an inbox notification row.
707
- "change_request.pending_review"
758
+ "change_request.pending_review",
759
+ // A node's metadata was written directly, outside the change-request flow
760
+ // (`PATCH /api/v1/nodes/{nodeId}/metadata` — agents, the SDK, an MCP tool,
761
+ // and every rich-node editor's own Save). Carries the touched node in
762
+ // `nodeIds` so open dashboards refetch the node tree instead of showing a
763
+ // stale whiteboard/workflow/HTML document until the next reload.
764
+ "node.metadata_updated"
708
765
  ]),
709
766
  spaceId: z.string(),
710
767
  actorId: z.string(),
711
- changeRequestId: z.string(),
768
+ // Null for events that aren't about a change request at all
769
+ // (`node.metadata_updated`), which is every direct, auto-audited write.
770
+ changeRequestId: z.string().nullable(),
712
771
  baseId: z.string().nullable(),
713
772
  nodeIds: z.array(z.string()),
714
773
  recordIds: z.array(z.string()),
@@ -1057,13 +1116,6 @@ var fileTreeRefSchema = z.object({
1057
1116
  type: fileTreeNodeTypeSchema.optional()
1058
1117
  });
1059
1118
  var fileTreeContract = {
1060
- list: oc.route({
1061
- method: "GET",
1062
- path: "/file-trees",
1063
- tags: ["File Trees"],
1064
- summary: "List file-tree nodes",
1065
- successDescription: "Skill, Drive, and AirApp nodes with their Asset-backed file trees. Pass `type` to narrow to one kind."
1066
- }).input(z.object({ type: fileTreeNodeTypeSchema.optional() })).output(z.array(fileTreeNodeSchema)),
1067
1119
  create: oc.route({
1068
1120
  method: "POST",
1069
1121
  path: "/file-trees",
@@ -1076,13 +1128,6 @@ var fileTreeContract = {
1076
1128
  changeRequestSchema.extend({ materialized: z.literal(false) })
1077
1129
  ])
1078
1130
  ),
1079
- get: oc.route({
1080
- method: "GET",
1081
- path: "/file-trees/{nodeId}",
1082
- tags: ["File Trees"],
1083
- summary: "Get file-tree node",
1084
- successDescription: "File-tree node detail and its file list."
1085
- }).input(fileTreeRefSchema).output(fileTreeNodeSchema),
1086
1131
  listFiles: oc.route({
1087
1132
  method: "GET",
1088
1133
  path: "/file-trees/{nodeId}/files",
@@ -1277,7 +1322,7 @@ var GREP_DEFAULT_MAX_MATCHES = 100;
1277
1322
  var GREP_HARD_MAX_MATCHES = 1e3;
1278
1323
  var GREP_DEFAULT_CONTEXT_LINES = 0;
1279
1324
  var GREP_MAX_CONTEXT_LINES = 10;
1280
- var GrepInputSchema = z.object({
1325
+ z.object({
1281
1326
  pattern: z.string().min(1),
1282
1327
  /** JS RegExp flags, e.g. `"i"` for case-insensitive. `g`/`y` are ignored (grep always scans every match per line). */
1283
1328
  flags: z.string().optional().default(""),
@@ -1298,7 +1343,7 @@ var GrepMatchVOSchema = z.object({
1298
1343
  before: z.array(z.string()),
1299
1344
  after: z.array(z.string())
1300
1345
  });
1301
- var GrepResultVOSchema = z.object({
1346
+ z.object({
1302
1347
  matches: z.array(GrepMatchVOSchema),
1303
1348
  filesScanned: z.number().int().nonnegative(),
1304
1349
  /** Asset ids in scope with no text yet (contentKind text-or-writable-binary, no row). */
@@ -1432,13 +1477,6 @@ var assetsContract = {
1432
1477
  summary: "Request a presigned upload URL for large text",
1433
1478
  successDescription: "Presigned (or dev) upload URL for a temporary text object; PUT the bytes there, then call putText with the returned storageKey to bind, verify, and content-address it."
1434
1479
  }).input(CreateTextUploadUrlInputSchema).output(CreateTextUploadUrlVOSchema),
1435
- grep: oc.route({
1436
- method: "POST",
1437
- path: "/assets/grep",
1438
- tags: ["Assets"],
1439
- summary: "Search every text-bearing asset in scope",
1440
- successDescription: "Streaming regex/literal matches with real file + line + column numbers and context, across every asset with text \u2014 any size, no 256KB cap. Honest coverage: missing/stale/unsearchable/errored name assets that were not (fully or successfully) searched, notReached counts present assets the scan never got to, and truncated flags a capped response."
1441
- }).input(GrepInputSchema).output(GrepResultVOSchema),
1442
1480
  readTextLines: oc.route({
1443
1481
  method: "GET",
1444
1482
  path: "/assets/{assetId}/text/lines",
@@ -1525,6 +1563,9 @@ var viewSchema = z.object({
1525
1563
  createdAt: z.string(),
1526
1564
  updatedAt: z.string()
1527
1565
  });
1566
+ var autoMergeSchema = z.boolean().optional().describe(
1567
+ "Whether to approve and merge this view change immediately. Omitted defaults to merging immediately if the actor has write access on the Base's node, otherwise falling back to a pending Change Request; pass explicit false to force review even with write access."
1568
+ );
1528
1569
  var createViewInputSchema = z.object({
1529
1570
  config: viewConfigSchema.optional().default({ filters: [], sorts: [] }),
1530
1571
  description: z.string().optional().default(""),
@@ -1532,7 +1573,8 @@ var createViewInputSchema = z.object({
1532
1573
  name: z.string().min(1),
1533
1574
  type: viewTypeSchema.optional().default("table"),
1534
1575
  slug: z.string().min(1).regex(/^[a-z0-9-]+$/).optional(),
1535
- submittedBy: z.string().optional().default("local-producer")
1576
+ submittedBy: z.string().optional().default("local-producer"),
1577
+ autoMerge: autoMergeSchema
1536
1578
  });
1537
1579
  var updateViewInputSchema = z.object({
1538
1580
  config: viewConfigSchema.optional(),
@@ -1540,15 +1582,18 @@ var updateViewInputSchema = z.object({
1540
1582
  message: z.string().optional().default("Update view"),
1541
1583
  name: z.string().min(1).optional(),
1542
1584
  type: viewTypeSchema.optional(),
1543
- submittedBy: z.string().optional().default("local-producer")
1585
+ submittedBy: z.string().optional().default("local-producer"),
1586
+ autoMerge: autoMergeSchema
1544
1587
  });
1545
1588
  var deleteViewInputSchema = z.object({
1546
1589
  message: z.string().optional().default("Delete view"),
1547
- submittedBy: z.string().optional().default("local-producer")
1590
+ submittedBy: z.string().optional().default("local-producer"),
1591
+ autoMerge: autoMergeSchema
1548
1592
  });
1549
1593
  var restoreViewInputSchema = z.object({
1550
1594
  message: z.string().optional().default("Restore view"),
1551
- submittedBy: z.string().optional().default("local-producer")
1595
+ submittedBy: z.string().optional().default("local-producer"),
1596
+ autoMerge: autoMergeSchema
1552
1597
  });
1553
1598
  var viewChangeRequestInputSchema = z.discriminatedUnion("operation", [
1554
1599
  createViewInputSchema.extend({
@@ -1616,6 +1661,19 @@ var listRecordsResponseSchema = z.object({
1616
1661
  records: z.array(recordSchema),
1617
1662
  nextCursor: z.string().nullable()
1618
1663
  });
1664
+ var listRecordsPageInputSchema = z.object({
1665
+ baseId: z.string().min(1),
1666
+ viewId: z.string().min(1).optional(),
1667
+ page: z.coerce.number().int().min(1).optional().default(1),
1668
+ pageSize: z.coerce.number().int().min(1).max(100).optional().default(50)
1669
+ });
1670
+ var listRecordsPageResponseSchema = z.object({
1671
+ records: z.array(recordSchema),
1672
+ total: z.number().int().nonnegative(),
1673
+ totalPages: z.number().int().nonnegative(),
1674
+ page: z.number().int().min(1),
1675
+ pageSize: z.number().int().min(1).max(100)
1676
+ });
1619
1677
  var countRecordsInputSchema = z.object({
1620
1678
  baseId: z.string().optional()
1621
1679
  }).optional().default({});
@@ -1662,10 +1720,16 @@ var recordFieldFilterInputSchema = z.object({
1662
1720
  limit: z.coerce.number().int().min(1).max(100).optional().default(50)
1663
1721
  });
1664
1722
  var recordFieldGetInputSchema = z.object({
1665
- baseId: z.string(),
1666
- fieldSlug: z.string().min(1),
1667
- valueText: z.string().min(1)
1723
+ baseId: z.string().describe("Field selector: Base id. Requires fieldSlug and valueText."),
1724
+ fieldSlug: z.string().min(1).describe("Field selector: exact field slug. Requires baseId and valueText."),
1725
+ valueText: z.string().min(1).describe("Field selector: exact text value. Requires baseId and fieldSlug.")
1668
1726
  });
1727
+ var recordGetInputSchema = z.union([
1728
+ z.object({
1729
+ recordId: z.string().min(1).describe("Record id selector. Use alone; do not combine with field selector fields.")
1730
+ }).strict(),
1731
+ recordFieldGetInputSchema.strict()
1732
+ ]);
1669
1733
  var restoreRecordInputSchema = z.object({
1670
1734
  message: z.string().optional(),
1671
1735
  submittedBy: z.string().optional().default("local-editor")
@@ -1801,6 +1865,13 @@ var recordContract = {
1801
1865
  summary: "List records",
1802
1866
  successDescription: "A page of canonical records plus an opaque nextCursor (null at the end). `status=archived` lists the Base's trash instead of its live rows."
1803
1867
  }).input(listRecordsInputSchema).output(listRecordsResponseSchema),
1868
+ listPage: oc.route({
1869
+ method: "GET",
1870
+ path: "/records/page",
1871
+ tags: ["Records"],
1872
+ summary: "List a numbered record page",
1873
+ 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."
1874
+ }).input(listRecordsPageInputSchema).output(listRecordsPageResponseSchema),
1804
1875
  count: oc.route({
1805
1876
  method: "GET",
1806
1877
  path: "/records/count",
@@ -1810,11 +1881,15 @@ var recordContract = {
1810
1881
  }).input(countRecordsInputSchema).output(countRecordsResponseSchema),
1811
1882
  get: oc.route({
1812
1883
  method: "GET",
1813
- path: "/records/{recordId}",
1884
+ path: "/records/get",
1814
1885
  tags: ["Records"],
1815
1886
  summary: "Get record",
1816
- successDescription: "Canonical record detail."
1817
- }).input(z.object({ recordId: z.string() })).output(recordSchema),
1887
+ description: "Provide exactly one selector: recordId alone, or the complete baseId + fieldSlug + valueText tuple. Other combinations return 400.",
1888
+ successDescription: "One canonical record selected by id or exact field value."
1889
+ }).errors({
1890
+ BAD_REQUEST: { status: 400, message: "Exactly one record selector is required" },
1891
+ NOT_FOUND: { status: 404, message: "Record not found" }
1892
+ }).input(recordGetInputSchema).output(recordSchema),
1818
1893
  search: oc.route({
1819
1894
  method: "GET",
1820
1895
  path: "/records/search",
@@ -1822,13 +1897,6 @@ var recordContract = {
1822
1897
  summary: "Filter records by field text",
1823
1898
  successDescription: "Canonical records matching a field text filter."
1824
1899
  }).input(recordFieldFilterInputSchema).output(z.array(recordSchema)),
1825
- getByField: oc.route({
1826
- method: "GET",
1827
- path: "/records/by-field",
1828
- tags: ["Records"],
1829
- summary: "Get record by field value",
1830
- successDescription: "Single canonical record whose field value exactly matches, or null when none does \u2014 a scoped point lookup (e.g. by a unique slug or path field), not a list."
1831
- }).input(recordFieldGetInputSchema).output(recordSchema.nullable()),
1832
1900
  changeRequest: oc.route({
1833
1901
  method: "POST",
1834
1902
  path: "/records/{recordId}/change-requests",
@@ -1862,8 +1930,13 @@ var viewContract = {
1862
1930
  path: "/views/change-requests",
1863
1931
  tags: ["Views", "Change Requests"],
1864
1932
  summary: "Create view change request",
1865
- successDescription: "Created change request proposing a view change. `operation` selects what to propose: `create` (addressed by `baseId`), or `update` / `delete` / `restore` (addressed by `viewId`)."
1866
- }).input(viewChangeRequestInputSchema).output(changeRequestSchema)
1933
+ successDescription: "Proposes a view change. `operation` selects what to propose: `create` (addressed by `baseId`), or `update` / `delete` / `restore` (addressed by `viewId`). Review-first when the actor lacks write access or passes `autoMerge: false` \u2014 a pending ChangeRequest (`materialized: false`). Otherwise the change is approved and merged in the same call and the materialized View comes back instead (`materialized: true`)."
1934
+ }).input(viewChangeRequestInputSchema).output(
1935
+ z.union([
1936
+ viewSchema.extend({ materialized: z.literal(true) }),
1937
+ changeRequestSchema.extend({ materialized: z.literal(false) })
1938
+ ])
1939
+ )
1867
1940
  };
1868
1941
  var ReadDocLinesInputSchema = z.object({
1869
1942
  nodeId: z.string(),
@@ -1902,13 +1975,6 @@ var createDocChangeRequestInputSchema = z.object({
1902
1975
  submittedBy: z.string().optional().default("local-producer")
1903
1976
  });
1904
1977
  var docContract = {
1905
- list: oc.route({
1906
- method: "GET",
1907
- path: "/docs",
1908
- tags: ["Docs"],
1909
- summary: "List Doc nodes",
1910
- successDescription: "Doc nodes with their storage-backed bodies."
1911
- }).output(z.array(docSchema)),
1912
1978
  create: oc.route({
1913
1979
  method: "POST",
1914
1980
  path: "/docs",
@@ -1921,19 +1987,12 @@ var docContract = {
1921
1987
  changeRequestSchema.extend({ materialized: z.literal(false) })
1922
1988
  ])
1923
1989
  ),
1924
- get: oc.route({
1925
- method: "GET",
1926
- path: "/docs/{nodeId}",
1927
- tags: ["Docs"],
1928
- summary: "Get Doc node",
1929
- successDescription: "Doc node detail and body."
1930
- }).input(z.object({ nodeId: z.string() })).output(docSchema),
1931
1990
  readLines: oc.route({
1932
1991
  method: "GET",
1933
1992
  path: "/docs/{nodeId}/lines",
1934
1993
  tags: ["Docs"],
1935
1994
  summary: "Read an exact line range from a Doc body",
1936
- successDescription: "Lines [startLine, endLine] (range capped at 2000 lines / ~2MB response) sliced from the Doc's full body \u2014 Docs are KB-scale, so the whole body is read in memory; no byte-range/checkpoint machinery like assets.readTextLines uses for potentially multi-GB files. The Doc-domain follow-up to a Unified Grep match with `source: \"docs\"`, so an agent can read just the lines around a match instead of `get`'s entire body."
1995
+ successDescription: "Lines [startLine, endLine] (range capped at 2000 lines / ~2MB response) sliced from the Doc's full body \u2014 Docs are KB-scale, so the whole body is read in memory; no byte-range/checkpoint machinery like assets.readTextLines uses for potentially multi-GB files. The Doc-domain follow-up to a Unified Grep match with `source: \"docs\"`, so an agent can read just the lines around a match instead of `nodes.get`'s entire body."
1937
1996
  }).input(ReadDocLinesInputSchema).output(ReadLinesVOSchema),
1938
1997
  updateBody: oc.route({
1939
1998
  method: "PUT",
@@ -2099,13 +2158,6 @@ var createFileNodeInputSchema = z.object({
2099
2158
  autoMerge: z.boolean().optional()
2100
2159
  });
2101
2160
  var fileContract = {
2102
- list: oc.route({
2103
- method: "GET",
2104
- path: "/files",
2105
- tags: ["Files"],
2106
- summary: "List File nodes",
2107
- successDescription: "Workspace File nodes with their backing Asset metadata."
2108
- }).output(z.array(FileNodeVOSchema)),
2109
2161
  create: oc.route({
2110
2162
  method: "POST",
2111
2163
  path: "/files",
@@ -2117,34 +2169,7 @@ var fileContract = {
2117
2169
  FileNodeVOSchema.extend({ materialized: z.literal(true) }),
2118
2170
  changeRequestSchema.extend({ materialized: z.literal(false) })
2119
2171
  ])
2120
- ),
2121
- get: oc.route({
2122
- method: "GET",
2123
- path: "/files/{nodeId}",
2124
- tags: ["Files"],
2125
- summary: "Get File node",
2126
- successDescription: "File node detail and backing Asset metadata."
2127
- }).input(z.object({ nodeId: z.string() })).output(FileNodeVOSchema)
2128
- };
2129
- var folderSchema = z.object({
2130
- node: nodeSchema,
2131
- children: z.array(nodeSchema)
2132
- });
2133
- var folderContract = {
2134
- list: oc.route({
2135
- method: "GET",
2136
- path: "/folders",
2137
- tags: ["Folders"],
2138
- summary: "List Folder nodes",
2139
- successDescription: "Folder nodes with their direct children."
2140
- }).output(z.array(folderSchema)),
2141
- get: oc.route({
2142
- method: "GET",
2143
- path: "/folders/{nodeId}",
2144
- tags: ["Folders"],
2145
- summary: "Get Folder node",
2146
- successDescription: "Folder node and its direct children."
2147
- }).input(z.object({ nodeId: z.string() })).output(folderSchema)
2172
+ )
2148
2173
  };
2149
2174
  var FormFieldBindingSchema = z.object({
2150
2175
  inputName: z.string().min(1),
@@ -2697,7 +2722,7 @@ var UnifiedGrepScopeSchema = z.object({
2697
2722
  });
2698
2723
  var UnifiedGrepInputSchema = z.object({
2699
2724
  pattern: z.string().min(1),
2700
- /** JS RegExp flags, e.g. `"i"` for case-insensitive — same language as `assets.grep`. */
2725
+ /** JS RegExp flags, e.g. `"i"` for case-insensitive. */
2701
2726
  flags: z.string().optional().default(""),
2702
2727
  /** Which sources to scan. Omitted = all three (`files`, `docs`, `records`). */
2703
2728
  sources: z.array(GrepSourceSchema).optional(),
@@ -2777,16 +2802,86 @@ var UnifiedGrepResultVOSchema = z.object({
2777
2802
  /** True when any source truncated, or any source has `notReached > 0`. */
2778
2803
  truncated: z.boolean()
2779
2804
  });
2805
+ var folderSchema = z.object({
2806
+ node: nodeSchema,
2807
+ children: z.array(nodeSchema)
2808
+ });
2809
+
2810
+ // ../../packages/busabase-contract/src/contract/node-detail-schemas.ts
2811
+ var genericNodeDetailSchema = (type) => z.object({
2812
+ type: z.literal(type),
2813
+ node: nodeSchema
2814
+ });
2815
+ var NODE_DETAIL_VARIANTS = {
2816
+ folder: folderSchema.extend({ type: z.literal("folder") }),
2817
+ doc: docSchema.extend({ type: z.literal("doc") }),
2818
+ file: FileNodeVOSchema.extend({ type: z.literal("file") }),
2819
+ // Skills, Drives, and AirApps are one server-side shape (`fileTreeNodeSchema`)
2820
+ // but three real node types — there is no synthetic "file-tree" node type, so
2821
+ // each gets its own discriminated variant rather than a shared alias.
2822
+ skill: fileTreeNodeSchema.extend({ type: z.literal("skill") }),
2823
+ drive: fileTreeNodeSchema.extend({ type: z.literal("drive") }),
2824
+ airapp: fileTreeNodeSchema.extend({ type: z.literal("airapp") }),
2825
+ base: genericNodeDetailSchema("base"),
2826
+ form: genericNodeDetailSchema("form"),
2827
+ whiteboard: genericNodeDetailSchema("whiteboard"),
2828
+ workflow: genericNodeDetailSchema("workflow"),
2829
+ html: genericNodeDetailSchema("html")
2830
+ };
2831
+ var NodeDetailVOSchema = z.discriminatedUnion("type", [
2832
+ NODE_DETAIL_VARIANTS.folder,
2833
+ NODE_DETAIL_VARIANTS.doc,
2834
+ NODE_DETAIL_VARIANTS.file,
2835
+ NODE_DETAIL_VARIANTS.skill,
2836
+ NODE_DETAIL_VARIANTS.drive,
2837
+ NODE_DETAIL_VARIANTS.airapp,
2838
+ NODE_DETAIL_VARIANTS.base,
2839
+ NODE_DETAIL_VARIANTS.form,
2840
+ NODE_DETAIL_VARIANTS.whiteboard,
2841
+ NODE_DETAIL_VARIANTS.workflow,
2842
+ NODE_DETAIL_VARIANTS.html
2843
+ ]);
2844
+ var getNodeInputSchema = z.object({
2845
+ nodeId: z.string().describe("Node id, or a slug that is unique within its type."),
2846
+ type: z.enum(NODE_TYPES).optional().describe(
2847
+ "Optional disambiguation hint, only needed when `nodeId` is a slug that exists under more than one node type."
2848
+ )
2849
+ });
2780
2850
 
2781
2851
  // ../../packages/busabase-contract/src/contract/busabase.ts
2782
- var changeRequestBatchResultSchema = z.object({
2852
+ var changeRequestBatchFailureSchema = z.object({
2853
+ changeRequestId: z.string(),
2854
+ ok: z.literal(false),
2855
+ error: z.string(),
2856
+ code: z.string().optional(),
2857
+ data: z.unknown().optional()
2858
+ });
2859
+ var changeRequestReviewBatchResultSchema = z.object({
2783
2860
  results: z.array(
2784
- z.object({
2785
- changeRequestId: z.string(),
2786
- ok: z.boolean(),
2787
- status: z.string().optional(),
2788
- error: z.string().optional()
2789
- })
2861
+ z.discriminatedUnion("ok", [
2862
+ z.object({
2863
+ changeRequestId: z.string(),
2864
+ ok: z.literal(true),
2865
+ status: z.string(),
2866
+ changeRequest: changeRequestSchema
2867
+ }),
2868
+ changeRequestBatchFailureSchema
2869
+ ])
2870
+ )
2871
+ });
2872
+ var changeRequestMergeBatchResultSchema = z.object({
2873
+ results: z.array(
2874
+ z.discriminatedUnion("ok", [
2875
+ z.object({
2876
+ changeRequestId: z.string(),
2877
+ ok: z.literal(true),
2878
+ status: z.string(),
2879
+ changeRequest: changeRequestSchema,
2880
+ record: recordSchema.nullable(),
2881
+ view: viewSchema.nullable()
2882
+ }),
2883
+ changeRequestBatchFailureSchema
2884
+ ])
2790
2885
  )
2791
2886
  });
2792
2887
  var busabaseContractRoutes = {
@@ -2806,11 +2901,9 @@ var busabaseContractRoutes = {
2806
2901
  summary: "Search Busabase",
2807
2902
  successDescription: "Paginated search results across records, change requests, Bases, File nodes, and Assets."
2808
2903
  }).input(searchInputSchema).output(searchResponseSchema),
2809
- // Unified Grep (P2a files+docs, P2b records) — top-level, cross-source
2810
- // superset of `assets.grep`. See apps/busabase/content/spec/unified-grep.md.
2811
- // Composes `logic/grep.ts`; `assets.grep` (files-only specialist) is
2812
- // unchanged and stays the dedicated endpoint for its fuller
2813
- // missing/stale/unsearchable reporting.
2904
+ // Unified Grep (P2a files+docs, P2b records) — the single public pattern
2905
+ // search endpoint. Files-only callers use `sources: ["files"]` and retain
2906
+ // the full missing/stale/unsearchable coverage block.
2814
2907
  grep: oc.route({
2815
2908
  method: "POST",
2816
2909
  path: "/grep",
@@ -2823,8 +2916,8 @@ var busabaseContractRoutes = {
2823
2916
  method: "GET",
2824
2917
  path: "/nodes",
2825
2918
  tags: ["Nodes"],
2826
- summary: "List node tree",
2827
- successDescription: "Workspace node tree including folders, Bases, files, and agents. With no `parentId`/`depth`, returns the FULL tree (legacy behavior, still what every non-sidebar caller gets). Passing `parentId` and/or `depth` switches to a depth-bounded fetch: `parentId` omitted/null starts from the space root and returns it wrapped exactly like the legacy call (just depth-limited); an explicit `parentId` returns that node's children directly, ready to merge into its `NodeVO.children` for a sidebar's lazy per-folder expand. See `NodeVO.hasChildren` for how a depth boundary is surfaced."
2919
+ summary: "List nodes (workspace tree, or a flat summary list by type)",
2920
+ successDescription: "Workspace node tree including folders, Bases, files, and agents. With no `parentId`/`depth`, returns the FULL tree (legacy behavior, still what every non-sidebar caller gets). Passing `parentId` and/or `depth` switches to a depth-bounded fetch: `parentId` omitted/null starts from the space root and returns it wrapped exactly like the legacy call (just depth-limited); an explicit `parentId` returns that node's children directly, ready to merge into its `NodeVO.children` for a sidebar's lazy per-folder expand. See `NodeVO.hasChildren` for how a depth boundary is surfaced. Passing `types` instead returns a FLAT, ACL-filtered list of lightweight summaries (`children: []`) for just those node types \u2014 this is what replaced `GET /docs`, `/files`, `/folders`, and `/file-trees`, and it deliberately hydrates nothing heavy (no Doc bodies, backing Assets, folder children, or file inventories). Open one item with `GET /nodes/{nodeId}`."
2828
2921
  }).input(listNodesInputSchema).output(z.array(nodeSchema)),
2829
2922
  searchByName: oc.route({
2830
2923
  method: "GET",
@@ -2894,6 +2987,21 @@ var busabaseContractRoutes = {
2894
2987
  summary: "List the current actor's favorited nodes",
2895
2988
  successDescription: "The acting user's favorited nodes, newest-favorited first, filtered through the same archived/deleted/visibility rules as the main tree \u2014 a favorited node that's later archived, purged, or (cloud) hidden from this actor silently drops out rather than erroring."
2896
2989
  }).output(z.array(nodeSchema)),
2990
+ // Registered LAST among the `/nodes/...` GETs on purpose. `GET /nodes/search`
2991
+ // and `GET /nodes/favorites` are literal paths that now share a prefix with
2992
+ // this template. The oRPC OpenAPI matcher is a rou3 radix trie, which
2993
+ // prefers a static segment over a param segment independently of insertion
2994
+ // order — but keeping the literals declared first means the source order
2995
+ // matches the resolution order, so nobody has to know that to read this
2996
+ // file. `tests/openapi-node-routes.test.ts` proves the literals still win
2997
+ // against a real handler rather than resolving as `nodeId: "search"`.
2998
+ get: oc.route({
2999
+ method: "GET",
3000
+ path: "/nodes/{nodeId}",
3001
+ tags: ["Nodes"],
3002
+ summary: "Get one node's typed detail",
3003
+ successDescription: "The node's full detail, discriminated by its `type`. One entry point for every node type, so a caller holding an id never has to discover the type first: `folder` carries its direct `children`, `doc` its storage-backed `body`, `file` its backing `asset`, and `skill`/`drive`/`airapp` their Asset-backed `files`. Types with no richer detail yet (`base`, `form`, `whiteboard`, `workflow`, `html`) return just `node`. `nodeId` accepts an id or a slug; pass `type` when a slug exists under more than one type. Archived nodes are not returned (404), matching the typed gets this replaced."
3004
+ }).input(getNodeInputSchema).output(NodeDetailVOSchema),
2897
3005
  principals: {
2898
3006
  list: oc.route({
2899
3007
  method: "GET",
@@ -3027,7 +3135,9 @@ var busabaseContractRoutes = {
3027
3135
  airapps: airappRuntimeContract,
3028
3136
  files: fileContract,
3029
3137
  docs: docContract,
3030
- folders: folderContract,
3138
+ // No `folders` key: the Folder domain's only two operations were `GET /folders`
3139
+ // and `GET /folders/{nodeId}`, both now served by the unified Node surface
3140
+ // (`nodes.list({ types: ["folder"] })` / `nodes.get`).
3031
3141
  forms: formContract,
3032
3142
  assets: assetsContract,
3033
3143
  vault: vaultContract,
@@ -3059,23 +3169,16 @@ var busabaseContractRoutes = {
3059
3169
  successDescription: "Change Request detail."
3060
3170
  }).input(z.object({ changeRequestId: z.string() })).output(changeRequestSchema),
3061
3171
  review: oc.route({
3062
- method: "POST",
3063
- path: "/change-requests/{changeRequestId}/reviews",
3064
- tags: ["Change Requests"],
3065
- summary: "Review change request",
3066
- successDescription: "Reviewed change request."
3067
- }).input(reviewChangeRequestInputSchema.extend({ changeRequestId: z.string() })).output(changeRequestSchema),
3068
- reviewMany: oc.route({
3069
3172
  method: "POST",
3070
3173
  path: "/change-requests/reviews",
3071
3174
  tags: ["Change Requests"],
3072
- summary: "Review many change requests",
3175
+ summary: "Review change requests",
3073
3176
  successDescription: "Per-change-request review results (failures isolated \u2014 one bad id does not abort the rest)."
3074
3177
  }).input(
3075
3178
  reviewChangeRequestInputSchema.extend({
3076
3179
  changeRequestIds: z.array(z.string()).min(1).max(100)
3077
3180
  })
3078
- ).output(changeRequestBatchResultSchema),
3181
+ ).output(changeRequestReviewBatchResultSchema),
3079
3182
  close: oc.route({
3080
3183
  method: "POST",
3081
3184
  path: "/change-requests/{changeRequestId}/close",
@@ -3084,25 +3187,12 @@ var busabaseContractRoutes = {
3084
3187
  successDescription: "Closed change request (terminal \u2014 distinct from request changes)."
3085
3188
  }).input(z.object({ changeRequestId: z.string(), reason: z.string().optional() })).output(changeRequestSchema),
3086
3189
  merge: oc.route({
3087
- method: "POST",
3088
- path: "/change-requests/{changeRequestId}/merge",
3089
- tags: ["Change Requests"],
3090
- summary: "Merge change request into Base",
3091
- successDescription: "Merged change request and canonical record."
3092
- }).input(z.object({ changeRequestId: z.string() })).output(
3093
- z.object({
3094
- changeRequest: changeRequestSchema,
3095
- record: recordSchema.nullable(),
3096
- view: viewSchema.nullable()
3097
- })
3098
- ),
3099
- mergeMany: oc.route({
3100
3190
  method: "POST",
3101
3191
  path: "/change-requests/merge",
3102
3192
  tags: ["Change Requests"],
3103
- summary: "Merge many change requests",
3193
+ summary: "Merge change requests",
3104
3194
  successDescription: "Per-change-request merge results (each merged in its own transaction; failures isolated)."
3105
- }).input(z.object({ changeRequestIds: z.array(z.string()).min(1).max(100) })).output(changeRequestBatchResultSchema)
3195
+ }).input(z.object({ changeRequestIds: z.array(z.string()).min(1).max(100) })).output(changeRequestMergeBatchResultSchema)
3106
3196
  },
3107
3197
  operations: {
3108
3198
  revise: oc.route({
@@ -3384,9 +3474,6 @@ var env = (key) => {
3384
3474
  const value = process.env[key];
3385
3475
  return value && value.length > 0 ? value : void 0;
3386
3476
  };
3387
- function normalizeBaseUrl(raw) {
3388
- return raw.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
3389
- }
3390
3477
  function resolveConfig(config = {}) {
3391
3478
  return {
3392
3479
  baseUrl: normalizeBaseUrl(config.baseUrl ?? env("BUSABASE_BASE_URL") ?? DEFAULT_BASE_URL),
@@ -3434,7 +3521,22 @@ function createBusabaseClient(config = {}) {
3434
3521
  return createORPCClient(link);
3435
3522
  }
3436
3523
 
3524
+ // src/record-get.ts
3525
+ var isNotFound = (error) => typeof error === "object" && error !== null && ("status" in error && error.status === 404 || "code" in error && error.code === "NOT_FOUND");
3526
+ var getRecordByField = async (client, input) => {
3527
+ try {
3528
+ return await client.records.get(input);
3529
+ } catch (error) {
3530
+ if (isNotFound(error)) return null;
3531
+ throw error;
3532
+ }
3533
+ };
3534
+
3437
3535
  // src/index.ts
3536
+ var batchItemError = (result) => Object.assign(new Error(result?.error ?? "Change request action returned no result"), {
3537
+ ...result?.code ? { code: result.code } : {},
3538
+ ...result?.data === void 0 ? {} : { data: result.data }
3539
+ });
3438
3540
  var Busabase = class {
3439
3541
  /** The underlying fully-typed oRPC client. Use it for anything not surfaced here. */
3440
3542
  client;
@@ -3450,17 +3552,67 @@ var Busabase = class {
3450
3552
  return this.client.bases;
3451
3553
  }
3452
3554
  get records() {
3453
- return this.client.records;
3555
+ const getByField = (input) => getRecordByField(this.client, input);
3556
+ return new Proxy(this.client.records, {
3557
+ get(target, property, receiver) {
3558
+ if (property === "getByField") return getByField;
3559
+ return Reflect.get(target, property, receiver);
3560
+ }
3561
+ });
3454
3562
  }
3455
3563
  get views() {
3456
3564
  return this.client.views;
3457
3565
  }
3458
3566
  get changeRequests() {
3459
- return this.client.changeRequests;
3567
+ const review = async (input) => {
3568
+ if ("changeRequestIds" in input) return this.client.changeRequests.review(input);
3569
+ const { changeRequestId, ...reviewInput } = input;
3570
+ const { results } = await this.client.changeRequests.review({
3571
+ ...reviewInput,
3572
+ changeRequestIds: [changeRequestId]
3573
+ });
3574
+ const result = results[0];
3575
+ if (!result?.ok) throw batchItemError(result);
3576
+ return result.changeRequest;
3577
+ };
3578
+ const merge = async (input) => {
3579
+ if ("changeRequestIds" in input) return this.client.changeRequests.merge(input);
3580
+ const { results } = await this.client.changeRequests.merge({
3581
+ changeRequestIds: [input.changeRequestId]
3582
+ });
3583
+ const result = results[0];
3584
+ if (!result?.ok) throw batchItemError(result);
3585
+ return {
3586
+ changeRequest: result.changeRequest,
3587
+ record: result.record,
3588
+ view: result.view
3589
+ };
3590
+ };
3591
+ return new Proxy(this.client.changeRequests, {
3592
+ get(target, property, receiver) {
3593
+ if (property === "review") return review;
3594
+ if (property === "merge") return merge;
3595
+ return Reflect.get(target, property, receiver);
3596
+ }
3597
+ });
3460
3598
  }
3461
3599
  get operations() {
3462
3600
  return this.client.operations;
3463
3601
  }
3602
+ /**
3603
+ * The workspace node surface, and the single entry point for reading ONE node
3604
+ * of any type: `bb.nodes.get({ nodeId })` returns a `NodeDetailVO`
3605
+ * discriminated by `type` (`folder` carries `children`, `doc` a `body`, `file`
3606
+ * its `asset`, `skill`/`drive`/`airapp` their `files`). It replaced the four
3607
+ * typed gets (`docs`/`files`/`folders`/`fileTrees`), so a caller holding an id
3608
+ * no longer has to know the node's type before it can read it.
3609
+ *
3610
+ * `bb.nodes.list({ types })` is the matching list: a flat array of lightweight
3611
+ * summaries for just those types. Without `types` it still returns the full
3612
+ * workspace tree.
3613
+ *
3614
+ * There is no `bb.folders` any more — folders are `type: "folder"` here.
3615
+ */
3464
3616
  get nodes() {
3465
3617
  return this.client.nodes;
3466
3618
  }
@@ -3474,21 +3626,46 @@ var Busabase = class {
3474
3626
  return this.client.agent;
3475
3627
  }
3476
3628
  get assets() {
3477
- return this.client.assets;
3629
+ const filesOnlyGrep = (input) => grepAssets(this.client, input);
3630
+ return new Proxy(this.client.assets, {
3631
+ get(target, property, receiver) {
3632
+ if (property === "grep") return filesOnlyGrep;
3633
+ return Reflect.get(target, property, receiver);
3634
+ }
3635
+ });
3478
3636
  }
3479
- /** Skills, Drives, and AirApps — one surface, discriminated by `type`. */
3637
+ /**
3638
+ * Skills, Drives, and AirApps — one surface, discriminated by `type`.
3639
+ *
3640
+ * Creation and per-file reads/writes live here. Listing them and reading one
3641
+ * node's detail moved to the unified Node surface:
3642
+ * `bb.nodes.list({ types: ["skill", "drive", "airapp"] })` and
3643
+ * `bb.nodes.get({ nodeId, type })`.
3644
+ */
3480
3645
  get fileTrees() {
3481
3646
  return this.client.fileTrees;
3482
3647
  }
3648
+ /**
3649
+ * File nodes. `create` only — list with `bb.nodes.list({ types: ["file"] })`
3650
+ * and read one (backing Asset included) with `bb.nodes.get({ nodeId })`.
3651
+ */
3483
3652
  get files() {
3484
3653
  return this.client.files;
3485
3654
  }
3655
+ /**
3656
+ * Docs. Create / read a line range / update the body / open a Change Request.
3657
+ * List with `bb.nodes.list({ types: ["doc"] })` and read one (body included)
3658
+ * with `bb.nodes.get({ nodeId })`.
3659
+ *
3660
+ * There is deliberately no `bb.docs.list()` shim. The retired `GET /docs`
3661
+ * returned every Doc *with its body*; the one-call replacement returns
3662
+ * lightweight summaries, and the only way to keep the old shape would be a
3663
+ * detail request per Doc. An SDK convenience that quietly turns one call into
3664
+ * N is worse than a compile error that points at `bb.nodes`.
3665
+ */
3486
3666
  get docs() {
3487
3667
  return this.client.docs;
3488
3668
  }
3489
- get folders() {
3490
- return this.client.folders;
3491
- }
3492
3669
  get agentTasks() {
3493
3670
  return this.client.agentTasks;
3494
3671
  }
@@ -3507,10 +3684,8 @@ var Busabase = class {
3507
3684
  * source (Drive/Skill files, Doc bodies, and Base records — records read
3508
3685
  * the canonical `headCommit.fields`, never the truncated search
3509
3686
  * projection), with a shared `maxMatches`/deadline budget and per-source
3510
- * honest coverage. Use this when the answer could live anywhere; use
3511
- * `client.assets.grep` directly instead when you specifically only care
3512
- * about files and want its fuller `missing`/`stale`/`unsearchable`
3513
- * file-only reporting.
3687
+ * honest coverage. `bb.assets.grep` remains available as a files-only SDK
3688
+ * convenience and delegates here with `sources: ["files"]`.
3514
3689
  */
3515
3690
  grep(input) {
3516
3691
  return this.client.grep(input);
@@ -3560,4 +3735,4 @@ var Busabase = class {
3560
3735
  }
3561
3736
  };
3562
3737
 
3563
- export { Busabase, CREATABLE_NODE_TYPES, DEFAULT_BASE_URL, cloudContract, createBusabaseClient, normalizeBaseUrl, resolveConfig };
3738
+ export { Busabase, CREATABLE_NODE_TYPES, DEFAULT_BASE_URL, cloudContract, createBusabaseClient, getRecordByField, grepAssets, resolveConfig, toFilesOnlyGrepResult, toUnifiedFilesGrepInput };