busa-cli 0.60.0 → 0.70.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-CTgZmlyi.js} +293 -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-CTgZmlyi.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-CTgZmlyi.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."),
|
|
@@ -6877,6 +6881,36 @@ z.object({
|
|
|
6877
6881
|
*/
|
|
6878
6882
|
contentTruncated: z.boolean().default(false)
|
|
6879
6883
|
});
|
|
6884
|
+
z.discriminatedUnion("event", [
|
|
6885
|
+
z.object({
|
|
6886
|
+
event: z.literal("result_click"),
|
|
6887
|
+
sessionId: z.string().uuid(),
|
|
6888
|
+
surface: z.enum(["quick", "advanced"]),
|
|
6889
|
+
position: z.number().int().min(1).max(100),
|
|
6890
|
+
resultKind: z.enum([
|
|
6891
|
+
"record",
|
|
6892
|
+
"change_request",
|
|
6893
|
+
"base",
|
|
6894
|
+
"file",
|
|
6895
|
+
"node"
|
|
6896
|
+
])
|
|
6897
|
+
}).strict(),
|
|
6898
|
+
z.object({
|
|
6899
|
+
event: z.literal("quick_to_advanced"),
|
|
6900
|
+
sessionId: z.string().uuid(),
|
|
6901
|
+
surface: z.literal("quick"),
|
|
6902
|
+
resultCount: z.number().int().min(0).max(100)
|
|
6903
|
+
}).strict(),
|
|
6904
|
+
z.object({
|
|
6905
|
+
event: z.literal("results_shown"),
|
|
6906
|
+
sessionId: z.string().uuid(),
|
|
6907
|
+
surface: z.enum(["quick", "advanced"]),
|
|
6908
|
+
resultCount: z.number().int().min(0).max(100),
|
|
6909
|
+
durationMs: z.number().int().min(0).max(12e4),
|
|
6910
|
+
hasMore: z.boolean()
|
|
6911
|
+
}).strict()
|
|
6912
|
+
]);
|
|
6913
|
+
z.object({ accepted: z.literal(true) });
|
|
6880
6914
|
z.object({
|
|
6881
6915
|
kind: z.enum([
|
|
6882
6916
|
"change_request.created",
|
|
@@ -7101,6 +7135,8 @@ const SearchSortSchema = z.enum([
|
|
|
7101
7135
|
]);
|
|
7102
7136
|
z.object({
|
|
7103
7137
|
query: z.string().default("").describe("Full-text query. An empty string matches nothing."),
|
|
7138
|
+
mode: z.enum(["quick", "full"]).optional().default("full").describe("Search depth. `quick` skips live file-body scans for typeahead; `full` preserves complete search behavior."),
|
|
7139
|
+
surface: z.enum(["quick", "advanced"]).optional().describe("Optional first-party UI surface for aggregate quality metrics. API/CLI callers should omit it."),
|
|
7104
7140
|
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
7141
|
offset: z.coerce.number().int().min(0).optional().default(0).describe("0-indexed skip count. This endpoint pages by offset, not by cursor."),
|
|
7106
7142
|
/**
|
|
@@ -7197,7 +7233,21 @@ z.object({
|
|
|
7197
7233
|
/** Cloud only: the server created this user's first Space during this exact request. */
|
|
7198
7234
|
createdSpace: z.boolean().optional(),
|
|
7199
7235
|
/** Cloud only: this auto-created Space still needs its idempotent starter initialization. */
|
|
7200
|
-
bootstrapRequired: z.boolean().optional()
|
|
7236
|
+
bootstrapRequired: z.boolean().optional(),
|
|
7237
|
+
/** Cloud only: effective permission ceiling of the credential used for this request. */
|
|
7238
|
+
credentialPermissionLevel: z.enum([
|
|
7239
|
+
"read",
|
|
7240
|
+
"changeRequest",
|
|
7241
|
+
"write",
|
|
7242
|
+
"manage"
|
|
7243
|
+
]).optional(),
|
|
7244
|
+
/** Cloud only: credential ceiling capped by the selected Space membership role. */
|
|
7245
|
+
effectivePermissionLevel: z.enum([
|
|
7246
|
+
"read",
|
|
7247
|
+
"changeRequest",
|
|
7248
|
+
"write",
|
|
7249
|
+
"manage"
|
|
7250
|
+
]).optional()
|
|
7201
7251
|
});
|
|
7202
7252
|
//#endregion
|
|
7203
7253
|
//#region ../../packages/busabase-contract/src/domains/base/contract/view-schemas.ts
|
|
@@ -11674,7 +11724,9 @@ const workspace = (level) => ({
|
|
|
11674
11724
|
*/
|
|
11675
11725
|
const PROCEDURE_PERMISSION_POLICY = {
|
|
11676
11726
|
"auth.verify": workspace("read"),
|
|
11727
|
+
"spaces.members": workspace("read"),
|
|
11677
11728
|
search: node("read"),
|
|
11729
|
+
"searchMetrics.report": node("read"),
|
|
11678
11730
|
grep: node("read"),
|
|
11679
11731
|
"nodes.list": node("read"),
|
|
11680
11732
|
"nodes.get": node("read"),
|
|
@@ -11787,7 +11839,9 @@ const PROCEDURE_PERMISSION_POLICY = {
|
|
|
11787
11839
|
"agents.catalog": workspace("manage"),
|
|
11788
11840
|
"agents.connections.list": workspace("manage"),
|
|
11789
11841
|
"agents.disconnect": workspace("manage"),
|
|
11842
|
+
"agents.deleteHistory": workspace("manage"),
|
|
11790
11843
|
"agents.sessions.list": workspace("manage"),
|
|
11844
|
+
"agents.sessions.listPaged": workspace("manage"),
|
|
11791
11845
|
"agents.sessions.create": workspace("manage"),
|
|
11792
11846
|
"agents.sessions.prompt": workspace("manage"),
|
|
11793
11847
|
"agents.sessions.setConfigOption": workspace("manage"),
|
|
@@ -12632,10 +12686,22 @@ function buildBootstrapMarkdown(origin, ctx) {
|
|
|
12632
12686
|
const ownMachine = isCloud ? "" : " Everything runs and stays on the user's own machine.";
|
|
12633
12687
|
const step0 = isCloud ? `## Step 0 — Connect to Busabase Cloud
|
|
12634
12688
|
|
|
12689
|
+
### 0a. MCP first
|
|
12690
|
+
|
|
12691
|
+
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
|
|
12692
|
+
a read-only identity and Space check; reconnect it first if configured but disconnected, and do not
|
|
12693
|
+
assume tool names.
|
|
12694
|
+
${preselectedSpaceId ? `Target the Dashboard-selected Space \`${preselectedSpaceId}\` in that first check. If access
|
|
12695
|
+
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
|
|
12696
|
+
personal Space when the identity has no memberships.`}
|
|
12697
|
+
If verification succeeds, skip **0b** and keep MCP for this run. Otherwise use the CLI fallback.
|
|
12698
|
+
|
|
12699
|
+
### 0b. CLI fallback
|
|
12700
|
+
|
|
12635
12701
|
${preselectedSpaceId ? `One move: sign in. The dashboard already chose the Space, so there is nothing to confirm
|
|
12636
12702
|
afterwards.` : `Two moves: sign in, then confirm the Space you landed in.
|
|
12637
12703
|
|
|
12638
|
-
|
|
12704
|
+
#### Sign in`}
|
|
12639
12705
|
|
|
12640
12706
|
Sign-in is two commands, one per conversational turn. The first returns immediately, so the user
|
|
12641
12707
|
actually receives the link even if your harness only delivers a turn's final message:
|
|
@@ -12675,14 +12741,14 @@ Two edge cases fall out of that same rule: an empty *existing* Space has no mark
|
|
|
12675
12741
|
connect-only, not a trigger to initialize; and a retry after interrupted initialization still has the
|
|
12676
12742
|
version-0 marker and safely resumes Step 2.
|
|
12677
12743
|
${preselectedSpaceId ? "" : `
|
|
12678
|
-
###
|
|
12744
|
+
### 0c. Confirm the target Space
|
|
12679
12745
|
|
|
12680
12746
|
Login never asks which Space to use: it takes the server default (the Space the user was most
|
|
12681
12747
|
recently active in) and lists the rest in \`availableSpaces\`. The user has not seen that choice yet.
|
|
12682
12748
|
|
|
12683
12749
|
Always tell the user which Space was selected (name + id). Then, by \`availableSpaces\` length:
|
|
12684
12750
|
|
|
12685
|
-
- **One** → nothing to choose. Continue with the \`bootstrapRequired\` branch from
|
|
12751
|
+
- **One** → nothing to choose. Continue with the \`bootstrapRequired\` branch from 0b.
|
|
12686
12752
|
- **More than one** → ask once, a lettered choice in the user's language: \`A\` keeps the current
|
|
12687
12753
|
Space, then one letter per remaining entry. Offer Spaces, never commands — switching is your job,
|
|
12688
12754
|
so never tell the user to run anything.
|
|
@@ -12690,13 +12756,18 @@ Always tell the user which Space was selected (name + id). Then, by \`availableS
|
|
|
12690
12756
|
> 📂 You're in **{space.name}** (the Space you used most recently). Keep it, or switch?
|
|
12691
12757
|
> **A.** Keep **{space.name}** **B.** {otherSpace1.name} **C.** {otherSpace2.name}
|
|
12692
12758
|
|
|
12693
|
-
On a switch, run both and re-decide the
|
|
12759
|
+
On a switch, run both and re-decide the 0b branch from the new \`bootstrapRequired\` — the old one
|
|
12694
12760
|
belonged to the previous Space:
|
|
12695
12761
|
|
|
12696
12762
|
\`\`\`bash
|
|
12697
12763
|
${BUSABASE_CLI} space use <id>
|
|
12698
12764
|
${BUSABASE_CLI} whoami --output json
|
|
12699
|
-
\`\`\``}
|
|
12765
|
+
\`\`\``}
|
|
12766
|
+
|
|
12767
|
+
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
|
|
12768
|
+
Step 1. Before the first write, require write access plus capabilities to inspect, propose, read
|
|
12769
|
+
back, and complete bootstrap. If anything is missing, stop before writing; ask the user to reconnect
|
|
12770
|
+
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
12771
|
|
|
12701
12772
|
Everything runs locally — no account, no API key. The user has already confirmed this edition, so
|
|
12702
12773
|
check whether the
|
|
@@ -12831,7 +12902,15 @@ until curl -fsS ${local}/api/v1/bases >/dev/null 2>&1; do sleep 2; done && echo
|
|
|
12831
12902
|
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
12903
|
const step4 = `## Step ${isCloud ? 3 : 4} — The last setup step: install the permanent skills
|
|
12833
12904
|
|
|
12834
|
-
|
|
12905
|
+
${isCloud ? `Have the Harness install both skills into its persistent skill store. Prefer its native
|
|
12906
|
+
installer; otherwise run:
|
|
12907
|
+
|
|
12908
|
+
\`\`\`bash
|
|
12909
|
+
npx skills add busabase/skills --skill busabase busabase-app-creator
|
|
12910
|
+
\`\`\`
|
|
12911
|
+
|
|
12912
|
+
MCP remains the transport when it connected successfully; installing skills does not require CLI
|
|
12913
|
+
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
12914
|
drive this workspace with **busabase** and create complete workspace apps with
|
|
12836
12915
|
**busabase-app-creator** every session — no re-pasting this onboarding doc:
|
|
12837
12916
|
|
|
@@ -12841,7 +12920,8 @@ npx skills add busabase/skills --skill busabase busabase-app-creator
|
|
|
12841
12920
|
|
|
12842
12921
|
The skills are self-describing. \`busabase\` reads \`~/.busabase/.env\` (set up above) and becomes
|
|
12843
12922
|
the reference for the everyday loop and full API; \`busabase-app-creator\` builds on it when the
|
|
12844
|
-
user wants a complete workspace app
|
|
12923
|
+
user wants a complete workspace app.`}
|
|
12924
|
+
This onboarding doc has now done its job.
|
|
12845
12925
|
|
|
12846
12926
|
## 🎉 You're set up — congratulations!
|
|
12847
12927
|
|
|
@@ -12858,12 +12938,14 @@ Tell them so, and close the journey with the tracker fully checked — e.g.:
|
|
|
12858
12938
|
|
|
12859
12939
|
> ✅ Connect · ✅ Initialize (new Spaces only) · ✅ Verify · ✅ Skills
|
|
12860
12940
|
> 🎉 *You're all set — Busabase is connected, your first workspace is live, and both skills are
|
|
12861
|
-
> installed.
|
|
12941
|
+
> installed.
|
|
12942
|
+
> From here it's everyday use: you ask, I write, and every change keeps a message, an
|
|
12862
12943
|
> author and a history you can undo — and I can build a complete workspace app when you need one.*
|
|
12863
12944
|
${isCloud ? `
|
|
12864
12945
|
Finish the congratulations with a clickable Markdown link labeled **Open Busabase Dashboard**.
|
|
12865
|
-
${preselectedSpaceId ? `Step 0 locked the Space ID used below;
|
|
12866
|
-
|
|
12946
|
+
${preselectedSpaceId ? `Step 0 locked the Space ID used below; verify whichever path you connected through confirmed
|
|
12947
|
+
that same Space before replying.` : `Replace \`{space_id}\` with the real selected Space ID — whichever path you connected
|
|
12948
|
+
through resolved and confirmed it — before replying.`}
|
|
12867
12949
|
Never show \`$BUSABASE_SPACE_ID\`, \`{space_id}\`, \`YOUR_SPACE_ID\`, or any other placeholder
|
|
12868
12950
|
literally to the user. The user's final line must be:
|
|
12869
12951
|
|
|
@@ -12891,10 +12973,10 @@ You are guiding the user through four milestones. Existing users skip initializa
|
|
|
12891
12973
|
|
|
12892
12974
|
| # | Milestone | Covered by | Done when |
|
|
12893
12975
|
| - | --------- | ---------- | --------- |
|
|
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"} |
|
|
12976
|
+
| 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
12977
|
| 2 | 🏗️ **Initialize if required** | ${isCloud ? "Step 1 + Step 2" : "Step 2 + Step 3"} | version-0 Space gets starter data |
|
|
12896
12978
|
| 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
|
|
12979
|
+
| 4 | 🎓 **Skills installed** | Step ${isCloud ? 3 : 4} | both skills available in the agent's persistent skill store, 🎉 |
|
|
12898
12980
|
|
|
12899
12981
|
**Conduct rules — these create the step-by-step feel; follow them at every turn:**
|
|
12900
12982
|
|
|
@@ -12965,7 +13047,15 @@ types (\`auto_number\`, \`created_time\`, \`ai_summary\`, \`ai_tags\`, …).
|
|
|
12965
13047
|
This is the everyday write loop, not an exception to it: the user has write access to their own
|
|
12966
13048
|
new Space, so structure and sample rows land immediately. They already chose this starter — do not
|
|
12967
13049
|
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.
|
|
13050
|
+
identity so retries cannot duplicate data.
|
|
13051
|
+
|
|
13052
|
+
${isCloud ? `**MCP path:** skip the shell examples and have the Harness run the same sequence: inspect,
|
|
13053
|
+
create missing structure/views, seed with stable idempotency keys, read back, then complete
|
|
13054
|
+
bootstrap. Require merged structure/records and \`materialized: true\` views; otherwise stop for
|
|
13055
|
+
approval and resume only after a canonical View read confirms materialization. The shell examples
|
|
13056
|
+
below are for CLI only.
|
|
13057
|
+
|
|
13058
|
+
` : ""}Briefly show what is being built:
|
|
12969
13059
|
|
|
12970
13060
|
\`\`\`txt
|
|
12971
13061
|
📁 CRM
|
|
@@ -13090,7 +13180,7 @@ curl -X POST "${api}/api/v1/onboarding/bootstrap-complete"${H}
|
|
|
13090
13180
|
\`\`\`
|
|
13091
13181
|
|
|
13092
13182
|
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
|
|
13183
|
+
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
13184
|
|
|
13095
13185
|
### Custom blueprint (any scenario — this is what keeps it general)
|
|
13096
13186
|
|
|
@@ -13283,6 +13373,107 @@ async function resolveSkillDocument(topic, doc, runtime) {
|
|
|
13283
13373
|
source: "server"
|
|
13284
13374
|
} : bundled(live.reason ?? "the live document could not be read");
|
|
13285
13375
|
}
|
|
13376
|
+
/**
|
|
13377
|
+
* Read `name`/`description` out of a hosted SKILL.md's YAML frontmatter.
|
|
13378
|
+
*
|
|
13379
|
+
* Deliberately a small hand-rolled reader rather than a YAML dependency: the two fields
|
|
13380
|
+
* it needs are flat scalars, and the shapes that actually occur in the wild are plain,
|
|
13381
|
+
* quoted, and folded (`>-`) — the last of which every long Busabase description uses.
|
|
13382
|
+
* Anything stranger is better rejected loudly here than half-understood.
|
|
13383
|
+
*/
|
|
13384
|
+
function parseSkillIdentity(body) {
|
|
13385
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(body.trimStart());
|
|
13386
|
+
if (!match) throw new Error("the hosted SKILL.md has no YAML frontmatter");
|
|
13387
|
+
const lines = match[1].split(/\r?\n/);
|
|
13388
|
+
const read = (key) => {
|
|
13389
|
+
const start = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
13390
|
+
if (start === -1) return void 0;
|
|
13391
|
+
const inline = lines[start].slice(key.length + 1).trim();
|
|
13392
|
+
if (inline === "" || inline === ">" || inline === ">-" || inline === "|" || inline === "|-") {
|
|
13393
|
+
const rest = [];
|
|
13394
|
+
for (const line of lines.slice(start + 1)) {
|
|
13395
|
+
if (!/^\s/.test(line) || line.trim() === "") break;
|
|
13396
|
+
rest.push(line.trim());
|
|
13397
|
+
}
|
|
13398
|
+
return rest.join(" ");
|
|
13399
|
+
}
|
|
13400
|
+
return inline.replace(/^["']|["']$/g, "");
|
|
13401
|
+
};
|
|
13402
|
+
const name = read("name");
|
|
13403
|
+
const description = read("description");
|
|
13404
|
+
if (!name) throw new Error("the hosted SKILL.md frontmatter has no `name`");
|
|
13405
|
+
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");
|
|
13406
|
+
return {
|
|
13407
|
+
name,
|
|
13408
|
+
description
|
|
13409
|
+
};
|
|
13410
|
+
}
|
|
13411
|
+
/**
|
|
13412
|
+
* Render the stub. Two details here are not stylistic:
|
|
13413
|
+
*
|
|
13414
|
+
* `--output json` — the default text output renders the body as a one-line preview
|
|
13415
|
+
* (a 40 KB skill comes back as ~400 bytes ending in an ellipsis), so a stub that
|
|
13416
|
+
* omitted it would hand the agent a truncated procedure and no error.
|
|
13417
|
+
*
|
|
13418
|
+
* `@latest` — `npx` will reuse whatever copy of the CLI it already cached, which may
|
|
13419
|
+
* predate the subcommand entirely.
|
|
13420
|
+
*
|
|
13421
|
+
* The fetch-first wording is equally deliberate. A stub whose fetch fails must stop,
|
|
13422
|
+
* not improvise from the one-line description; that failure mode is the whole reason
|
|
13423
|
+
* the body is centralized in the first place.
|
|
13424
|
+
*/
|
|
13425
|
+
function buildPointerStub(input) {
|
|
13426
|
+
const space = input.spaceName ? `${input.spaceName}${input.spaceId ? ` \`${input.spaceId}\`` : ""}` : input.spaceId ?? "—";
|
|
13427
|
+
return `---
|
|
13428
|
+
name: ${input.name}
|
|
13429
|
+
description: ${JSON.stringify(input.description)}
|
|
13430
|
+
---
|
|
13431
|
+
|
|
13432
|
+
The body of this skill lives in Busabase, in the same folder as the resources it
|
|
13433
|
+
works with, so it moves when they move. **Fetch it before doing anything:**
|
|
13434
|
+
|
|
13435
|
+
\`\`\`bash
|
|
13436
|
+
npx busabase-cli@latest skills read-file --node-id ${input.nodeId} \\
|
|
13437
|
+
--file-path SKILL.md --output json
|
|
13438
|
+
\`\`\`
|
|
13439
|
+
|
|
13440
|
+
\`--output json\` is required — the default text output truncates the body to a
|
|
13441
|
+
one-line preview. \`@latest\` is required too: \`npx\` will otherwise reuse a cached
|
|
13442
|
+
older CLI that may not have this subcommand.
|
|
13443
|
+
|
|
13444
|
+
If the fetch fails — no access, the node moved, the network is down — **stop and say
|
|
13445
|
+
so.** Do not improvise the procedure from the description above; the body is hosted in
|
|
13446
|
+
one place precisely so that every repository runs the same one.
|
|
13447
|
+
|
|
13448
|
+
| What | Where |
|
|
13449
|
+
| --- | --- |
|
|
13450
|
+
| Space | ${space} |
|
|
13451
|
+
| Skill node | \`${input.name}\` — \`${input.nodeId}\` |
|
|
13452
|
+
`;
|
|
13453
|
+
}
|
|
13454
|
+
/**
|
|
13455
|
+
* Write `<dir>/<name>/SKILL.md`. Same no-clobber rule as `installSkillDoc`, for the
|
|
13456
|
+
* same reason: the file already there may be a real skill someone wrote, and replacing
|
|
13457
|
+
* a body with a pointer to a different one is not a refresh, it is a swap.
|
|
13458
|
+
*/
|
|
13459
|
+
function linkSkillDoc(input) {
|
|
13460
|
+
const exists = input.exists ?? existsSync;
|
|
13461
|
+
const write = input.write ?? ((path, body) => {
|
|
13462
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
13463
|
+
writeFileSync(path, body, "utf8");
|
|
13464
|
+
});
|
|
13465
|
+
const path = join(input.dir, input.name, "SKILL.md");
|
|
13466
|
+
if (exists(path) && !input.force) return {
|
|
13467
|
+
path,
|
|
13468
|
+
written: false,
|
|
13469
|
+
reason: `${input.name} already exists there; pass --force to replace it`
|
|
13470
|
+
};
|
|
13471
|
+
write(path, content(input.content));
|
|
13472
|
+
return {
|
|
13473
|
+
path,
|
|
13474
|
+
written: true
|
|
13475
|
+
};
|
|
13476
|
+
}
|
|
13286
13477
|
//#endregion
|
|
13287
13478
|
//#region src/task-command.ts
|
|
13288
13479
|
const collect = (value, previous = []) => [...previous, value];
|
|
@@ -13868,24 +14059,6 @@ async function putTextCommand(client, opts) {
|
|
|
13868
14059
|
});
|
|
13869
14060
|
}
|
|
13870
14061
|
/** 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
14062
|
function runAction(state, handler) {
|
|
13890
14063
|
return async (_opts, cmd) => {
|
|
13891
14064
|
const opts = cmd.optsWithGlobals();
|
|
@@ -14628,6 +14801,64 @@ Default target when neither is present: ${skillsDirDefaultHint()}`).action((dir,
|
|
|
14628
14801
|
console.log(` ${result.path}`);
|
|
14629
14802
|
console.error(`\n[busabase-cli] this copy is bound to this host. For the canonical pair, run:\n ${SKILL_REGISTRY_HINT}`);
|
|
14630
14803
|
});
|
|
14804
|
+
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", `
|
|
14805
|
+
\`install\` writes a COPY. \`link\` writes a POINTER: a short SKILL.md holding only the
|
|
14806
|
+
hosted skill's own name and description plus the command to fetch its body, so the
|
|
14807
|
+
body itself never lands on disk here.
|
|
14808
|
+
|
|
14809
|
+
Use it when one skill has to serve several repositories. Vendoring a copy into each
|
|
14810
|
+
one means copies that drift; a pointer means every repository reads the one body, and
|
|
14811
|
+
an edit to it — reviewed once, through a Change Request — is live for all of them at
|
|
14812
|
+
the same moment.
|
|
14813
|
+
|
|
14814
|
+
busabase-cli skill link --node-id nodmtv5rwuofuhaqwl
|
|
14815
|
+
|
|
14816
|
+
Writes <dir>/<the skill's own name>/SKILL.md. The name comes from the hosted
|
|
14817
|
+
frontmatter, not from you, because that name is how an agent resolves "read the
|
|
14818
|
+
crm-visits skill in this folder".
|
|
14819
|
+
|
|
14820
|
+
A pointer names a specific node in a specific space, so: everyone who uses it needs
|
|
14821
|
+
read access to that space, and it must never be published as a template — a template
|
|
14822
|
+
materializes fresh resources in the installer's space, while these ids name yours.
|
|
14823
|
+
Default target when neither is present: ${skillsDirDefaultHint()}`).action(async (dir, _opts, cmd) => {
|
|
14824
|
+
const opts = cmd.optsWithGlobals();
|
|
14825
|
+
const config = resolveConfig(opts);
|
|
14826
|
+
state.config = config;
|
|
14827
|
+
const nodeId = String(opts.nodeId);
|
|
14828
|
+
const file = await createBusabaseClient(config).fileTrees.readFile({
|
|
14829
|
+
nodeId,
|
|
14830
|
+
type: "skill",
|
|
14831
|
+
filePath: "SKILL.md"
|
|
14832
|
+
});
|
|
14833
|
+
const identity = parseSkillIdentity(String(file.content ?? ""));
|
|
14834
|
+
const result = linkSkillDoc({
|
|
14835
|
+
dir: resolveSkillsDir({
|
|
14836
|
+
explicit: dir ?? opts.dir,
|
|
14837
|
+
cwd: process.cwd(),
|
|
14838
|
+
home: homedir(),
|
|
14839
|
+
exists: existsSync
|
|
14840
|
+
}),
|
|
14841
|
+
name: identity.name,
|
|
14842
|
+
content: buildPointerStub({
|
|
14843
|
+
...identity,
|
|
14844
|
+
nodeId,
|
|
14845
|
+
spaceId: config.spaceId
|
|
14846
|
+
}),
|
|
14847
|
+
force: Boolean(opts.force)
|
|
14848
|
+
});
|
|
14849
|
+
if (!result.written) throw new CliOutcomeError("CONFLICT", `Not linked: ${result.path} — ${result.reason}`);
|
|
14850
|
+
if (config.output === "json") {
|
|
14851
|
+
console.log(render({
|
|
14852
|
+
path: result.path,
|
|
14853
|
+
name: identity.name,
|
|
14854
|
+
nodeId
|
|
14855
|
+
}, "json"));
|
|
14856
|
+
return;
|
|
14857
|
+
}
|
|
14858
|
+
console.log(`Linked the \`${identity.name}\` skill to node ${nodeId} at:`);
|
|
14859
|
+
console.log(` ${result.path}`);
|
|
14860
|
+
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.");
|
|
14861
|
+
});
|
|
14631
14862
|
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
14863
|
const opts = cmd.optsWithGlobals();
|
|
14633
14864
|
const config = resolveConfig(opts);
|
|
@@ -14911,18 +15142,10 @@ Examples:
|
|
|
14911
15142
|
throw new CliOutcomeError("VALIDATION", `Invalid agent prompts in ${filePath}${detail ? ` — ${detail}` : ""}`);
|
|
14912
15143
|
}
|
|
14913
15144
|
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
|
-
}
|
|
15145
|
+
return await client.nodes.updateAgentPrompts({
|
|
15146
|
+
nodeId,
|
|
15147
|
+
agentPrompts: parsed.data
|
|
15148
|
+
});
|
|
14926
15149
|
}));
|
|
14927
15150
|
const bases = program.command("bases").description("Bases (structured tables)");
|
|
14928
15151
|
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.70.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.70.0",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"qrcode": "^1.5.4",
|
|
39
39
|
"tar-stream": "^3.1.7",
|