busa-cli 0.60.0 → 0.71.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/bin/busabase-cli.mjs +9 -1
- package/dist/cli.js +37 -1
- package/dist/index.js +1 -1
- package/dist/{run-bQ1VIBLc.js → run-BkaaGFK_.js} +313 -70
- package/package.json +2 -2
package/bin/busabase-cli.mjs
CHANGED
|
@@ -24,7 +24,15 @@ try {
|
|
|
24
24
|
if (!Array.isArray(delegatedArgv) || delegatedArgv.some((arg) => typeof arg !== "string")) {
|
|
25
25
|
throw new Error("BUSABASE_CLI_DELEGATED_ARGV must be a JSON string array.");
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
const delegatedExit = await runCli(delegatedArgv);
|
|
28
|
+
// process.exit() drops whatever stdout has not flushed, and stdout is async when
|
|
29
|
+
// it is a pipe — so a large `--output json` body would arrive truncated at one
|
|
30
|
+
// pipe buffer. Inlined rather than imported from dist: this file is the entry
|
|
31
|
+
// point and must keep working even when dist is mid-build. See src/flush-stdio.ts.
|
|
32
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
33
|
+
if (stream.writableLength > 0) await new Promise((done) => stream.write("", done));
|
|
34
|
+
}
|
|
35
|
+
process.exit(delegatedExit);
|
|
28
36
|
}
|
|
29
37
|
await import(builtCli.href);
|
|
30
38
|
} catch (error) {
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
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-BkaaGFK_.js";
|
|
3
|
+
//#region src/flush-stdio.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wait for stdout/stderr to reach the other side before the process is torn down.
|
|
6
|
+
*
|
|
7
|
+
* `process.exit()` discards whatever is still buffered. That is harmless when stdout
|
|
8
|
+
* is a file or a TTY — Node writes those synchronously — but a PIPE is asynchronous,
|
|
9
|
+
* so exiting immediately after a large `console.log` delivers only what happened to
|
|
10
|
+
* fit in the pipe buffer and drops the rest, with a zero exit code either way.
|
|
11
|
+
*
|
|
12
|
+
* The symptom is brutal to diagnose because it depends on the caller, not the command:
|
|
13
|
+
*
|
|
14
|
+
* busabase-cli skills read-file … --output json > body.json # 41970 bytes
|
|
15
|
+
* busabase-cli skills read-file … --output json | jq .content # 8192 bytes
|
|
16
|
+
*
|
|
17
|
+
* and 8192 is one pipe buffer, so the JSON simply stops mid-string. Anyone piping a
|
|
18
|
+
* large skill or record body into a parser gets a parse error that reads like a server
|
|
19
|
+
* problem. It has been misattributed to stale CLI versions before; the version is
|
|
20
|
+
* irrelevant, the destination is everything.
|
|
21
|
+
*
|
|
22
|
+
* Writing an empty chunk with a callback is the portable way to ask "is everything I
|
|
23
|
+
* queued delivered?" — the callback fires after the stream has flushed what preceded
|
|
24
|
+
* it. A stream that is already drained resolves without queueing anything.
|
|
25
|
+
*/
|
|
26
|
+
const flush = (stream) => new Promise((resolve) => {
|
|
27
|
+
if (stream.writableLength === 0) {
|
|
28
|
+
resolve();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
stream.write("", () => resolve());
|
|
32
|
+
});
|
|
33
|
+
const flushStdio = async () => {
|
|
34
|
+
await flush(process.stdout);
|
|
35
|
+
await flush(process.stderr);
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
3
38
|
//#region src/cli.ts
|
|
4
39
|
const exitCode = await runCli(process.argv.slice(2));
|
|
5
40
|
const notice = await checkForUpdate("busabase-cli", pkgVersion());
|
|
6
41
|
if (notice) console.error(notice);
|
|
42
|
+
await flushStdio();
|
|
7
43
|
process.exit(exitCode);
|
|
8
44
|
//#endregion
|
|
9
45
|
export {};
|
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-BkaaGFK_.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 };
|
|
@@ -1420,22 +1420,34 @@ const formNodeType = {
|
|
|
1420
1420
|
label: "Form",
|
|
1421
1421
|
icon: "form",
|
|
1422
1422
|
/**
|
|
1423
|
-
* `hidden
|
|
1423
|
+
* No longer `hidden`. The two conditions that hid it are both met:
|
|
1424
1424
|
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1425
|
+
* 1. The New-item flow ASKS FOR THE TARGET BASE — `create-node-modal.tsx`
|
|
1426
|
+
* renders a "Writes into" Base picker (plus the field checklist) whenever
|
|
1427
|
+
* `form` is the selected type, and keeps its submit disabled until one is
|
|
1428
|
+
* chosen, the same way it already does for `file` and its asset.
|
|
1429
|
+
* 2. A MATERIALIZER WRITES THE CONFIG ROW — `materializeFormNode`
|
|
1430
|
+
* (busabase-core `domains/form/logic/form-ops.ts`) inserts the
|
|
1431
|
+
* `busabase_forms` row inside the merge transaction, from the
|
|
1432
|
+
* `node_create` operation's `metadata.targetBaseId` / `metadata.formBindings`.
|
|
1433
|
+
* Both create paths run it, so a Form merged after review is configured
|
|
1434
|
+
* exactly like one created immediately.
|
|
1435
|
+
*
|
|
1436
|
+
* A Form that still arrives unconfigured (an API caller that sent only the
|
|
1437
|
+
* generic node fields) is no longer a dead end either: the detail view's
|
|
1438
|
+
* "not set up yet" state now carries a "Connect this form to a Base" action.
|
|
1439
|
+
*
|
|
1440
|
+
* `hidden` is read by THREE create surfaces — web's `create-node-modal.tsx`,
|
|
1441
|
+
* `node-agent-prompts.ts`, and React Native's
|
|
1442
|
+
* `apps/busabase-mobile/.../CreateNodeModal.tsx`. Mobile has no Base picker,
|
|
1443
|
+
* so it names `form` in its own `UNSUPPORTED_TYPES` set (alongside `file`,
|
|
1444
|
+
* which is excluded there for the same "can't collect the required input"
|
|
1445
|
+
* reason) rather than re-creating the dead end on another platform.
|
|
1433
1446
|
*/
|
|
1434
1447
|
capabilities: {
|
|
1435
1448
|
hasDetail: true,
|
|
1436
1449
|
creatable: true,
|
|
1437
|
-
publicAccess: "submit"
|
|
1438
|
-
hidden: true
|
|
1450
|
+
publicAccess: "submit"
|
|
1439
1451
|
},
|
|
1440
1452
|
operations: []
|
|
1441
1453
|
};
|
|
@@ -1710,18 +1722,10 @@ const writeAgentPrompts = async (client, type, result, agentPrompts) => {
|
|
|
1710
1722
|
nextStep: "Once the change request merges: busabase-cli nodes set-agent-prompts --node-id <nodeId> --file prompts.json"
|
|
1711
1723
|
}
|
|
1712
1724
|
};
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
});
|
|
1718
|
-
} catch (error) {
|
|
1719
|
-
if ((isRecord(error) ? error.status : void 0) !== 404) throw error;
|
|
1720
|
-
await client.nodes.updateMetadata({
|
|
1721
|
-
nodeId,
|
|
1722
|
-
metadata: { agentPrompts }
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
+
await client.nodes.updateAgentPrompts({
|
|
1726
|
+
nodeId,
|
|
1727
|
+
agentPrompts
|
|
1728
|
+
});
|
|
1725
1729
|
return {
|
|
1726
1730
|
...isRecord(result) ? result : { result },
|
|
1727
1731
|
agentPromptsWrite: {
|
|
@@ -6138,6 +6142,7 @@ const fieldTypeSchema = z.enum([
|
|
|
6138
6142
|
"html",
|
|
6139
6143
|
"attachment",
|
|
6140
6144
|
"relation",
|
|
6145
|
+
"member",
|
|
6141
6146
|
"number",
|
|
6142
6147
|
"date",
|
|
6143
6148
|
"checkbox",
|
|
@@ -6244,8 +6249,7 @@ const baseSchema = z.object({
|
|
|
6244
6249
|
requiredApprovals: z.number()
|
|
6245
6250
|
}),
|
|
6246
6251
|
createdAt: z.string(),
|
|
6247
|
-
fields: z.array(baseFieldSchema)
|
|
6248
|
-
metadata: z.record(z.string(), z.unknown()).default({})
|
|
6252
|
+
fields: z.array(baseFieldSchema)
|
|
6249
6253
|
});
|
|
6250
6254
|
z.object({
|
|
6251
6255
|
parentNodeId: z.string().optional().describe("Parent node id. Must be a folder or the space root; container-incapable node types (Base, Doc, AirApp, etc.) cannot hold children."),
|
|
@@ -6495,6 +6499,25 @@ z.object({
|
|
|
6495
6499
|
expiresAt: z.string().nullable(),
|
|
6496
6500
|
updatedAt: z.string()
|
|
6497
6501
|
});
|
|
6502
|
+
z.object({
|
|
6503
|
+
nodeId: z.string(),
|
|
6504
|
+
name: z.string(),
|
|
6505
|
+
slug: z.string(),
|
|
6506
|
+
type: z.enum(NODE_TYPES),
|
|
6507
|
+
icon: NodeIconSchema.nullable().default(null),
|
|
6508
|
+
capability: z.enum(["read", "submit"]),
|
|
6509
|
+
hasPassword: z.boolean(),
|
|
6510
|
+
expiresAt: z.string().nullable(),
|
|
6511
|
+
/**
|
|
6512
|
+
* When this share ROW was first created — NOT "public continuously since".
|
|
6513
|
+
* `disableNodeShare` flips `scope` to `"none"` and keeps the row, so a node
|
|
6514
|
+
* that was shared, revoked, and shared again still reports the original
|
|
6515
|
+
* date. There is no column recording when `scope` last became `"public"`,
|
|
6516
|
+
* so the UI says "first shared {time}" rather than claiming an exposure
|
|
6517
|
+
* window the data cannot support. Adding that column is the follow-up.
|
|
6518
|
+
*/
|
|
6519
|
+
createdAt: z.string()
|
|
6520
|
+
});
|
|
6498
6521
|
z.object({
|
|
6499
6522
|
parentId: z.string().nullable().optional().describe("Node to start from. Omit or null to start from the space root."),
|
|
6500
6523
|
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."),
|
|
@@ -6877,6 +6900,36 @@ z.object({
|
|
|
6877
6900
|
*/
|
|
6878
6901
|
contentTruncated: z.boolean().default(false)
|
|
6879
6902
|
});
|
|
6903
|
+
z.discriminatedUnion("event", [
|
|
6904
|
+
z.object({
|
|
6905
|
+
event: z.literal("result_click"),
|
|
6906
|
+
sessionId: z.string().uuid(),
|
|
6907
|
+
surface: z.enum(["quick", "advanced"]),
|
|
6908
|
+
position: z.number().int().min(1).max(100),
|
|
6909
|
+
resultKind: z.enum([
|
|
6910
|
+
"record",
|
|
6911
|
+
"change_request",
|
|
6912
|
+
"base",
|
|
6913
|
+
"file",
|
|
6914
|
+
"node"
|
|
6915
|
+
])
|
|
6916
|
+
}).strict(),
|
|
6917
|
+
z.object({
|
|
6918
|
+
event: z.literal("quick_to_advanced"),
|
|
6919
|
+
sessionId: z.string().uuid(),
|
|
6920
|
+
surface: z.literal("quick"),
|
|
6921
|
+
resultCount: z.number().int().min(0).max(100)
|
|
6922
|
+
}).strict(),
|
|
6923
|
+
z.object({
|
|
6924
|
+
event: z.literal("results_shown"),
|
|
6925
|
+
sessionId: z.string().uuid(),
|
|
6926
|
+
surface: z.enum(["quick", "advanced"]),
|
|
6927
|
+
resultCount: z.number().int().min(0).max(100),
|
|
6928
|
+
durationMs: z.number().int().min(0).max(12e4),
|
|
6929
|
+
hasMore: z.boolean()
|
|
6930
|
+
}).strict()
|
|
6931
|
+
]);
|
|
6932
|
+
z.object({ accepted: z.literal(true) });
|
|
6880
6933
|
z.object({
|
|
6881
6934
|
kind: z.enum([
|
|
6882
6935
|
"change_request.created",
|
|
@@ -7101,6 +7154,8 @@ const SearchSortSchema = z.enum([
|
|
|
7101
7154
|
]);
|
|
7102
7155
|
z.object({
|
|
7103
7156
|
query: z.string().default("").describe("Full-text query. An empty string matches nothing."),
|
|
7157
|
+
mode: z.enum(["quick", "full"]).optional().default("full").describe("Search depth. `quick` skips live file-body scans for typeahead; `full` preserves complete search behavior."),
|
|
7158
|
+
surface: z.enum(["quick", "advanced"]).optional().describe("Optional first-party UI surface for aggregate quality metrics. API/CLI callers should omit it."),
|
|
7104
7159
|
limit: z.coerce.number().int().min(1).max(100).optional().default(20).describe("Results per page. Capped at 100; note the default is 20, not 50."),
|
|
7105
7160
|
offset: z.coerce.number().int().min(0).optional().default(0).describe("0-indexed skip count. This endpoint pages by offset, not by cursor."),
|
|
7106
7161
|
/**
|
|
@@ -7197,7 +7252,21 @@ z.object({
|
|
|
7197
7252
|
/** Cloud only: the server created this user's first Space during this exact request. */
|
|
7198
7253
|
createdSpace: z.boolean().optional(),
|
|
7199
7254
|
/** Cloud only: this auto-created Space still needs its idempotent starter initialization. */
|
|
7200
|
-
bootstrapRequired: z.boolean().optional()
|
|
7255
|
+
bootstrapRequired: z.boolean().optional(),
|
|
7256
|
+
/** Cloud only: effective permission ceiling of the credential used for this request. */
|
|
7257
|
+
credentialPermissionLevel: z.enum([
|
|
7258
|
+
"read",
|
|
7259
|
+
"changeRequest",
|
|
7260
|
+
"write",
|
|
7261
|
+
"manage"
|
|
7262
|
+
]).optional(),
|
|
7263
|
+
/** Cloud only: credential ceiling capped by the selected Space membership role. */
|
|
7264
|
+
effectivePermissionLevel: z.enum([
|
|
7265
|
+
"read",
|
|
7266
|
+
"changeRequest",
|
|
7267
|
+
"write",
|
|
7268
|
+
"manage"
|
|
7269
|
+
]).optional()
|
|
7201
7270
|
});
|
|
7202
7271
|
//#endregion
|
|
7203
7272
|
//#region ../../packages/busabase-contract/src/domains/base/contract/view-schemas.ts
|
|
@@ -11674,7 +11743,9 @@ const workspace = (level) => ({
|
|
|
11674
11743
|
*/
|
|
11675
11744
|
const PROCEDURE_PERMISSION_POLICY = {
|
|
11676
11745
|
"auth.verify": workspace("read"),
|
|
11746
|
+
"spaces.members": workspace("read"),
|
|
11677
11747
|
search: node("read"),
|
|
11748
|
+
"searchMetrics.report": node("read"),
|
|
11678
11749
|
grep: node("read"),
|
|
11679
11750
|
"nodes.list": node("read"),
|
|
11680
11751
|
"nodes.get": node("read"),
|
|
@@ -11747,6 +11818,7 @@ const PROCEDURE_PERMISSION_POLICY = {
|
|
|
11747
11818
|
"embedLinks.create": workspace("manage"),
|
|
11748
11819
|
"embedLinks.list": workspace("manage"),
|
|
11749
11820
|
"embedLinks.revoke": workspace("manage"),
|
|
11821
|
+
"nodes.share.list": workspace("manage"),
|
|
11750
11822
|
"nodes.listFavorites": node("read"),
|
|
11751
11823
|
"auditEvents.list": node("read"),
|
|
11752
11824
|
"activity.listPaged": node("read"),
|
|
@@ -11787,7 +11859,9 @@ const PROCEDURE_PERMISSION_POLICY = {
|
|
|
11787
11859
|
"agents.catalog": workspace("manage"),
|
|
11788
11860
|
"agents.connections.list": workspace("manage"),
|
|
11789
11861
|
"agents.disconnect": workspace("manage"),
|
|
11862
|
+
"agents.deleteHistory": workspace("manage"),
|
|
11790
11863
|
"agents.sessions.list": workspace("manage"),
|
|
11864
|
+
"agents.sessions.listPaged": workspace("manage"),
|
|
11791
11865
|
"agents.sessions.create": workspace("manage"),
|
|
11792
11866
|
"agents.sessions.prompt": workspace("manage"),
|
|
11793
11867
|
"agents.sessions.setConfigOption": workspace("manage"),
|
|
@@ -12632,10 +12706,22 @@ function buildBootstrapMarkdown(origin, ctx) {
|
|
|
12632
12706
|
const ownMachine = isCloud ? "" : " Everything runs and stays on the user's own machine.";
|
|
12633
12707
|
const step0 = isCloud ? `## Step 0 — Connect to Busabase Cloud
|
|
12634
12708
|
|
|
12709
|
+
### 0a. MCP first
|
|
12710
|
+
|
|
12711
|
+
Ask the Harness to discover and use an existing Busabase MCP integration before CLI. Use Call "list_integration_tools" with "providerKey: busabase-cloud-mcp" to find the Busabase integration, if the tool exists. Verify it with
|
|
12712
|
+
a read-only identity and Space check; reconnect it first if configured but disconnected, and do not
|
|
12713
|
+
assume tool names.
|
|
12714
|
+
${preselectedSpaceId ? `Target the Dashboard-selected Space \`${preselectedSpaceId}\` in that first check. If access
|
|
12715
|
+
fails, ask the user to switch or reconnect accounts; never choose or create another Space.` : `With no preselected Space, use the only result, ask when there are several, and accept a new
|
|
12716
|
+
personal Space when the identity has no memberships.`}
|
|
12717
|
+
If verification succeeds, skip **0b** and keep MCP for this run. Otherwise use the CLI fallback.
|
|
12718
|
+
|
|
12719
|
+
### 0b. CLI fallback
|
|
12720
|
+
|
|
12635
12721
|
${preselectedSpaceId ? `One move: sign in. The dashboard already chose the Space, so there is nothing to confirm
|
|
12636
12722
|
afterwards.` : `Two moves: sign in, then confirm the Space you landed in.
|
|
12637
12723
|
|
|
12638
|
-
|
|
12724
|
+
#### Sign in`}
|
|
12639
12725
|
|
|
12640
12726
|
Sign-in is two commands, one per conversational turn. The first returns immediately, so the user
|
|
12641
12727
|
actually receives the link even if your harness only delivers a turn's final message:
|
|
@@ -12675,14 +12761,14 @@ Two edge cases fall out of that same rule: an empty *existing* Space has no mark
|
|
|
12675
12761
|
connect-only, not a trigger to initialize; and a retry after interrupted initialization still has the
|
|
12676
12762
|
version-0 marker and safely resumes Step 2.
|
|
12677
12763
|
${preselectedSpaceId ? "" : `
|
|
12678
|
-
###
|
|
12764
|
+
### 0c. Confirm the target Space
|
|
12679
12765
|
|
|
12680
12766
|
Login never asks which Space to use: it takes the server default (the Space the user was most
|
|
12681
12767
|
recently active in) and lists the rest in \`availableSpaces\`. The user has not seen that choice yet.
|
|
12682
12768
|
|
|
12683
12769
|
Always tell the user which Space was selected (name + id). Then, by \`availableSpaces\` length:
|
|
12684
12770
|
|
|
12685
|
-
- **One** → nothing to choose. Continue with the \`bootstrapRequired\` branch from
|
|
12771
|
+
- **One** → nothing to choose. Continue with the \`bootstrapRequired\` branch from 0b.
|
|
12686
12772
|
- **More than one** → ask once, a lettered choice in the user's language: \`A\` keeps the current
|
|
12687
12773
|
Space, then one letter per remaining entry. Offer Spaces, never commands — switching is your job,
|
|
12688
12774
|
so never tell the user to run anything.
|
|
@@ -12690,13 +12776,18 @@ Always tell the user which Space was selected (name + id). Then, by \`availableS
|
|
|
12690
12776
|
> 📂 You're in **{space.name}** (the Space you used most recently). Keep it, or switch?
|
|
12691
12777
|
> **A.** Keep **{space.name}** **B.** {otherSpace1.name} **C.** {otherSpace2.name}
|
|
12692
12778
|
|
|
12693
|
-
On a switch, run both and re-decide the
|
|
12779
|
+
On a switch, run both and re-decide the 0b branch from the new \`bootstrapRequired\` — the old one
|
|
12694
12780
|
belonged to the previous Space:
|
|
12695
12781
|
|
|
12696
12782
|
\`\`\`bash
|
|
12697
12783
|
${BUSABASE_CLI} space use <id>
|
|
12698
12784
|
${BUSABASE_CLI} whoami --output json
|
|
12699
|
-
\`\`\``}
|
|
12785
|
+
\`\`\``}
|
|
12786
|
+
|
|
12787
|
+
On MCP, resolve the target Space from the verification result: ${preselectedSpaceId ? `it must match \`${preselectedSpaceId}\`.` : `use one result; ask the user when there are several.`} Then follow \`bootstrapRequired\`: \`false\` goes to Step 3 with no writes; \`true\` goes to
|
|
12788
|
+
Step 1. Before the first write, require write access plus capabilities to inspect, propose, read
|
|
12789
|
+
back, and complete bootstrap. If anything is missing, stop before writing; ask the user to reconnect
|
|
12790
|
+
with more access or approve CLI fallback. Keep the same transport for the rest of the run.` : `## Step 0 — Connect to Busabase Personal Desktop
|
|
12700
12791
|
|
|
12701
12792
|
Everything runs locally — no account, no API key. The user has already confirmed this edition, so
|
|
12702
12793
|
check whether the
|
|
@@ -12831,7 +12922,15 @@ until curl -fsS ${local}/api/v1/bases >/dev/null 2>&1; do sleep 2; done && echo
|
|
|
12831
12922
|
const spaceNote = isCloud ? "(Every call carries `x-busabase-space: $BUSABASE_SPACE_ID` — the space picked in Step 0. The API key is user-scoped, so do not omit the header.)" : "(No `spaceId` in local mode.)";
|
|
12832
12923
|
const step4 = `## Step ${isCloud ? 3 : 4} — The last setup step: install the permanent skills
|
|
12833
12924
|
|
|
12834
|
-
|
|
12925
|
+
${isCloud ? `Have the Harness install both skills into its persistent skill store. Prefer its native
|
|
12926
|
+
installer; otherwise run:
|
|
12927
|
+
|
|
12928
|
+
\`\`\`bash
|
|
12929
|
+
npx skills add busabase/skills --skill busabase busabase-app-creator
|
|
12930
|
+
\`\`\`
|
|
12931
|
+
|
|
12932
|
+
MCP remains the transport when it connected successfully; installing skills does not require CLI
|
|
12933
|
+
credentials. Finish only after both skills are available in future sessions.` : `One step left before everyday use. Install both permanent skills so any agent on this machine can
|
|
12835
12934
|
drive this workspace with **busabase** and create complete workspace apps with
|
|
12836
12935
|
**busabase-app-creator** every session — no re-pasting this onboarding doc:
|
|
12837
12936
|
|
|
@@ -12841,7 +12940,8 @@ npx skills add busabase/skills --skill busabase busabase-app-creator
|
|
|
12841
12940
|
|
|
12842
12941
|
The skills are self-describing. \`busabase\` reads \`~/.busabase/.env\` (set up above) and becomes
|
|
12843
12942
|
the reference for the everyday loop and full API; \`busabase-app-creator\` builds on it when the
|
|
12844
|
-
user wants a complete workspace app
|
|
12943
|
+
user wants a complete workspace app.`}
|
|
12944
|
+
This onboarding doc has now done its job.
|
|
12845
12945
|
|
|
12846
12946
|
## 🎉 You're set up — congratulations!
|
|
12847
12947
|
|
|
@@ -12858,12 +12958,14 @@ Tell them so, and close the journey with the tracker fully checked — e.g.:
|
|
|
12858
12958
|
|
|
12859
12959
|
> ✅ Connect · ✅ Initialize (new Spaces only) · ✅ Verify · ✅ Skills
|
|
12860
12960
|
> 🎉 *You're all set — Busabase is connected, your first workspace is live, and both skills are
|
|
12861
|
-
> installed.
|
|
12961
|
+
> installed.
|
|
12962
|
+
> From here it's everyday use: you ask, I write, and every change keeps a message, an
|
|
12862
12963
|
> author and a history you can undo — and I can build a complete workspace app when you need one.*
|
|
12863
12964
|
${isCloud ? `
|
|
12864
12965
|
Finish the congratulations with a clickable Markdown link labeled **Open Busabase Dashboard**.
|
|
12865
|
-
${preselectedSpaceId ? `Step 0 locked the Space ID used below;
|
|
12866
|
-
|
|
12966
|
+
${preselectedSpaceId ? `Step 0 locked the Space ID used below; verify whichever path you connected through confirmed
|
|
12967
|
+
that same Space before replying.` : `Replace \`{space_id}\` with the real selected Space ID — whichever path you connected
|
|
12968
|
+
through resolved and confirmed it — before replying.`}
|
|
12867
12969
|
Never show \`$BUSABASE_SPACE_ID\`, \`{space_id}\`, \`YOUR_SPACE_ID\`, or any other placeholder
|
|
12868
12970
|
literally to the user. The user's final line must be:
|
|
12869
12971
|
|
|
@@ -12891,10 +12993,10 @@ You are guiding the user through four milestones. Existing users skip initializa
|
|
|
12891
12993
|
|
|
12892
12994
|
| # | Milestone | Covered by | Done when |
|
|
12893
12995
|
| - | --------- | ---------- | --------- |
|
|
12894
|
-
| 1 | 🔌 **Connect** | ${isCloud ? "Step 0" : "Step 0 + Step 1"} | ${isCloud ? "device login returns the selected Space" : "the local API responds and the connection is saved"} |
|
|
12996
|
+
| 1 | 🔌 **Connect** | ${isCloud ? "Step 0" : "Step 0 + Step 1"} | ${isCloud ? "a verified MCP integration, or device login, returns the selected Space" : "the local API responds and the connection is saved"} |
|
|
12895
12997
|
| 2 | 🏗️ **Initialize if required** | ${isCloud ? "Step 1 + Step 2" : "Step 2 + Step 3"} | version-0 Space gets starter data |
|
|
12896
12998
|
| 3 | ✅ **Verify** | Step ${isCloud ? 2 : 3} | starter records read back and marker becomes version 1 |
|
|
12897
|
-
| 4 | 🎓 **Skills installed** | Step ${isCloud ? 3 : 4} | both skills
|
|
12999
|
+
| 4 | 🎓 **Skills installed** | Step ${isCloud ? 3 : 4} | both skills available in the agent's persistent skill store, 🎉 |
|
|
12898
13000
|
|
|
12899
13001
|
**Conduct rules — these create the step-by-step feel; follow them at every turn:**
|
|
12900
13002
|
|
|
@@ -12965,7 +13067,15 @@ types (\`auto_number\`, \`created_time\`, \`ai_summary\`, \`ai_tags\`, …).
|
|
|
12965
13067
|
This is the everyday write loop, not an exception to it: the user has write access to their own
|
|
12966
13068
|
new Space, so structure and sample rows land immediately. They already chose this starter — do not
|
|
12967
13069
|
ask them to approve it, and do not force a review by passing \`autoMerge: false\`. Every write carries system provenance and stable
|
|
12968
|
-
identity so retries cannot duplicate data.
|
|
13070
|
+
identity so retries cannot duplicate data.
|
|
13071
|
+
|
|
13072
|
+
${isCloud ? `**MCP path:** skip the shell examples and have the Harness run the same sequence: inspect,
|
|
13073
|
+
create missing structure/views, seed with stable idempotency keys, read back, then complete
|
|
13074
|
+
bootstrap. Require merged structure/records and \`materialized: true\` views; otherwise stop for
|
|
13075
|
+
approval and resume only after a canonical View read confirms materialization. The shell examples
|
|
13076
|
+
below are for CLI only.
|
|
13077
|
+
|
|
13078
|
+
` : ""}Briefly show what is being built:
|
|
12969
13079
|
|
|
12970
13080
|
\`\`\`txt
|
|
12971
13081
|
📁 CRM
|
|
@@ -13090,7 +13200,7 @@ curl -X POST "${api}/api/v1/onboarding/bootstrap-complete"${H}
|
|
|
13090
13200
|
\`\`\`
|
|
13091
13201
|
|
|
13092
13202
|
The completion call is idempotent and changes \`agentBootstrapVersion\` from 0 to 1 while preserving
|
|
13093
|
-
the rest of the Space onboarding state. Re-run \`whoami\`; \`bootstrapRequired\` must now be false
|
|
13203
|
+
the rest of the Space onboarding state. ${isCloud ? `Recheck through the active transport; \`bootstrapRequired\` must now be false.` : `Re-run \`whoami\`; \`bootstrapRequired\` must now be false.`}
|
|
13094
13204
|
|
|
13095
13205
|
### Custom blueprint (any scenario — this is what keeps it general)
|
|
13096
13206
|
|
|
@@ -13283,6 +13393,107 @@ async function resolveSkillDocument(topic, doc, runtime) {
|
|
|
13283
13393
|
source: "server"
|
|
13284
13394
|
} : bundled(live.reason ?? "the live document could not be read");
|
|
13285
13395
|
}
|
|
13396
|
+
/**
|
|
13397
|
+
* Read `name`/`description` out of a hosted SKILL.md's YAML frontmatter.
|
|
13398
|
+
*
|
|
13399
|
+
* Deliberately a small hand-rolled reader rather than a YAML dependency: the two fields
|
|
13400
|
+
* it needs are flat scalars, and the shapes that actually occur in the wild are plain,
|
|
13401
|
+
* quoted, and folded (`>-`) — the last of which every long Busabase description uses.
|
|
13402
|
+
* Anything stranger is better rejected loudly here than half-understood.
|
|
13403
|
+
*/
|
|
13404
|
+
function parseSkillIdentity(body) {
|
|
13405
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(body.trimStart());
|
|
13406
|
+
if (!match) throw new Error("the hosted SKILL.md has no YAML frontmatter");
|
|
13407
|
+
const lines = match[1].split(/\r?\n/);
|
|
13408
|
+
const read = (key) => {
|
|
13409
|
+
const start = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
13410
|
+
if (start === -1) return void 0;
|
|
13411
|
+
const inline = lines[start].slice(key.length + 1).trim();
|
|
13412
|
+
if (inline === "" || inline === ">" || inline === ">-" || inline === "|" || inline === "|-") {
|
|
13413
|
+
const rest = [];
|
|
13414
|
+
for (const line of lines.slice(start + 1)) {
|
|
13415
|
+
if (!/^\s/.test(line) || line.trim() === "") break;
|
|
13416
|
+
rest.push(line.trim());
|
|
13417
|
+
}
|
|
13418
|
+
return rest.join(" ");
|
|
13419
|
+
}
|
|
13420
|
+
return inline.replace(/^["']|["']$/g, "");
|
|
13421
|
+
};
|
|
13422
|
+
const name = read("name");
|
|
13423
|
+
const description = read("description");
|
|
13424
|
+
if (!name) throw new Error("the hosted SKILL.md frontmatter has no `name`");
|
|
13425
|
+
if (!description) throw new Error("the hosted SKILL.md frontmatter has no `description`; without it no agent would know when to fetch this skill, so a stub for it cannot work");
|
|
13426
|
+
return {
|
|
13427
|
+
name,
|
|
13428
|
+
description
|
|
13429
|
+
};
|
|
13430
|
+
}
|
|
13431
|
+
/**
|
|
13432
|
+
* Render the stub. Two details here are not stylistic:
|
|
13433
|
+
*
|
|
13434
|
+
* `--output json` — the default text output renders the body as a one-line preview
|
|
13435
|
+
* (a 40 KB skill comes back as ~400 bytes ending in an ellipsis), so a stub that
|
|
13436
|
+
* omitted it would hand the agent a truncated procedure and no error.
|
|
13437
|
+
*
|
|
13438
|
+
* `@latest` — `npx` will reuse whatever copy of the CLI it already cached, which may
|
|
13439
|
+
* predate the subcommand entirely.
|
|
13440
|
+
*
|
|
13441
|
+
* The fetch-first wording is equally deliberate. A stub whose fetch fails must stop,
|
|
13442
|
+
* not improvise from the one-line description; that failure mode is the whole reason
|
|
13443
|
+
* the body is centralized in the first place.
|
|
13444
|
+
*/
|
|
13445
|
+
function buildPointerStub(input) {
|
|
13446
|
+
const space = input.spaceName ? `${input.spaceName}${input.spaceId ? ` \`${input.spaceId}\`` : ""}` : input.spaceId ?? "—";
|
|
13447
|
+
return `---
|
|
13448
|
+
name: ${input.name}
|
|
13449
|
+
description: ${JSON.stringify(input.description)}
|
|
13450
|
+
---
|
|
13451
|
+
|
|
13452
|
+
The body of this skill lives in Busabase, in the same folder as the resources it
|
|
13453
|
+
works with, so it moves when they move. **Fetch it before doing anything:**
|
|
13454
|
+
|
|
13455
|
+
\`\`\`bash
|
|
13456
|
+
npx busabase-cli@latest skills read-file --node-id ${input.nodeId} \\
|
|
13457
|
+
--file-path SKILL.md --output json
|
|
13458
|
+
\`\`\`
|
|
13459
|
+
|
|
13460
|
+
\`--output json\` is required — the default text output truncates the body to a
|
|
13461
|
+
one-line preview. \`@latest\` is required too: \`npx\` will otherwise reuse a cached
|
|
13462
|
+
older CLI that may not have this subcommand.
|
|
13463
|
+
|
|
13464
|
+
If the fetch fails — no access, the node moved, the network is down — **stop and say
|
|
13465
|
+
so.** Do not improvise the procedure from the description above; the body is hosted in
|
|
13466
|
+
one place precisely so that every repository runs the same one.
|
|
13467
|
+
|
|
13468
|
+
| What | Where |
|
|
13469
|
+
| --- | --- |
|
|
13470
|
+
| Space | ${space} |
|
|
13471
|
+
| Skill node | \`${input.name}\` — \`${input.nodeId}\` |
|
|
13472
|
+
`;
|
|
13473
|
+
}
|
|
13474
|
+
/**
|
|
13475
|
+
* Write `<dir>/<name>/SKILL.md`. Same no-clobber rule as `installSkillDoc`, for the
|
|
13476
|
+
* same reason: the file already there may be a real skill someone wrote, and replacing
|
|
13477
|
+
* a body with a pointer to a different one is not a refresh, it is a swap.
|
|
13478
|
+
*/
|
|
13479
|
+
function linkSkillDoc(input) {
|
|
13480
|
+
const exists = input.exists ?? existsSync;
|
|
13481
|
+
const write = input.write ?? ((path, body) => {
|
|
13482
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
13483
|
+
writeFileSync(path, body, "utf8");
|
|
13484
|
+
});
|
|
13485
|
+
const path = join(input.dir, input.name, "SKILL.md");
|
|
13486
|
+
if (exists(path) && !input.force) return {
|
|
13487
|
+
path,
|
|
13488
|
+
written: false,
|
|
13489
|
+
reason: `${input.name} already exists there; pass --force to replace it`
|
|
13490
|
+
};
|
|
13491
|
+
write(path, content(input.content));
|
|
13492
|
+
return {
|
|
13493
|
+
path,
|
|
13494
|
+
written: true
|
|
13495
|
+
};
|
|
13496
|
+
}
|
|
13286
13497
|
//#endregion
|
|
13287
13498
|
//#region src/task-command.ts
|
|
13288
13499
|
const collect = (value, previous = []) => [...previous, value];
|
|
@@ -13868,24 +14079,6 @@ async function putTextCommand(client, opts) {
|
|
|
13868
14079
|
});
|
|
13869
14080
|
}
|
|
13870
14081
|
/** Wrap a leaf-command handler: resolve config, build the client, render the result. */
|
|
13871
|
-
/**
|
|
13872
|
-
* Is this a 404 — either "this server has no such route" or "no such node"?
|
|
13873
|
-
*
|
|
13874
|
-
* Duck-typed rather than an `instanceof ORPCError` check, because this package
|
|
13875
|
-
* deliberately depends on no `@orpc/*` package; the SDK builds the error and
|
|
13876
|
-
* only its `status` is needed here.
|
|
13877
|
-
*
|
|
13878
|
-
* Not distinguishing the two 404s is safe for the one thing this gates: a
|
|
13879
|
-
* version fallback whose other branch targets the SAME node. An old server
|
|
13880
|
-
* answers 404 because the route is missing and the fallback then succeeds; a
|
|
13881
|
-
* current server answers 404 because the node does not exist, the fallback
|
|
13882
|
-
* asks about the same missing node and 404s too, and the caller still sees a
|
|
13883
|
-
* "not found" error. The cost of guessing wrong is one extra request, not a
|
|
13884
|
-
* write landing somewhere unintended.
|
|
13885
|
-
*/
|
|
13886
|
-
function isNotFound(error) {
|
|
13887
|
-
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
13888
|
-
}
|
|
13889
14082
|
function runAction(state, handler) {
|
|
13890
14083
|
return async (_opts, cmd) => {
|
|
13891
14084
|
const opts = cmd.optsWithGlobals();
|
|
@@ -14628,6 +14821,64 @@ Default target when neither is present: ${skillsDirDefaultHint()}`).action((dir,
|
|
|
14628
14821
|
console.log(` ${result.path}`);
|
|
14629
14822
|
console.error(`\n[busabase-cli] this copy is bound to this host. For the canonical pair, run:\n ${SKILL_REGISTRY_HINT}`);
|
|
14630
14823
|
});
|
|
14824
|
+
addGlobalFlags(skill.command("link")).argument("[dir]", `skills directory; default: ./.agents/skills, else ./.claude/skills`).description("Point this repo at a Skill node hosted in a workspace, without copying it").requiredOption("--node-id <id>", "the Skill node whose body stays canonical").addOption(new Option("--dir <path>", "same as the positional argument")).addOption(new Option("--force", "replace an existing skill of the same name")).addHelpText("after", `
|
|
14825
|
+
\`install\` writes a COPY. \`link\` writes a POINTER: a short SKILL.md holding only the
|
|
14826
|
+
hosted skill's own name and description plus the command to fetch its body, so the
|
|
14827
|
+
body itself never lands on disk here.
|
|
14828
|
+
|
|
14829
|
+
Use it when one skill has to serve several repositories. Vendoring a copy into each
|
|
14830
|
+
one means copies that drift; a pointer means every repository reads the one body, and
|
|
14831
|
+
an edit to it — reviewed once, through a Change Request — is live for all of them at
|
|
14832
|
+
the same moment.
|
|
14833
|
+
|
|
14834
|
+
busabase-cli skill link --node-id nodmtv5rwuofuhaqwl
|
|
14835
|
+
|
|
14836
|
+
Writes <dir>/<the skill's own name>/SKILL.md. The name comes from the hosted
|
|
14837
|
+
frontmatter, not from you, because that name is how an agent resolves "read the
|
|
14838
|
+
crm-visits skill in this folder".
|
|
14839
|
+
|
|
14840
|
+
A pointer names a specific node in a specific space, so: everyone who uses it needs
|
|
14841
|
+
read access to that space, and it must never be published as a template — a template
|
|
14842
|
+
materializes fresh resources in the installer's space, while these ids name yours.
|
|
14843
|
+
Default target when neither is present: ${skillsDirDefaultHint()}`).action(async (dir, _opts, cmd) => {
|
|
14844
|
+
const opts = cmd.optsWithGlobals();
|
|
14845
|
+
const config = resolveConfig(opts);
|
|
14846
|
+
state.config = config;
|
|
14847
|
+
const nodeId = String(opts.nodeId);
|
|
14848
|
+
const file = await createBusabaseClient(config).fileTrees.readFile({
|
|
14849
|
+
nodeId,
|
|
14850
|
+
type: "skill",
|
|
14851
|
+
filePath: "SKILL.md"
|
|
14852
|
+
});
|
|
14853
|
+
const identity = parseSkillIdentity(String(file.content ?? ""));
|
|
14854
|
+
const result = linkSkillDoc({
|
|
14855
|
+
dir: resolveSkillsDir({
|
|
14856
|
+
explicit: dir ?? opts.dir,
|
|
14857
|
+
cwd: process.cwd(),
|
|
14858
|
+
home: homedir(),
|
|
14859
|
+
exists: existsSync
|
|
14860
|
+
}),
|
|
14861
|
+
name: identity.name,
|
|
14862
|
+
content: buildPointerStub({
|
|
14863
|
+
...identity,
|
|
14864
|
+
nodeId,
|
|
14865
|
+
spaceId: config.spaceId
|
|
14866
|
+
}),
|
|
14867
|
+
force: Boolean(opts.force)
|
|
14868
|
+
});
|
|
14869
|
+
if (!result.written) throw new CliOutcomeError("CONFLICT", `Not linked: ${result.path} — ${result.reason}`);
|
|
14870
|
+
if (config.output === "json") {
|
|
14871
|
+
console.log(render({
|
|
14872
|
+
path: result.path,
|
|
14873
|
+
name: identity.name,
|
|
14874
|
+
nodeId
|
|
14875
|
+
}, "json"));
|
|
14876
|
+
return;
|
|
14877
|
+
}
|
|
14878
|
+
console.log(`Linked the \`${identity.name}\` skill to node ${nodeId} at:`);
|
|
14879
|
+
console.log(` ${result.path}`);
|
|
14880
|
+
console.error("\n[busabase-cli] this is a pointer, not a copy — the body stays in the workspace and is fetched on use. Everyone using it needs read access to that space.");
|
|
14881
|
+
});
|
|
14631
14882
|
addGlobalFlags(program.command("setup-skill")).description("Alias for `skill setup` — the onboarding document").addOption(modeOption()).addOption(withKeyOption()).addOption(new Option("--offline", "skip the server and print the bundled copy")).action(async (_opts, cmd) => {
|
|
14632
14883
|
const opts = cmd.optsWithGlobals();
|
|
14633
14884
|
const config = resolveConfig(opts);
|
|
@@ -14911,18 +15162,10 @@ Examples:
|
|
|
14911
15162
|
throw new CliOutcomeError("VALIDATION", `Invalid agent prompts in ${filePath}${detail ? ` — ${detail}` : ""}`);
|
|
14912
15163
|
}
|
|
14913
15164
|
const nodeId = opts.nodeId;
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
});
|
|
14919
|
-
} catch (error) {
|
|
14920
|
-
if (!isNotFound(error)) throw error;
|
|
14921
|
-
return await client.nodes.updateMetadata({
|
|
14922
|
-
nodeId,
|
|
14923
|
-
metadata: { agentPrompts: parsed.data }
|
|
14924
|
-
});
|
|
14925
|
-
}
|
|
15165
|
+
return await client.nodes.updateAgentPrompts({
|
|
15166
|
+
nodeId,
|
|
15167
|
+
agentPrompts: parsed.data
|
|
15168
|
+
});
|
|
14926
15169
|
}));
|
|
14927
15170
|
const bases = program.command("bases").description("Bases (structured tables)");
|
|
14928
15171
|
addGlobalFlags(bases.command("list")).description("List Bases in the active space").addOption(new Option("--status <active|archived>", "live Bases (default) or archived ones").choices(["active", "archived"])).action(runAction(state, (client, opts) => client.bases.list({ status: opts.status ?? "active" })));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.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.71.0",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"qrcode": "^1.5.4",
|
|
39
39
|
"tar-stream": "^3.1.7",
|