busa-cli 0.96.0 → 0.98.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-B3hHU9aE.js";
2
+ import { i as checkForUpdate, n as pkgVersion, r as runCli } from "./run-BVOkW2wU.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-B3hHU9aE.js";
1
+ import { a as render, i as checkForUpdate, n as pkgVersion, r as runCli, t as HELP } from "./run-BVOkW2wU.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 };
@@ -18,7 +18,8 @@ import { checkAirApp } from "busabase-sdk/airapp-check";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { Uint8ArrayReader, ZipReader } from "@zip.js/zip.js";
20
20
  import QRCode from "qrcode";
21
- const LocaleSchema = z.enum({
21
+ //#region ../../packages/openlib/i18n/i18n.ts
22
+ const i18n = {
22
23
  defaultLocale: "en",
23
24
  locales: [
24
25
  "en",
@@ -46,7 +47,8 @@ const LocaleSchema = z.enum({
46
47
  "vi",
47
48
  "pt"
48
49
  ]
49
- }.locales);
50
+ };
51
+ const LocaleSchema = z.enum(i18n.locales);
50
52
  //#endregion
51
53
  //#region ../../packages/openlib/i18n/i-string.ts
52
54
  /**
@@ -158,15 +160,15 @@ const utf8ByteLength = (value) => new TextEncoder().encode(value).length;
158
160
  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
161
  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
162
  /**
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.
163
+ * One custom scenario prompt. `body` is only the task: every rendered prompt
164
+ * opens with the node's target line, the same one a built-in prompt opens with
165
+ * (see `node-agent-prompts.ts`), so authors never place it and this schema
166
+ * interpolates nothing.
164
167
  *
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.
168
+ * `{target}` is a retired placeholder that is still accepted, because bodies
169
+ * saved before the target line became fixed contain it: a line that is only
170
+ * `{target}` is dropped (the target already opens the prompt), and one inside a
171
+ * sentence reads as the node's name. Rejecting it would break those prompts.
170
172
  */
171
173
  const customPromptDefSchema = z.object({
172
174
  /** Stable id, unique within this node's custom list. */
@@ -178,7 +180,7 @@ const customPromptDefSchema = z.object({
178
180
  intent: customPromptIntentSchema.optional(),
179
181
  /** Short title shown in the dialog's left list. */
180
182
  label: customPromptLabelSchema,
181
- /** Template text; "{target}" is substituted with the rendered target line. */
183
+ /** The task. The node's target line is added ahead of it when the prompt renders. */
182
184
  body: customPromptBodySchema
183
185
  });
184
186
  /**
@@ -246,14 +248,15 @@ const PlaybookMatchFieldSchema = z.enum([
246
248
  "body"
247
249
  ]);
248
250
  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({
251
+ const playbookSearchInputShape = {
250
252
  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
253
  kinds: z.array(PlaybookKindSchema).optional().describe("Restrict to \"skill\" and/or \"prompt\". Default: both."),
252
254
  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
255
  inNodeId: z.string().trim().min(1).optional().describe("Only return playbooks on this node or inside its subtree."),
254
256
  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
257
  locale: localeSchema.optional()
256
- });
258
+ };
259
+ 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
260
  /** Fields shared by a search item and a get result: where the playbook lives. */
258
261
  const playbookLocationShape = {
259
262
  kind: PlaybookKindSchema,
@@ -1597,7 +1600,7 @@ const driveNodeType = makeFileTreeNodeType({
1597
1600
  routeBase: "drives",
1598
1601
  tag: "Drives",
1599
1602
  entryFile: "README.md",
1600
- publicAccess: "no"
1603
+ publicAccess: "detail"
1601
1604
  });
1602
1605
  //#endregion
1603
1606
  //#region ../../packages/busabase-contract/src/domains/file-node/definition.ts
@@ -1696,7 +1699,7 @@ const htmlNodeType = {
1696
1699
  capabilities: {
1697
1700
  hasDetail: true,
1698
1701
  creatable: true,
1699
- publicAccess: "no"
1702
+ publicAccess: "detail"
1700
1703
  },
1701
1704
  operations: [{
1702
1705
  kind: "html_document_update",
@@ -1714,7 +1717,7 @@ const skillNodeType = makeFileTreeNodeType({
1714
1717
  routeBase: "skills",
1715
1718
  tag: "Skills",
1716
1719
  entryFile: "SKILL.md",
1717
- publicAccess: "no"
1720
+ publicAccess: "detail"
1718
1721
  });
1719
1722
  //#endregion
1720
1723
  //#region ../../packages/busabase-contract/src/domains/whiteboard/definition.ts
@@ -1883,6 +1886,7 @@ const rejectMismatchedPayload = (input) => {
1883
1886
  const type = input.type;
1884
1887
  const wrong = [];
1885
1888
  if (input.fields !== void 0 && type !== "base") wrong.push("fields");
1889
+ if (input.skipDefaultView !== void 0 && type !== "base") wrong.push("skipDefaultView");
1886
1890
  if (input.body !== void 0 && type !== "doc") wrong.push("body");
1887
1891
  if (input.assetId !== void 0 && type !== "file") wrong.push("assetId");
1888
1892
  if ((input.files !== void 0 || input.mergeMode !== void 0) && !FILE_TREE_TYPES.has(type)) wrong.push(input.files !== void 0 ? "files" : "mergeMode");
@@ -2012,7 +2016,8 @@ const createNode = async (client, input) => {
2012
2016
  if (type === "base") {
2013
2017
  const payload = {
2014
2018
  ...common,
2015
- fields: asFieldArray(input.fields) ?? []
2019
+ fields: asFieldArray(input.fields) ?? [],
2020
+ ...input.skipDefaultView ? { defaultView: false } : {}
2016
2021
  };
2017
2022
  return client.bases.create(payload);
2018
2023
  }
@@ -2135,6 +2140,15 @@ const nodeCreateTask = {
2135
2140
  parser: "fieldSpecs"
2136
2141
  }
2137
2142
  },
2143
+ {
2144
+ name: "skipDefaultView",
2145
+ appliesWhen: {
2146
+ param: "type",
2147
+ values: ["base"]
2148
+ },
2149
+ kind: "boolean",
2150
+ 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."
2151
+ },
2138
2152
  {
2139
2153
  name: "body",
2140
2154
  kind: "string",
@@ -2225,7 +2239,7 @@ const nodeCreateTask = {
2225
2239
  {
2226
2240
  name: "agentPrompts",
2227
2241
  kind: "json",
2228
- 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`."
2242
+ 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`."
2229
2243
  }
2230
2244
  ],
2231
2245
  examples: [
@@ -2570,7 +2584,7 @@ const viewChangeTask = {
2570
2584
  name: "view_change_request",
2571
2585
  cliPath: ["views", "change-request"],
2572
2586
  summary: "Propose a Base view change (create / update / delete / restore)",
2573
- 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.",
2587
+ 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.",
2574
2588
  annotations: {
2575
2589
  readOnly: false,
2576
2590
  destructive: false
@@ -2641,6 +2655,16 @@ const viewChangeTask = {
2641
2655
  },
2642
2656
  description: "Changes to apply to the view (update only)."
2643
2657
  },
2658
+ {
2659
+ name: "position",
2660
+ kind: "number",
2661
+ min: 0,
2662
+ appliesWhen: {
2663
+ param: "action",
2664
+ values: ["update"]
2665
+ },
2666
+ 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."
2667
+ },
2644
2668
  {
2645
2669
  name: "message",
2646
2670
  kind: "string",
@@ -2665,6 +2689,7 @@ const viewChangeTask = {
2665
2689
  examples: [
2666
2690
  "busabase-cli views change-request --action create --base-id bas_1 --name \"Published\" --type table",
2667
2691
  "busabase-cli views change-request --action create --base-id bas_1 --name \"Published\" --require-review",
2692
+ "busabase-cli views change-request --action update --view-id viw_1 --position 0",
2668
2693
  "busabase-cli views change-request --action delete --view-id viw_1"
2669
2694
  ],
2670
2695
  execute: async (client, input) => {
@@ -2690,11 +2715,12 @@ const viewChangeTask = {
2690
2715
  ...meta
2691
2716
  };
2692
2717
  if (input.action === "update") {
2693
- if (!input.patch) throw new Error("action \"update\" requires patch.");
2718
+ if (!input.patch && input.position === void 0) throw new Error("action \"update\" requires patch or position.");
2694
2719
  return client.views.changeRequest({
2695
2720
  operation: "update",
2696
2721
  ...target,
2697
- ...input.patch
2722
+ ...input.patch ?? {},
2723
+ ...input.position !== void 0 ? { position: input.position } : {}
2698
2724
  });
2699
2725
  }
2700
2726
  if (input.action === "delete") return client.views.changeRequest({
@@ -2793,6 +2819,162 @@ const TASK_SUPERSEDED_MCP_TOOLS = [
2793
2819
  "bases_list_deleted_fields"
2794
2820
  ];
2795
2821
  //#endregion
2822
+ //#region ../../packages/busabase-contract/src/contract/agent-client.ts
2823
+ /**
2824
+ * Agent client attribution — "which agent program made this change".
2825
+ *
2826
+ * The API key says WHO is accountable (its name, its owner). On one machine,
2827
+ * several agents often share one key (`~/.busabase/.env`), so the key alone
2828
+ * cannot tell "Claude Code added this" from "Codex added this". A caller names
2829
+ * its agent client with ONE request header; busabase-cli detects it from the
2830
+ * environment and sends it on every request.
2831
+ *
2832
+ * It is SELF-REPORTED: a label for people reading the history, never identity,
2833
+ * authorization or anything security relevant. The server normalizes it with
2834
+ * `normalizeAgentClient` and stores only the normalized `{ id, name }`.
2835
+ */
2836
+ const BUSABASE_AGENT_HEADER = "x-busabase-agent";
2837
+ const AgentClientVOSchema = z.object({
2838
+ /** Normalized, lowercase `[a-z0-9-]`, at most 40 characters (e.g. `codex`). */
2839
+ id: z.string(),
2840
+ /** Display name: a known agent's product name, otherwise derived from the id. */
2841
+ name: z.string()
2842
+ });
2843
+ const MAX_AGENT_ID_CHARS = 40;
2844
+ /** Longer raw values are not an agent name; don't spend work on them. */
2845
+ const MAX_RAW_CHARS = 256;
2846
+ /**
2847
+ * Agents whose names we know, keyed by every id they are commonly reported
2848
+ * as. Only agents whose environment markers were verified (see busabase-cli
2849
+ * `agent-client.ts`) get a canonical entry; anything else still works through
2850
+ * the generic path below.
2851
+ */
2852
+ const KNOWN_AGENTS = {
2853
+ "claude-code": {
2854
+ id: "claude-code",
2855
+ name: "Claude Code"
2856
+ },
2857
+ claudecode: {
2858
+ id: "claude-code",
2859
+ name: "Claude Code"
2860
+ },
2861
+ codex: {
2862
+ id: "codex",
2863
+ name: "Codex"
2864
+ },
2865
+ "codex-cli": {
2866
+ id: "codex",
2867
+ name: "Codex"
2868
+ },
2869
+ "openai-codex": {
2870
+ id: "codex",
2871
+ name: "Codex"
2872
+ },
2873
+ opencode: {
2874
+ id: "opencode",
2875
+ name: "OpenCode"
2876
+ },
2877
+ "gemini-cli": {
2878
+ id: "gemini-cli",
2879
+ name: "Gemini CLI"
2880
+ },
2881
+ gemini: {
2882
+ id: "gemini-cli",
2883
+ name: "Gemini CLI"
2884
+ },
2885
+ pi: {
2886
+ id: "pi",
2887
+ name: "Pi"
2888
+ },
2889
+ "pi-coding-agent": {
2890
+ id: "pi",
2891
+ name: "Pi"
2892
+ }
2893
+ };
2894
+ /**
2895
+ * `AI_AGENT` is a cross-tool convention shaped `<tool>_<version>_agent`
2896
+ * (Claude Code: `claude-code_2-1-304_agent`); some tools set just `<tool>`
2897
+ * (OpenCode: `opencode`). Keep the tool part.
2898
+ */
2899
+ const stripAiAgentConvention = (value) => {
2900
+ return /^([^_]+)_[^_]*_agent$/.exec(value)?.[1] ?? value;
2901
+ };
2902
+ const toDisplayName = (id) => id.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2903
+ /**
2904
+ * Normalize a self-reported agent client (the `x-busabase-agent` header, or a
2905
+ * raw `AI_AGENT` value) to `{ id, name }`, or null when there is nothing
2906
+ * usable. Known agents map to their product name; anything else is reduced to
2907
+ * a lowercase `[a-z0-9-]` id of at most 40 characters with a name derived from
2908
+ * it — so the output never carries markup, whitespace or control characters.
2909
+ */
2910
+ const normalizeAgentClient = (raw) => {
2911
+ const trimmed = raw?.trim();
2912
+ if (!trimmed || trimmed.length > MAX_RAW_CHARS) return null;
2913
+ const id = stripAiAgentConvention(trimmed.toLowerCase()).replace(/[^a-z0-9]+/g, "-").slice(0, MAX_AGENT_ID_CHARS).replace(/^-+|-+$/g, "");
2914
+ if (!id) return null;
2915
+ return KNOWN_AGENTS[id] ?? {
2916
+ id,
2917
+ name: toDisplayName(id)
2918
+ };
2919
+ };
2920
+ //#endregion
2921
+ //#region src/agent-client.ts
2922
+ const isSet = (value) => {
2923
+ const trimmed = value?.trim().toLowerCase();
2924
+ return Boolean(trimmed) && trimmed !== "0" && trimmed !== "false";
2925
+ };
2926
+ /**
2927
+ * Environment markers each agent puts on the commands it runs. Checked in
2928
+ * order; only markers backed by evidence are listed:
2929
+ * - Claude Code: `CLAUDECODE=1` (measured in a command Claude Code ran).
2930
+ * - Codex CLI: `CODEX_SESSION_ID` / `CODEX_THREAD_ID` (measured, Codex 0.163).
2931
+ * - OpenCode: `OPENCODE=1` (its shell tool sets it — packages/core/src/tool/plugin/shell.ts).
2932
+ * - Gemini CLI: `GEMINI_CLI=1` (ShellExecutionService, @google/gemini-cli-core 0.65).
2933
+ * - Pi: `PI_CODING_AGENT=true` (set in its own process, inherited by its bash
2934
+ * tool; @mariozechner/pi-coding-agent 0.73).
2935
+ * Anything else is left to the generic `AI_AGENT` or the `BUSABASE_AGENT` override.
2936
+ */
2937
+ const AGENT_MARKERS = [
2938
+ {
2939
+ id: "claude-code",
2940
+ vars: ["CLAUDECODE"]
2941
+ },
2942
+ {
2943
+ id: "codex",
2944
+ vars: ["CODEX_SESSION_ID", "CODEX_THREAD_ID"]
2945
+ },
2946
+ {
2947
+ id: "opencode",
2948
+ vars: ["OPENCODE"]
2949
+ },
2950
+ {
2951
+ id: "gemini-cli",
2952
+ vars: ["GEMINI_CLI"]
2953
+ },
2954
+ {
2955
+ id: "pi",
2956
+ vars: ["PI_CODING_AGENT"]
2957
+ }
2958
+ ];
2959
+ const normalizedId = (value) => normalizeAgentClient(value)?.id;
2960
+ /**
2961
+ * Which agent client is running this CLI, as the id sent in `x-busabase-agent`.
2962
+ * Precedence: an explicit `BUSABASE_AGENT` → a specific agent's marker → the
2963
+ * cross-tool `AI_AGENT` convention (`<tool>_<version>_agent`) → none.
2964
+ * Self-reported attribution only — it labels the change, it grants nothing.
2965
+ */
2966
+ const detectAgentClient = (env) => {
2967
+ const explicit = normalizedId(env.BUSABASE_AGENT);
2968
+ if (explicit) return explicit;
2969
+ for (const marker of AGENT_MARKERS) if (marker.vars.some((name) => isSet(env[name]))) return marker.id;
2970
+ return normalizedId(env.AI_AGENT);
2971
+ };
2972
+ /** The request header for the detected agent client, or undefined when there is none. */
2973
+ const agentClientHeaders = (env) => {
2974
+ const agent = detectAgentClient(env);
2975
+ return agent ? { [BUSABASE_AGENT_HEADER]: agent } : void 0;
2976
+ };
2977
+ //#endregion
2796
2978
  //#region src/config-file.ts
2797
2979
  /**
2798
2980
  * Credential + settings storage for every Busabase client.
@@ -6199,6 +6381,27 @@ const TemplateSecretSchema = z.object({
6199
6381
  description: z.string().default(""),
6200
6382
  required: z.boolean().default(true)
6201
6383
  });
6384
+ /**
6385
+ * One locale's own gallery media — the value side of `template.locales`.
6386
+ *
6387
+ * Both fields optional and independent: a locale that only re-recorded the clip
6388
+ * declares just `video` and keeps the default screenshots. Paths are
6389
+ * package-relative in `busabase.json` and absolute URLs on the catalog card;
6390
+ * the shape is the same, which is why the card VO reuses this schema.
6391
+ */
6392
+ const TemplateLocaleMediaSchema = z.object({
6393
+ /** Replaces the default list WHOLE for this locale — never merged per index. */
6394
+ screenshots: z.array(z.string()).optional(),
6395
+ video: z.string().optional()
6396
+ });
6397
+ /**
6398
+ * Per-locale media keyed by locale (`"zh-CN"`, `"ja"`).
6399
+ *
6400
+ * Keys are plain strings on purpose, not the supported-locale enum: a template
6401
+ * written for a locale this server does not know yet must still install. The
6402
+ * audit warns about such keys; the schema never rejects them.
6403
+ */
6404
+ const TemplateLocalesSchema = z.record(z.string(), TemplateLocaleMediaSchema);
6202
6405
  const TemplateManifestSchema = z.object({
6203
6406
  /** Template Center category, e.g. `"crm"`, `"email"`, `"content"`. */
6204
6407
  category: z.string().min(1),
@@ -6219,6 +6422,21 @@ const TemplateManifestSchema = z.object({
6219
6422
  */
6220
6423
  video: z.string().optional(),
6221
6424
  /**
6425
+ * Localized gallery media, the App Store / Google Play rule: a viewer whose
6426
+ * locale has an entry sees that entry's `screenshots` / `video`; anything the
6427
+ * entry leaves out, and every locale without an entry, falls back to the two
6428
+ * fields above. Those stay the English/default set — they are what every
6429
+ * deployed server and CLI already reads, so their meaning cannot change.
6430
+ *
6431
+ * `{ "zh-CN": { "screenshots": ["assets/screenshots/cover-zh-CN.webp"],
6432
+ * "video": "assets/recordings/busa-rnd-zh-CN.mp4" } }`
6433
+ *
6434
+ * Additive by construction: an older parser strips the key (see the note on
6435
+ * `video`), so an older server simply shows the defaults. Selection lives in
6436
+ * one place — `pickTemplateMedia` in `domains/templates/media`.
6437
+ */
6438
+ locales: TemplateLocalesSchema.optional(),
6439
+ /**
6222
6440
  * Ready-made prompts shown after install ("Ask agent" prefills the first).
6223
6441
  *
6224
6442
  * They are the difference between a folder of tables and something a user can
@@ -6546,6 +6764,7 @@ z.object({
6546
6764
  required: z.boolean().default(false),
6547
6765
  options: fieldOptionsSchema.optional().default({})
6548
6766
  })).default([]),
6767
+ 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."),
6549
6768
  autoMerge: z.boolean().optional()
6550
6769
  });
6551
6770
  const createBaseFieldInputSchema = z.object({
@@ -6805,9 +7024,9 @@ z.object({
6805
7024
  createdAt: z.string()
6806
7025
  });
6807
7026
  z.object({
6808
- parentId: z.string().nullable().optional().describe("Node to start from. Omit or null to start from the space root."),
6809
- 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."),
6810
- 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."),
7027
+ 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."),
7028
+ 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)."),
7029
+ 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."),
6811
7030
  /**
6812
7031
  * Narrow to specific node types and return a FLAT list of lightweight node
6813
7032
  * summaries (`children: []`) instead of walking the tree. This is what
@@ -6831,7 +7050,7 @@ z.object({
6831
7050
  * bare string, not a 1-element array — only a REPEATED occurrence
6832
7051
  * (`?types=doc&types=file`) becomes an array. Accept both and normalize.
6833
7052
  */
6834
- 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}.")
7053
+ 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}.")
6835
7054
  }).optional();
6836
7055
  z.object({
6837
7056
  nodeId: z.string().describe("The node walked UPWARDS from — the possible descendant."),
@@ -6932,7 +7151,12 @@ const sourceAttributionSchema = z.object({
6932
7151
  ownerName: z.string().nullable(),
6933
7152
  channel: sourceChannelSchema.nullable(),
6934
7153
  /** Present only when the write declared a playbook the server could validate. */
6935
- playbook: PlaybookAttributionVOSchema.nullable().optional()
7154
+ playbook: PlaybookAttributionVOSchema.nullable().optional(),
7155
+ /**
7156
+ * Present only when the write named its agent client (`x-busabase-agent`;
7157
+ * busabase-cli sends it automatically). Self-reported — a label, not identity.
7158
+ */
7159
+ agent: AgentClientVOSchema.nullable().optional()
6936
7160
  });
6937
7161
  const commitSchema = z.object({
6938
7162
  id: z.string(),
@@ -7135,6 +7359,7 @@ const changeRequestSchema = z.object({
7135
7359
  operationCount: z.number(),
7136
7360
  reviews: z.array(reviewSchema)
7137
7361
  });
7362
+ changeRequestSchema.extend({ materialized: z.literal(false) });
7138
7363
  z.object({
7139
7364
  changeRequest: changeRequestSchema,
7140
7365
  trigger: z.enum(["changes_requested", "ai_mention"]),
@@ -7359,7 +7584,7 @@ z.object({
7359
7584
  autoMerge: destructiveAutoMerge("Archiving is reversible: the record leaves every listing but is restored intact by `operation: \"restore\"`.")
7360
7585
  });
7361
7586
  z.object({
7362
- 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."),
7587
+ 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."),
7363
7588
  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\"."),
7364
7589
  author: z.string().optional().default("local-producer"),
7365
7590
  baseCommitId: z.string().optional()
@@ -7615,6 +7840,7 @@ z.object({
7615
7840
  type: viewTypeSchema,
7616
7841
  config: viewConfigSchema,
7617
7842
  status: z.enum(["active", "archived"]),
7843
+ position: z.number().int(),
7618
7844
  createdBy: z.string(),
7619
7845
  createdByUser: userRefSchema.nullable().optional().default(null),
7620
7846
  archivedAt: z.string().nullable(),
@@ -7648,6 +7874,7 @@ const updateViewInputSchema = z.object({
7648
7874
  message: z.string().optional().default("Update view"),
7649
7875
  name: z.string().min(1).optional(),
7650
7876
  type: viewTypeSchema.optional(),
7877
+ 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."),
7651
7878
  submittedBy: z.string().optional().default("local-producer"),
7652
7879
  autoMerge: autoMergeSchema
7653
7880
  });
@@ -8246,6 +8473,25 @@ const TEXT_EXTENSIONS = /\.(?:js|mjs|cjs|ts|mts|cts|json|ndjson|md|html|css|ya?m
8246
8473
  const IS_ASSERTION_CODE = /(?:^|\/)(?:scripts\/check\.mjs|tests?\/|[^/]*\.test\.[cm]?[jt]s)$/;
8247
8474
  /** Lockfiles carry integrity hashes that are not ours to interpret. */
8248
8475
  const NOT_SOURCE = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/;
8476
+ /**
8477
+ * The locales Busabase renders its interface in — the same list `iStringParse`'s
8478
+ * `LocaleSchema` is built from, never a second copy. A `template.locales` key
8479
+ * outside it is legal (the schema accepts any string so an install never fails
8480
+ * over one) but no viewer is ever served it.
8481
+ */
8482
+ const SUPPORTED_LOCALES = i18n.locales;
8483
+ /**
8484
+ * A file name that marks itself as one non-English locale's media —
8485
+ * `cover-zh-CN.webp`, `busa-rnd-ja.mp4` — following the `<name>-<locale>.<ext>`
8486
+ * convention. Case-insensitive because file systems and authors disagree on
8487
+ * `zh-cn` versus `zh-CN`; the separator may be `-`, `_` or `.`.
8488
+ */
8489
+ const LOCALIZED_FILE = new RegExp(`[-_.](${SUPPORTED_LOCALES.filter((locale) => locale !== i18n.defaultLocale).join("|")})\\.[a-z0-9]+$`, "i");
8490
+ const localeMarkedIn = (path) => {
8491
+ const match = LOCALIZED_FILE.exec(path);
8492
+ if (!match) return void 0;
8493
+ return SUPPORTED_LOCALES.find((locale) => locale.toLowerCase() === match[1].toLowerCase());
8494
+ };
8249
8495
  const relativePaths = (files, root = "") => {
8250
8496
  const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
8251
8497
  const out = [];
@@ -8283,6 +8529,24 @@ const auditPackage = (tree, files, options = {}) => {
8283
8529
  for (const screenshot of tree.manifest.template?.screenshots ?? []) if (!present.has(screenshot)) error("package/screenshot-missing", `busabase.json declares screenshot "${screenshot}", which is not in the package. The Template Center card would render a broken image.`);
8284
8530
  const video = tree.manifest.template?.video;
8285
8531
  if (video && !present.has(video)) error("package/video-missing", `busabase.json declares video "${video}", which is not in the package. The Template Center detail page would show a play button that leads nowhere.`);
8532
+ for (const [locale, media] of Object.entries(tree.manifest.template?.locales ?? {})) {
8533
+ if (!SUPPORTED_LOCALES.includes(locale)) warn("package/locale-unknown", `busabase.json declares media for locale "${locale}", which is not one Busabase shows its interface in (${SUPPORTED_LOCALES.join(", ")}). It still installs, but no viewer is ever served it — check the spelling (keys are exact, e.g. "zh-CN", not "zh").`);
8534
+ else if (locale === i18n.defaultLocale) warn("package/locale-default-language", `busabase.json declares \`template.locales.${locale}\`, but the default \`screenshots\` / \`video\` already are the English set — and they, not \`locales.${locale}\`, are what every language without its own entry falls back to. Put English media in the defaults and drop \`locales.${locale}\`, or the two drift apart.`);
8535
+ for (const screenshot of media.screenshots ?? []) if (!present.has(screenshot)) error("package/screenshot-missing", `busabase.json declares ${locale} screenshot "${screenshot}" (template.locales.${locale}.screenshots), which is not in the package. A ${locale} viewer's Template Center card would render a broken image.`);
8536
+ if (media.video && !present.has(media.video)) error("package/video-missing", `busabase.json declares ${locale} video "${media.video}" (template.locales.${locale}.video), which is not in the package. A ${locale} viewer's detail page would show a play button that leads nowhere.`);
8537
+ }
8538
+ const defaults = [...(tree.manifest.template?.screenshots ?? []).map((path) => ({
8539
+ path,
8540
+ field: "screenshots"
8541
+ })), ...video ? [{
8542
+ path: video,
8543
+ field: "video"
8544
+ }] : []];
8545
+ for (const { path, field } of defaults) {
8546
+ const locale = localeMarkedIn(path);
8547
+ if (!locale) continue;
8548
+ warn("package/localized-default-media", `"${path}" is in the default \`template.${field}\`, but its name marks it ${locale}. The defaults are the English set every language without its own entry falls back to; move it to \`template.locales.${locale}.${field}\`.`);
8549
+ }
8286
8550
  for (const relative of paths) {
8287
8551
  const base = relative.slice(relative.lastIndexOf("/") + 1);
8288
8552
  if (/^\.env(?!\.example$)(?:\.|$)/.test(base)) error("package/dotenv", `${relative} is part of the package. Credentials belong in Vault, never in something people install.`);
@@ -9802,6 +10066,7 @@ const createBase = async (client, node, parentNodeId, options, state, bases, fie
9802
10066
  name: node.name,
9803
10067
  description: node.description,
9804
10068
  fields: immediateFields,
10069
+ ...node.base.views.length > 0 ? { defaultView: false } : {},
9805
10070
  autoMerge: true
9806
10071
  });
9807
10072
  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.`);
@@ -10329,7 +10594,10 @@ const collectBase = async (client, source, common, options) => {
10329
10594
  const base = await client.bases.get({ baseId });
10330
10595
  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.`);
10331
10596
  const allViews = await client.bases.listViews({ baseId: base.id });
10332
- const views = allViews.filter((view) => view.slug.length > 0);
10597
+ const views = allViews.filter((view) => view.slug.length > 0).map((view, index) => ({
10598
+ view,
10599
+ index
10600
+ })).sort((left, right) => (left.view.position ?? 0) - (right.view.position ?? 0) || left.index - right.index).map(({ view }) => view);
10333
10601
  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).`);
10334
10602
  const { baseSlugById, fieldSlugById } = await buildIndexes(client, options.baseResourceKeys ?? /* @__PURE__ */ new Map());
10335
10603
  const fields = [...base.fields].sort((a, b) => a.position - b.position).map((field) => toPackageField(field, {
@@ -10591,6 +10859,41 @@ const assertSelfContained = (tree, options) => {
10591
10859
  }
10592
10860
  };
10593
10861
  //#endregion
10862
+ //#region ../../packages/busabase-contract/src/domains/templates/media.ts
10863
+ const nonEmptyStrings = (value) => {
10864
+ if (!Array.isArray(value)) return void 0;
10865
+ const list = value.filter((item) => typeof item === "string" && item !== "");
10866
+ return list.length > 0 ? list : void 0;
10867
+ };
10868
+ const nonEmptyString = (value) => typeof value === "string" && value !== "" ? value : void 0;
10869
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10870
+ /**
10871
+ * Normalize an untrusted `locales` value, or `undefined` when nothing usable is
10872
+ * left.
10873
+ *
10874
+ * Never throws — the catalog is fetched JSON from a repository the server does
10875
+ * not control, and one malformed entry must not blank the gallery for every
10876
+ * other card. A non-object `locales`, a non-object entry, a non-array
10877
+ * `screenshots` or a non-string `video` is ignored, and the viewer gets the
10878
+ * defaults. An empty `screenshots: []` or `video: ""` counts as "not declared":
10879
+ * a locale that ships no gallery should see the default one, not a placeholder.
10880
+ */
10881
+ const parseTemplateLocales = (raw) => {
10882
+ if (!isPlainObject(raw)) return void 0;
10883
+ const locales = {};
10884
+ for (const [locale, entry] of Object.entries(raw)) {
10885
+ if (!locale || !isPlainObject(entry)) continue;
10886
+ const screenshots = nonEmptyStrings(entry.screenshots);
10887
+ const video = nonEmptyString(entry.video);
10888
+ if (!screenshots && !video) continue;
10889
+ locales[locale] = {
10890
+ ...screenshots ? { screenshots } : {},
10891
+ ...video ? { video } : {}
10892
+ };
10893
+ }
10894
+ return Object.keys(locales).length > 0 ? locales : void 0;
10895
+ };
10896
+ //#endregion
10594
10897
  //#region ../../packages/busabase-package/src/plan.ts
10595
10898
  /**
10596
10899
  * Install planning: package tree + the target space's current state → a plan, a
@@ -11048,6 +11351,7 @@ const discoverPackages = (files) => {
11048
11351
  }
11049
11352
  const validation = validateTemplate(tree);
11050
11353
  const claimsTemplate = Boolean(tree.rootSkill) || Boolean(tree.manifest.template);
11354
+ const locales = parseTemplateLocales(tree.manifest.template?.locales);
11051
11355
  found.push({
11052
11356
  subdir,
11053
11357
  name: tree.manifest.name,
@@ -11061,6 +11365,7 @@ const discoverPackages = (files) => {
11061
11365
  ...validation.risk ? { risk: validation.risk } : {},
11062
11366
  screenshots: tree.manifest.template?.screenshots ?? [],
11063
11367
  ...tree.manifest.template?.video ? { video: tree.manifest.template.video } : {},
11368
+ ...locales ? { locales } : {},
11064
11369
  tags: [.../* @__PURE__ */ new Set([...tree.manifest.tags, ...tree.manifest.template?.tags ?? []])],
11065
11370
  agentPrompts: tree.manifest.template?.agentPrompts ?? [],
11066
11371
  ...tree.manifest.version ? { version: tree.manifest.version } : {},
@@ -11288,6 +11593,7 @@ const toEntry = (found, airapps) => ({
11288
11593
  tags: found.tags ?? [],
11289
11594
  screenshots: found.screenshots,
11290
11595
  ...found.video ? { video: found.video } : {},
11596
+ ...found.locales ? { locales: found.locales } : {},
11291
11597
  agentPrompts: found.agentPrompts ?? [],
11292
11598
  ...found.version ? { version: found.version } : {},
11293
11599
  ...found.author ? { author: found.author } : {},
@@ -12634,6 +12940,7 @@ Quick reference — every path is relative to the base URL above; worked example
12634
12940
  | Bulk create records CR | \`POST /api/v1/bases/:baseId/records/bulk-change-request\` | propose many records (≤1000) as ONE CR |
12635
12941
  | Bulk update records CR | \`POST /api/v1/bases/:baseId/records/bulk-update-change-request\` | partially update many existing records (≤1000) as ONE atomic CR |
12636
12942
  | Create node CR | \`POST /api/v1/nodes/change-requests\` | propose folder / Skill structure changes (supports in-CR \`ref\`/\`parentNodeRef\`) |
12943
+ | 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\` |
12637
12944
  | Read Skill files | \`GET /api/v1/file-trees/:nodeId/files/:filePath\` | read a Skill file (same route for Drives and AirApps) |
12638
12945
  | Create Skill file CR | \`POST /api/v1/file-trees/:nodeId/change-requests\` | propose a Skill file edit |
12639
12946
  | Request asset upload | \`POST /api/v1/assets/upload-urls\` | request an upload target for attachment-field files |
@@ -12984,7 +13291,9 @@ curl ${base}/api/v1/openapi.json
12984
13291
  \`\`\`
12985
13292
 
12986
13293
  It is large — **do not load the whole file into context.** Pull out only the path you need
12987
- (e.g. \`curl ${base}/api/v1/openapi.json | jq '.paths["/api/v1/records"]'\`), or browse the
13294
+ (e.g. \`curl ${base}/api/v1/openapi.json | jq '.paths["/api/v1/records"]'\`); shared types such as
13295
+ \`ChangeRequest\`, \`Base\`, and \`Record\` are \`$ref\`s into \`.components.schemas\` (e.g.
13296
+ \`jq '.components.schemas.ChangeRequest'\`). Or browse the
12988
13297
  interactive docs at \`${base}/api/v1/doc\`. MCP-capable agents can skip docs entirely and
12989
13298
  connect to \`${base}/api/mcp\` (Streamable HTTP) — the most context-efficient path.
12990
13299
 
@@ -13077,9 +13386,9 @@ ${authLine} -H 'content-type: application/json' \\
13077
13386
  --data '{
13078
13387
  "agentPrompts": [
13079
13388
  { "key": "log-visit", "label": "Log a customer visit",
13080
- "body": "{target}\\n\\nAdd a visit record with the date I give, the contact I name, and a one-line summary." },
13389
+ "body": "Add a visit record with the date I give, the contact I name, and a one-line summary." },
13081
13390
  { "key": "quarter-recap", "intent": "read-only", "label": "What did we discuss this quarter",
13082
- "body": "{target}\\n\\nSummarize every visit to the account I name since the quarter started." }
13391
+ "body": "Summarize every visit to the account I name since the quarter started." }
13083
13392
  ]
13084
13393
  }'
13085
13394
  \`\`\`
@@ -13094,8 +13403,8 @@ Four rules, each of which is a way this goes wrong:
13094
13403
  write nothing — the defaults already cover the generic work.
13095
13404
  3. **Only write them when you know the job.** You usually do — the user just told you why they
13096
13405
  wanted this node. A scratch table, a one-off Doc, a folder that only groups things: no prompts.
13097
- 4. **\`{target}\` expands to a full sentence** naming the node and space, so give it its own line.
13098
- Dropped mid-sentence it reads as a run-on with two full stops.
13406
+ 4. **Write only the task.** Busabase opens every prompt with a line naming the node and space, so
13407
+ do not write \`{target}\` — the target is no longer yours to place.
13099
13408
 
13100
13409
  Prompts are addressed by node id, so they are a second call after the node exists. When your
13101
13410
  create was proposed for review instead of merged, there is no node id yet — say so, and write
@@ -14329,6 +14638,7 @@ function resolveConfig(opts, { strict = true } = {}) {
14329
14638
  spaceId: opts.spaceId ?? process.env.BUSABASE_SPACE_ID ?? file.BUSABASE_SPACE_ID,
14330
14639
  sourceChannel: "cli",
14331
14640
  playbook: resolvePlaybook(opts.playbook, strict),
14641
+ headers: agentClientHeaders(process.env),
14332
14642
  output: opts.output ?? configuredOutput() ?? "text",
14333
14643
  fetch: withRetry(fetch, { retries })
14334
14644
  };
@@ -14501,7 +14811,8 @@ async function rawFetch(config, method, path, body) {
14501
14811
  ...config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {},
14502
14812
  ...config.spaceId ? { "x-busabase-space": config.spaceId } : {},
14503
14813
  "x-busabase-channel": "cli",
14504
- ...config.playbook ? { [BUSABASE_PLAYBOOK_HEADER]: config.playbook } : {}
14814
+ ...config.playbook ? { [BUSABASE_PLAYBOOK_HEADER]: config.playbook } : {},
14815
+ ...typeof config.headers === "object" ? config.headers : {}
14505
14816
  },
14506
14817
  body: body !== void 0 ? JSON.stringify(body) : void 0
14507
14818
  });
@@ -15444,10 +15755,11 @@ function buildProgram(state = {}) {
15444
15755
  Answers "what do I send, and what comes back" without fetching anything. The
15445
15756
  contract is compiled into this CLI, so \`schema\` works offline and unauthenticated.
15446
15757
 
15447
- Prefer it over \`busabase-cli openapi\`: that returns the WHOLE document — 936 KB
15448
- across 103 endpoints as of 2026-09-04, with every type inlined per endpoint — so
15449
- reading one endpoint's arguments out of it costs roughly 234k tokens. One
15450
- endpoint through \`schema\` is a few KB.
15758
+ Prefer it over \`busabase-cli openapi\`: that returns the WHOLE document — about
15759
+ 240 KB across 106 endpoints as of 2026-10-20, with shared types behind \`$ref\`s
15760
+ into \`components.schemas\` — so reading one endpoint's arguments out of it costs
15761
+ tens of thousands of tokens plus chasing references. One endpoint through
15762
+ \`schema\` is a few KB and self-contained.
15451
15763
 
15452
15764
  busabase-cli schema # every endpoint: id, method, path
15453
15765
  busabase-cli schema ${sampleEndpointId()} # input + output schema
@@ -15678,8 +15990,8 @@ Each entry: { key: string (unique per node), intent?: "read-only" | "change",
15678
15990
  label: string | { <locale>: string }, body: string | { <locale>: string } }.
15679
15991
  Locale keys (when using the object form): en, zh-CN, zh-TW, ja, ko, de, fr, es,
15680
15992
  pt — anything else fails validation as "Invalid input" on that field.
15681
- "body" may contain a literal "{target}" placeholder, substituted at render time
15682
- with the same node/space description every built-in prompt uses. At most 50
15993
+ "body" is only the task: every prompt opens with the same node/space line the
15994
+ built-in prompts use, added for you, so do not write "{target}". At most 50
15683
15995
  entries per node, 80 characters per localized label, 8 KiB per localized body.
15684
15996
 
15685
15997
  The file is validated against this exact schema BEFORE any network request —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busa-cli",
3
- "version": "0.96.0",
3
+ "version": "0.98.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.96.0",
36
+ "busabase-sdk": "0.98.0",
37
37
  "commander": "^14.0.0",
38
38
  "qrcode": "^1.5.4",
39
39
  "tar-stream": "^3.1.7",