busa-cli 0.95.1 → 0.97.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/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as checkForUpdate, n as pkgVersion, r as runCli } from "./run-fBARIIox.js";
2
+ import { i as checkForUpdate, n as pkgVersion, r as runCli } from "./run-C7tO-SDN.js";
3
3
  //#region src/flush-stdio.ts
4
4
  /**
5
5
  * Wait for stdout/stderr to reach the other side before the process is torn down.
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as render, i as checkForUpdate, n as pkgVersion, r as runCli, t as HELP } from "./run-fBARIIox.js";
1
+ import { a as render, i as checkForUpdate, n as pkgVersion, r as runCli, t as HELP } from "./run-C7tO-SDN.js";
2
2
  import { DEFAULT_BASE_URL, createBusabaseClient, normalizeBaseUrl } from "busabase-sdk";
3
3
  export { DEFAULT_BASE_URL, HELP, checkForUpdate, createBusabaseClient, normalizeBaseUrl, pkgVersion, render, runCli };
@@ -158,15 +158,15 @@ const utf8ByteLength = (value) => new TextEncoder().encode(value).length;
158
158
  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` });
159
159
  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` });
160
160
  /**
161
- * One custom scenario prompt. `body`'s `{target}` placeholder is substituted at
162
- * render time with the same target string `PromptDef.body(target)` receives
163
- * today (see `node-agent-prompts.ts`) — this schema does not interpolate it.
161
+ * One custom scenario prompt. `body` is only the task: every rendered prompt
162
+ * opens with the node's target line, the same one a built-in prompt opens with
163
+ * (see `node-agent-prompts.ts`), so authors never place it and this schema
164
+ * interpolates nothing.
164
165
  *
165
- * The placeholder is OPTIONAL and chooses placement only: a `body` that never
166
- * mentions `{target}` gets the target line prepended as its first paragraph, so
167
- * a custom prompt can never reach an agent without naming the node it acts on.
168
- * That is why the schema does not require it — forgetting it is not an error to
169
- * reject, it is a default to supply.
166
+ * `{target}` is a retired placeholder that is still accepted, because bodies
167
+ * saved before the target line became fixed contain it: a line that is only
168
+ * `{target}` is dropped (the target already opens the prompt), and one inside a
169
+ * sentence reads as the node's name. Rejecting it would break those prompts.
170
170
  */
171
171
  const customPromptDefSchema = z.object({
172
172
  /** Stable id, unique within this node's custom list. */
@@ -178,7 +178,7 @@ const customPromptDefSchema = z.object({
178
178
  intent: customPromptIntentSchema.optional(),
179
179
  /** Short title shown in the dialog's left list. */
180
180
  label: customPromptLabelSchema,
181
- /** Template text; "{target}" is substituted with the rendered target line. */
181
+ /** The task. The node's target line is added ahead of it when the prompt renders. */
182
182
  body: customPromptBodySchema
183
183
  });
184
184
  /**
@@ -246,14 +246,15 @@ const PlaybookMatchFieldSchema = z.enum([
246
246
  "body"
247
247
  ]);
248
248
  const localeSchema = z.string().trim().min(1).max(20).describe("Locale for prompt labels and the rendered prompt body, e.g. \"en\" or \"zh-CN\". Falls back to English, then to whatever locale the prompt has.");
249
- z.object({
249
+ const playbookSearchInputShape = {
250
250
  queries: z.array(z.string().max(PLAYBOOK_SEARCH_LIMITS.maxQueryChars)).max(PLAYBOOK_SEARCH_LIMITS.maxQueries).default([]).describe("2–5 phrasings of what the user wants, in the user's language AND in English (e.g. [\"记录客户拜访\", \"log customer visit\", \"visit\"]). Each is matched as a case-insensitive substring; an item hit by more phrasings ranks higher. Empty = browse: every playbook, nearest first (at most 30)."),
251
251
  kinds: z.array(PlaybookKindSchema).optional().describe("Restrict to \"skill\" and/or \"prompt\". Default: both."),
252
252
  nearNodeId: z.string().trim().min(1).optional().describe("The node the user is on. Playbooks on that node rank first, then its folder, then ancestor folders (closest first)."),
253
253
  inNodeId: z.string().trim().min(1).optional().describe("Only return playbooks on this node or inside its subtree."),
254
254
  limit: z.coerce.number().int().min(1).max(PLAYBOOK_SEARCH_LIMITS.maxLimit).optional().describe("Max items (1–50). Default 10 for a search; 30 for a browse."),
255
255
  locale: localeSchema.optional()
256
- });
256
+ };
257
+ z.strictObject(playbookSearchInputShape, { error: (issue) => issue.code === "unrecognized_keys" ? `Unknown key${issue.keys.length === 1 ? "" : "s"} ${issue.keys.map((key) => `"${key}"`).join(", ")}. playbooks search accepts: ${Object.keys(playbookSearchInputShape).join(", ")}${issue.keys.includes("query") ? " (did you mean \"queries\", an array of phrasings?)" : ""}.` : void 0 });
257
258
  /** Fields shared by a search item and a get result: where the playbook lives. */
258
259
  const playbookLocationShape = {
259
260
  kind: PlaybookKindSchema,
@@ -635,6 +636,7 @@ const baseFieldChangeTask = {
635
636
  examples: [
636
637
  "busabase-cli bases field-change-request --base-id bas_1 --operation add --slug status --name Status --field-type select",
637
638
  "busabase-cli bases field-change-request --base-id bas_1 --operation update --field-id fld_1 --patch-json '{\"required\":true}'",
639
+ "busabase-cli bases field-change-request --base-id bas_1 --operation update --field-id fld_1 --patch-json '{\"help\":{\"description\":\"Actual customer acceptance date\",\"example\":\"2026-10-14\"}}'",
638
640
  "busabase-cli bases field-change-request --base-id bas_1 --operation reorder --field-ids fld_2 --field-ids fld_1"
639
641
  ],
640
642
  execute: async (client, input) => {
@@ -1882,6 +1884,7 @@ const rejectMismatchedPayload = (input) => {
1882
1884
  const type = input.type;
1883
1885
  const wrong = [];
1884
1886
  if (input.fields !== void 0 && type !== "base") wrong.push("fields");
1887
+ if (input.skipDefaultView !== void 0 && type !== "base") wrong.push("skipDefaultView");
1885
1888
  if (input.body !== void 0 && type !== "doc") wrong.push("body");
1886
1889
  if (input.assetId !== void 0 && type !== "file") wrong.push("assetId");
1887
1890
  if ((input.files !== void 0 || input.mergeMode !== void 0) && !FILE_TREE_TYPES.has(type)) wrong.push(input.files !== void 0 ? "files" : "mergeMode");
@@ -2011,7 +2014,8 @@ const createNode = async (client, input) => {
2011
2014
  if (type === "base") {
2012
2015
  const payload = {
2013
2016
  ...common,
2014
- fields: asFieldArray(input.fields) ?? []
2017
+ fields: asFieldArray(input.fields) ?? [],
2018
+ ...input.skipDefaultView ? { defaultView: false } : {}
2015
2019
  };
2016
2020
  return client.bases.create(payload);
2017
2021
  }
@@ -2134,6 +2138,15 @@ const nodeCreateTask = {
2134
2138
  parser: "fieldSpecs"
2135
2139
  }
2136
2140
  },
2141
+ {
2142
+ name: "skipDefaultView",
2143
+ appliesWhen: {
2144
+ param: "type",
2145
+ values: ["base"]
2146
+ },
2147
+ kind: "boolean",
2148
+ description: "Base only. Create the Base WITHOUT its default \"Grid view\" (a table of every record and field). Only when you add your own views right after with view_change_request — a Base must keep at least one view, and the first one is what it opens on."
2149
+ },
2137
2150
  {
2138
2151
  name: "body",
2139
2152
  kind: "string",
@@ -2195,7 +2208,7 @@ const nodeCreateTask = {
2195
2208
  "workspace",
2196
2209
  "public"
2197
2210
  ],
2198
- description: "Skill/Drive/AirApp only. Defaults to private."
2211
+ description: "Skill/Drive/AirApp only. Omit to follow the space default and parent folder, like any other node."
2199
2212
  },
2200
2213
  {
2201
2214
  name: "version",
@@ -2224,7 +2237,7 @@ const nodeCreateTask = {
2224
2237
  {
2225
2238
  name: "agentPrompts",
2226
2239
  kind: "json",
2227
- description: "Scenario prompts shown when someone opens this node and asks an agent for something: [{\"key\":\"log-visit\",\"label\":\"Log a customer visit\",\"body\":\"{target}\\n\\nAdd a visit record with today's date, the contact I name, and a one-line summary.\",\"intent\":\"change\"}]. Write what the PERSON wants in their own words (\"Log a customer visit\"), not the operation (\"Create a record in Visits\") — the node type already covers the operations. These REPLACE the node type's default scenario prompts, so 2-5 real recurring jobs help and a generic pair is worse than none: omit this when you cannot name one. `{target}` expands to a complete sentence naming the node and space, so give it its own line. Stored in a second call after the node exists, so they are skipped (and reported) when this call proposes a ChangeRequest instead of creating the node; add them afterwards with `nodes set-agent-prompts`."
2240
+ description: "Scenario prompts shown when someone opens this node and asks an agent for something: [{\"key\":\"log-visit\",\"label\":\"Log a customer visit\",\"body\":\"Add a visit record with today's date, the contact I name, and a one-line summary.\",\"intent\":\"change\"}]. Write what the PERSON wants in their own words (\"Log a customer visit\"), not the operation (\"Create a record in Visits\") — the node type already covers the operations. These are added after the node type's built-in scenario prompts, so 2-5 real recurring jobs help and a generic pair only repeats the defaults: omit this when you cannot name one. Write only the task: Busabase opens every prompt with a line naming the node and space, so do not write `{target}`. Stored in a second call after the node exists, so they are skipped (and reported) when this call proposes a ChangeRequest instead of creating the node; add them afterwards with `nodes set-agent-prompts`."
2228
2241
  }
2229
2242
  ],
2230
2243
  examples: [
@@ -2569,7 +2582,7 @@ const viewChangeTask = {
2569
2582
  name: "view_change_request",
2570
2583
  cliPath: ["views", "change-request"],
2571
2584
  summary: "Propose a Base view change (create / update / delete / restore)",
2572
- guidance: "Review is permission-aware, decided server-side: the change merges immediately when your key has write access on the Base's node and lands as a pending ChangeRequest otherwise — check the response's `materialized` field to see which happened. Pass requireReview to always propose instead of merging. Views are structure, not content — when you are building starter structure (a Base plus the views that make it usable) let it merge so the workspace is not left half-built behind a review queue, exactly as you would for `node_create` and `POST /bases`. `create` needs the BASE id; update / delete / restore need the VIEW id — the endpoint split follows whichever id identifies the target, which this task handles for you.",
2585
+ guidance: "Review is permission-aware, decided server-side: the change merges immediately when your key has write access on the Base's node and lands as a pending ChangeRequest otherwise — check the response's `materialized` field to see which happened. Pass requireReview to always propose instead of merging. Views are structure, not content — when you are building starter structure (a Base plus the views that make it usable) let it merge so the workspace is not left half-built behind a review queue, exactly as you would for `node_create` and `POST /bases`. `create` needs the BASE id; update / delete / restore need the VIEW id — the endpoint split follows whichever id identifies the target, which this task handles for you. View order is shared by everyone: a new view joins the end, `update` with `position` moves one (0 = leftmost), and the leftmost view is what the Base opens on. Every Base keeps at least one view, so deleting its last one is rejected — create the replacement first. A new Base already starts with a default \"Grid view\" (every record and field): when you are about to add your own views, create the Base with `node_create` `skipDefaultView` (REST `defaultView: false`) so your first view is the one it opens on.",
2573
2586
  annotations: {
2574
2587
  readOnly: false,
2575
2588
  destructive: false
@@ -2640,6 +2653,16 @@ const viewChangeTask = {
2640
2653
  },
2641
2654
  description: "Changes to apply to the view (update only)."
2642
2655
  },
2656
+ {
2657
+ name: "position",
2658
+ kind: "number",
2659
+ min: 0,
2660
+ appliesWhen: {
2661
+ param: "action",
2662
+ values: ["update"]
2663
+ },
2664
+ description: "Move the view to this 0-based index among the Base's views (update only); the others shift to make room. 0 makes it the view the Base opens on."
2665
+ },
2643
2666
  {
2644
2667
  name: "message",
2645
2668
  kind: "string",
@@ -2664,6 +2687,7 @@ const viewChangeTask = {
2664
2687
  examples: [
2665
2688
  "busabase-cli views change-request --action create --base-id bas_1 --name \"Published\" --type table",
2666
2689
  "busabase-cli views change-request --action create --base-id bas_1 --name \"Published\" --require-review",
2690
+ "busabase-cli views change-request --action update --view-id viw_1 --position 0",
2667
2691
  "busabase-cli views change-request --action delete --view-id viw_1"
2668
2692
  ],
2669
2693
  execute: async (client, input) => {
@@ -2689,11 +2713,12 @@ const viewChangeTask = {
2689
2713
  ...meta
2690
2714
  };
2691
2715
  if (input.action === "update") {
2692
- if (!input.patch) throw new Error("action \"update\" requires patch.");
2716
+ if (!input.patch && input.position === void 0) throw new Error("action \"update\" requires patch or position.");
2693
2717
  return client.views.changeRequest({
2694
2718
  operation: "update",
2695
2719
  ...target,
2696
- ...input.patch
2720
+ ...input.patch ?? {},
2721
+ ...input.position !== void 0 ? { position: input.position } : {}
2697
2722
  });
2698
2723
  }
2699
2724
  if (input.action === "delete") return client.views.changeRequest({
@@ -6445,7 +6470,13 @@ const lookupRollupSchema = z.enum([
6445
6470
  "max",
6446
6471
  "concatenate"
6447
6472
  ]).default("values");
6473
+ /** Public, plain-text guidance. Examples are hints, never default record values. */
6474
+ const fieldHelpSchema = z.object({
6475
+ description: z.string().max(1e3).optional(),
6476
+ example: z.string().max(200).optional()
6477
+ });
6448
6478
  const fieldOptionsSchema = z.object({
6479
+ help: fieldHelpSchema.optional(),
6449
6480
  ai: z.object({
6450
6481
  model: z.string().optional(),
6451
6482
  prompt: z.string().optional(),
@@ -6476,7 +6507,18 @@ const fieldOptionsSchema = z.object({
6476
6507
  height: z.number().int().positive().max(1200).optional(),
6477
6508
  providers: z.array(z.string()).optional()
6478
6509
  }).optional(),
6479
- formula: z.object({ expression: z.string().min(1) }).optional(),
6510
+ formula: z.object({
6511
+ expression: z.string().min(1),
6512
+ format: z.enum([
6513
+ "auto",
6514
+ "number",
6515
+ "currency",
6516
+ "percent",
6517
+ "date"
6518
+ ]).optional(),
6519
+ precision: z.number().int().min(0).max(10).optional(),
6520
+ currency: z.string().regex(/^[A-Z]{3}$/).optional()
6521
+ }).optional(),
6480
6522
  inverseFieldId: z.string().optional(),
6481
6523
  lookup: z.object({
6482
6524
  relationFieldSlug: z.string().min(1).describe("Slug of a `relation` field on THIS Base — the hop to follow."),
@@ -6528,6 +6570,7 @@ z.object({
6528
6570
  required: z.boolean().default(false),
6529
6571
  options: fieldOptionsSchema.optional().default({})
6530
6572
  })).default([]),
6573
+ defaultView: z.boolean().optional().describe("Whether the new Base gets its default \"Grid view\" (a table view showing every record and field). Defaults to true. Pass false only when you add your own views right away — a Base must always keep at least one view, and the leftmost one is what it opens on."),
6531
6574
  autoMerge: z.boolean().optional()
6532
6575
  });
6533
6576
  const createBaseFieldInputSchema = z.object({
@@ -6566,7 +6609,9 @@ const updateFieldChangeRequestInputSchema = z.object({
6566
6609
  patch: z.object({
6567
6610
  name: fieldNameSchema.optional(),
6568
6611
  required: z.boolean().optional(),
6569
- options: fieldOptionsSchema.optional(),
6612
+ options: fieldOptionsSchema.removeDefault().optional(),
6613
+ /** Applied against current options at merge time; null removes all guidance. */
6614
+ help: fieldHelpSchema.nullable().optional(),
6570
6615
  /**
6571
6616
  * Not a patch key — rejected on purpose, and the only key here that is.
6572
6617
  *
@@ -6785,9 +6830,9 @@ z.object({
6785
6830
  createdAt: z.string()
6786
6831
  });
6787
6832
  z.object({
6788
- parentId: z.string().nullable().optional().describe("Node to start from. Omit or null to start from the space root."),
6789
- depth: z.coerce.number().int().min(1).max(5).optional().describe("How many levels beneath the start point to eagerly include (default 2 once either field is set). Capped at 5."),
6790
- status: z.enum(["active", "archived"]).optional().default("active").describe("`active` walks the live TREE. `archived` returns a FLAT list of soft-archived nodes (the Trash view) with no parent/depth walk — so the response shape you can rely on differs between the two, not just the rows."),
6833
+ parentId: z.string().nullable().optional().describe("Folder to start from: a node id or a folder slug. Omit or null to start from the space root. An unknown ref is a 404 and a node that is not a folder is a 400 — never an empty list. With `types`, scopes the flat list to this folder's subtree."),
6834
+ depth: z.coerce.number().int().min(1).max(5).optional().describe("How many levels beneath the start point to eagerly include (default 2 once either field is set). Capped at 5. With `types` and `parentId`, how many levels of the folder's subtree to search (default: all of it)."),
6835
+ status: z.enum(["active", "archived"]).optional().default("active").describe("`active` walks the live TREE. `archived` returns a FLAT list of soft-archived nodes (the Trash view) with no parent/depth walk — so the response shape you can rely on differs between the two, not just the rows. `parentId` with `archived` is a 400."),
6791
6836
  /**
6792
6837
  * Narrow to specific node types and return a FLAT list of lightweight node
6793
6838
  * summaries (`children: []`) instead of walking the tree. This is what
@@ -6811,7 +6856,7 @@ z.object({
6811
6856
  * bare string, not a 1-element array — only a REPEATED occurrence
6812
6857
  * (`?types=doc&types=file`) becomes an array. Accept both and normalize.
6813
6858
  */
6814
- types: z.union([z.array(z.enum(NODE_TYPES)), z.enum(NODE_TYPES)]).transform((value) => Array.isArray(value) ? value : [value]).optional().describe("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}.")
6859
+ types: z.union([z.array(z.enum(NODE_TYPES)), z.enum(NODE_TYPES)]).transform((value) => Array.isArray(value) ? value : [value]).optional().describe("Return a flat list of lightweight summaries for these node types instead of the tree — across the space, or within `parentId`'s subtree when given. Read one node's full detail with GET /nodes/{nodeId}.")
6815
6860
  }).optional();
6816
6861
  z.object({
6817
6862
  nodeId: z.string().describe("The node walked UPWARDS from — the possible descendant."),
@@ -7102,7 +7147,7 @@ const changeRequestSchema = z.object({
7102
7147
  sourceAttribution: sourceAttributionSchema.nullable().optional(),
7103
7148
  sourceMeta: z.record(z.string(), z.unknown()),
7104
7149
  reviewPolicySnapshot: z.record(z.string(), z.unknown()),
7105
- mergeSummary: z.record(z.string(), z.unknown()),
7150
+ mergeSummary: z.record(z.string(), z.unknown()).describe("Filled in when the change request merges. `slugChanges` (present only when non-empty) lists items a restore brought back under a new slug because another item took the old one while they were in the Trash: `{ nodeId, nodeType, name, fromSlug, toSlug }`. Read the new slug from here instead of assuming the old one."),
7106
7151
  rejectedReason: z.string().nullable(),
7107
7152
  reviewedAt: z.string().nullable(),
7108
7153
  mergedAt: z.string().nullable(),
@@ -7115,6 +7160,7 @@ const changeRequestSchema = z.object({
7115
7160
  operationCount: z.number(),
7116
7161
  reviews: z.array(reviewSchema)
7117
7162
  });
7163
+ changeRequestSchema.extend({ materialized: z.literal(false) });
7118
7164
  z.object({
7119
7165
  changeRequest: changeRequestSchema,
7120
7166
  trigger: z.enum(["changes_requested", "ai_mention"]),
@@ -7339,7 +7385,7 @@ z.object({
7339
7385
  autoMerge: destructiveAutoMerge("Archiving is reversible: the record leaves every listing but is restored intact by `operation: \"restore\"`.")
7340
7386
  });
7341
7387
  z.object({
7342
- fields: z.record(z.string(), z.unknown()).describe("Updated field values keyed by field slug. If you set the base's PRIMARY field (its first field), keep it a short human-readable name — it is the record's display title everywhere."),
7388
+ fields: z.record(z.string(), z.unknown()).describe("Updated field values keyed by field slug. A field id or its exact display name (any language, unambiguous) is mapped to the slug; a write in which no key names a field of the Base is a 400 listing the valid slugs. If you set the base's PRIMARY field (its first field), keep it a short human-readable name — it is the record's display title everywhere."),
7343
7389
  message: z.string().optional().default("Revise operation").describe("Explanation shown to the human reviewer. Write a conventional-commit style subject — imperative verb + what + why, e.g. \"Update deal stage to qualified — demo booked for July 8\"."),
7344
7390
  author: z.string().optional().default("local-producer"),
7345
7391
  baseCommitId: z.string().optional()
@@ -7595,6 +7641,7 @@ z.object({
7595
7641
  type: viewTypeSchema,
7596
7642
  config: viewConfigSchema,
7597
7643
  status: z.enum(["active", "archived"]),
7644
+ position: z.number().int(),
7598
7645
  createdBy: z.string(),
7599
7646
  createdByUser: userRefSchema.nullable().optional().default(null),
7600
7647
  archivedAt: z.string().nullable(),
@@ -7628,6 +7675,7 @@ const updateViewInputSchema = z.object({
7628
7675
  message: z.string().optional().default("Update view"),
7629
7676
  name: z.string().min(1).optional(),
7630
7677
  type: viewTypeSchema.optional(),
7678
+ position: z.number().int().min(0).optional().describe("Move the view to this 0-based index among the Base's active views; the others shift to make room. The order is shared by everyone, and the leftmost view (0) is the one the Base opens on. Out-of-range values clamp to the end."),
7631
7679
  submittedBy: z.string().optional().default("local-producer"),
7632
7680
  autoMerge: autoMergeSchema
7633
7681
  });
@@ -7872,6 +7920,7 @@ const PackageDocFrontmatterSchema = z.object({
7872
7920
  * so choice ids survive and record values referencing them need no remap.
7873
7921
  */
7874
7922
  const PackageFieldOptionsSchema = z.object({
7923
+ help: fieldHelpSchema.optional(),
7875
7924
  ai: z.object({
7876
7925
  model: z.string().optional(),
7877
7926
  prompt: z.string().optional(),
@@ -9647,7 +9696,7 @@ const applyAgentPrompts = async (client, nodeId, node, state) => {
9647
9696
  const agentPrompts = node.agentPrompts;
9648
9697
  if (!agentPrompts?.length) return;
9649
9698
  if (!nodeId) {
9650
- state.warnings.push(`"${node.slug}" installs as a change request, so its ${agentPrompts.length} agent prompt(s) were not applied. After merging it, run \`busabase-cli nodes set-agent-prompts\` for that node, or re-install with --auto-merge.`);
9699
+ state.warnings.push(`"${node.slug}" installs as a change request, so its ${agentPrompts.length} agent prompt(s) were not applied. After merging it, run \`busabase-cli nodes set-agent-prompts\` for that node, or re-install without asking for review (drop --require-review, or leave "Send the content for review first" off).`);
9651
9700
  return;
9652
9701
  }
9653
9702
  try {
@@ -9781,6 +9830,7 @@ const createBase = async (client, node, parentNodeId, options, state, bases, fie
9781
9830
  name: node.name,
9782
9831
  description: node.description,
9783
9832
  fields: immediateFields,
9833
+ ...node.base.views.length > 0 ? { defaultView: false } : {},
9784
9834
  autoMerge: true
9785
9835
  });
9786
9836
  if (!result.materialized) throw new Error(`Base "${node.slug}" was requested immediately but the server returned a pending change request; install cannot attach its views or records to it.`);
@@ -10308,7 +10358,10 @@ const collectBase = async (client, source, common, options) => {
10308
10358
  const base = await client.bases.get({ baseId });
10309
10359
  if (!base) throw new Error(`Base node "${source.slug}" has no Base behind it (looked up ${baseId}). The space may be mid-migration; re-run export.`);
10310
10360
  const allViews = await client.bases.listViews({ baseId: base.id });
10311
- const views = allViews.filter((view) => view.slug.length > 0);
10361
+ const views = allViews.filter((view) => view.slug.length > 0).map((view, index) => ({
10362
+ view,
10363
+ index
10364
+ })).sort((left, right) => (left.view.position ?? 0) - (right.view.position ?? 0) || left.index - right.index).map(({ view }) => view);
10312
10365
  if (views.length !== allViews.length) options.warn(`Base "${common.slug}" has ${allViews.length - views.length} view(s) with no slug — they were dropped (predates the slug requirement; rename it in the UI to keep it).`);
10313
10366
  const { baseSlugById, fieldSlugById } = await buildIndexes(client, options.baseResourceKeys ?? /* @__PURE__ */ new Map());
10314
10367
  const fields = [...base.fields].sort((a, b) => a.position - b.position).map((field) => toPackageField(field, {
@@ -10971,7 +11024,7 @@ const findPlanBlockers = (plan, autoMerge) => {
10971
11024
  const list = plan.collisions.filter((collision) => !collision.renamedTo).map((collision) => ` • ${collision.kind} "${collision.slug}" (${collision.path})`).join("\n");
10972
11025
  blockers.push(`Install would collide with content that already exists:\n${list}\n\nNothing was created. Re-run with --rename to install the colliding items under suffixed slugs (e.g. "-2"), or pick a different --into-folder.`);
10973
11026
  }
10974
- if (plan.requiresAutoMerge && !autoMerge) blockers.push("This package's records carry relation values, and a relation stores the ids of the records it points at — which only exist once the records are merged. Installing review-first would leave every relation empty.\n\nRe-run with --auto-merge to install it (this trusts the package author: records are created immediately instead of landing as change requests you review first).");
11027
+ if (plan.requiresAutoMerge && !autoMerge) blockers.push("This package's records carry relation values, and a relation stores the ids of the records it points at — which only exist once the records are merged. Installing review-first would leave every relation empty.\n\nInstall it without asking for review: drop --require-review, or leave \"Send the content for review first\" off in the dashboard. Records are then created immediately. A credential that can only propose changes cannot install this package; use one with write access to the space.");
10975
11028
  const unsatisfiable = findUnsatisfiableRequiredFields(plan);
10976
11029
  if (unsatisfiable.length > 0) blockers.push(`This package cannot satisfy its own required fields:\n${unsatisfiable.join("\n")}\n\nThe server rejects these while writing records, partway through the install. Re-export the package (export relaxes fields it cannot fill), or edit the Base definitions in the package to make them optional.`);
10977
11030
  return blockers;
@@ -12243,6 +12296,11 @@ const PROCEDURE_PERMISSION_POLICY = {
12243
12296
  "agents.connections.list": workspace("manage"),
12244
12297
  "agents.disconnect": workspace("manage"),
12245
12298
  "agents.deleteHistory": workspace("manage"),
12299
+ "agents.routerEnrollment.grants": workspace("manage"),
12300
+ "agents.routerEnrollment.mint": workspace("manage"),
12301
+ "agents.routerEnrollment.status": workspace("manage"),
12302
+ "agents.routerEnrollment.complete": workspace("manage"),
12303
+ "agents.routerEnrollment.cancel": workspace("manage"),
12246
12304
  "agents.sessions.list": workspace("manage"),
12247
12305
  "agents.sessions.listPaged": workspace("manage"),
12248
12306
  "agents.sessions.create": workspace("manage"),
@@ -12256,6 +12314,7 @@ const PROCEDURE_PERMISSION_POLICY = {
12256
12314
  "changeRequests.listPage": node("read"),
12257
12315
  "changeRequests.inboxSnapshot": node("read"),
12258
12316
  "changeRequests.counts": node("read"),
12317
+ "changeRequests.reviewCount": node("read"),
12259
12318
  "changeRequests.get": node("read"),
12260
12319
  "changeRequests.createPreviewLink": node("changeRequest"),
12261
12320
  "records.list": node("read"),
@@ -12607,6 +12666,7 @@ Quick reference — every path is relative to the base URL above; worked example
12607
12666
  | Bulk create records CR | \`POST /api/v1/bases/:baseId/records/bulk-change-request\` | propose many records (≤1000) as ONE CR |
12608
12667
  | Bulk update records CR | \`POST /api/v1/bases/:baseId/records/bulk-update-change-request\` | partially update many existing records (≤1000) as ONE atomic CR |
12609
12668
  | Create node CR | \`POST /api/v1/nodes/change-requests\` | propose folder / Skill structure changes (supports in-CR \`ref\`/\`parentNodeRef\`) |
12669
+ | View CR | \`POST /api/v1/views/change-requests\` | create / update / delete / restore a Base view. Order is shared: \`update\` with \`position\` reorders, the leftmost view is what the Base opens on, and a Base's last view can't be deleted. A new Base starts with a "Grid view" unless created with \`"defaultView": false\` |
12610
12670
  | Read Skill files | \`GET /api/v1/file-trees/:nodeId/files/:filePath\` | read a Skill file (same route for Drives and AirApps) |
12611
12671
  | Create Skill file CR | \`POST /api/v1/file-trees/:nodeId/change-requests\` | propose a Skill file edit |
12612
12672
  | Request asset upload | \`POST /api/v1/assets/upload-urls\` | request an upload target for attachment-field files |
@@ -12957,7 +13017,9 @@ curl ${base}/api/v1/openapi.json
12957
13017
  \`\`\`
12958
13018
 
12959
13019
  It is large — **do not load the whole file into context.** Pull out only the path you need
12960
- (e.g. \`curl ${base}/api/v1/openapi.json | jq '.paths["/api/v1/records"]'\`), or browse the
13020
+ (e.g. \`curl ${base}/api/v1/openapi.json | jq '.paths["/api/v1/records"]'\`); shared types such as
13021
+ \`ChangeRequest\`, \`Base\`, and \`Record\` are \`$ref\`s into \`.components.schemas\` (e.g.
13022
+ \`jq '.components.schemas.ChangeRequest'\`). Or browse the
12961
13023
  interactive docs at \`${base}/api/v1/doc\`. MCP-capable agents can skip docs entirely and
12962
13024
  connect to \`${base}/api/mcp\` (Streamable HTTP) — the most context-efficient path.
12963
13025
 
@@ -13050,9 +13112,9 @@ ${authLine} -H 'content-type: application/json' \\
13050
13112
  --data '{
13051
13113
  "agentPrompts": [
13052
13114
  { "key": "log-visit", "label": "Log a customer visit",
13053
- "body": "{target}\\n\\nAdd a visit record with the date I give, the contact I name, and a one-line summary." },
13115
+ "body": "Add a visit record with the date I give, the contact I name, and a one-line summary." },
13054
13116
  { "key": "quarter-recap", "intent": "read-only", "label": "What did we discuss this quarter",
13055
- "body": "{target}\\n\\nSummarize every visit to the account I name since the quarter started." }
13117
+ "body": "Summarize every visit to the account I name since the quarter started." }
13056
13118
  ]
13057
13119
  }'
13058
13120
  \`\`\`
@@ -13067,8 +13129,8 @@ Four rules, each of which is a way this goes wrong:
13067
13129
  write nothing — the defaults already cover the generic work.
13068
13130
  3. **Only write them when you know the job.** You usually do — the user just told you why they
13069
13131
  wanted this node. A scratch table, a one-off Doc, a folder that only groups things: no prompts.
13070
- 4. **\`{target}\` expands to a full sentence** naming the node and space, so give it its own line.
13071
- Dropped mid-sentence it reads as a run-on with two full stops.
13132
+ 4. **Write only the task.** Busabase opens every prompt with a line naming the node and space, so
13133
+ do not write \`{target}\` — the target is no longer yours to place.
13072
13134
 
13073
13135
  Prompts are addressed by node id, so they are a second call after the node exists. When your
13074
13136
  create was proposed for review instead of merged, there is no node id yet — say so, and write
@@ -15417,10 +15479,11 @@ function buildProgram(state = {}) {
15417
15479
  Answers "what do I send, and what comes back" without fetching anything. The
15418
15480
  contract is compiled into this CLI, so \`schema\` works offline and unauthenticated.
15419
15481
 
15420
- Prefer it over \`busabase-cli openapi\`: that returns the WHOLE document — 936 KB
15421
- across 103 endpoints as of 2026-09-04, with every type inlined per endpoint — so
15422
- reading one endpoint's arguments out of it costs roughly 234k tokens. One
15423
- endpoint through \`schema\` is a few KB.
15482
+ Prefer it over \`busabase-cli openapi\`: that returns the WHOLE document — about
15483
+ 240 KB across 106 endpoints as of 2026-10-20, with shared types behind \`$ref\`s
15484
+ into \`components.schemas\` — so reading one endpoint's arguments out of it costs
15485
+ tens of thousands of tokens plus chasing references. One endpoint through
15486
+ \`schema\` is a few KB and self-contained.
15424
15487
 
15425
15488
  busabase-cli schema # every endpoint: id, method, path
15426
15489
  busabase-cli schema ${sampleEndpointId()} # input + output schema
@@ -15651,8 +15714,8 @@ Each entry: { key: string (unique per node), intent?: "read-only" | "change",
15651
15714
  label: string | { <locale>: string }, body: string | { <locale>: string } }.
15652
15715
  Locale keys (when using the object form): en, zh-CN, zh-TW, ja, ko, de, fr, es,
15653
15716
  pt — anything else fails validation as "Invalid input" on that field.
15654
- "body" may contain a literal "{target}" placeholder, substituted at render time
15655
- with the same node/space description every built-in prompt uses. At most 50
15717
+ "body" is only the task: every prompt opens with the same node/space line the
15718
+ built-in prompts use, added for you, so do not write "{target}". At most 50
15656
15719
  entries per node, 80 characters per localized label, 8 KiB per localized body.
15657
15720
 
15658
15721
  The file is validated against this exact schema BEFORE any network request —
@@ -15878,7 +15941,8 @@ manual lands as a Skill node, its nodes are stamped as belonging to that app, it
15878
15941
  Base slugs are prefixed with the target folder so two templates cannot collide on
15879
15942
  a name like "settings", and its sample rows are merged so the app is not empty
15880
15943
  when you open it (--no-sample-records opts out). Its AirApp code and its Skill
15881
- still wait for your review.
15944
+ follow the same rule as other content: live if you can write, held for review
15945
+ with --require-review.
15882
15946
 
15883
15947
  If the URL is a repository that holds several packages rather than being one,
15884
15948
  install lists them and you pick with --skill <name>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busa-cli",
3
- "version": "0.95.1",
3
+ "version": "0.97.0",
4
4
  "description": "Command-line client for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server`. Short-name alias for busabase-cli.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-cli",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@zip.js/zip.js": "^2.8.20",
35
35
  "busabase-package": "workspace:*",
36
- "busabase-sdk": "0.95.1",
36
+ "busabase-sdk": "0.97.0",
37
37
  "commander": "^14.0.0",
38
38
  "qrcode": "^1.5.4",
39
39
  "tar-stream": "^3.1.7",