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 +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-B3hHU9aE.js → run-BVOkW2wU.js} +352 -40
- package/package.json +2 -2
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
}
|
|
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`
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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\":\"
|
|
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("
|
|
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"]'\`)
|
|
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": "
|
|
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": "
|
|
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.
|
|
13098
|
-
|
|
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 —
|
|
15448
|
-
across
|
|
15449
|
-
reading one endpoint's arguments out of it costs
|
|
15450
|
-
|
|
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"
|
|
15682
|
-
|
|
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.
|
|
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.
|
|
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",
|