busa-cli 0.42.0 → 0.51.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.
|
@@ -103,6 +103,21 @@ const LocaleSchema = z.enum({
|
|
|
103
103
|
*/
|
|
104
104
|
const iStringRecordSchema = z.partialRecord(LocaleSchema, z.string());
|
|
105
105
|
const iStringSchema = z.union([z.string(), iStringRecordSchema]).describe("i18n string");
|
|
106
|
+
/**
|
|
107
|
+
* Quick parse to string
|
|
108
|
+
*
|
|
109
|
+
* @param str
|
|
110
|
+
* @param lang default en
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
const iStringParse = (str, lang = "en") => {
|
|
114
|
+
if (!str) return "";
|
|
115
|
+
if (typeof str === "string") return str;
|
|
116
|
+
const locales = Object.keys(str || {});
|
|
117
|
+
const result = str[locales.find((locale) => locale === lang) || locales.find((locale) => locale !== "en") || "en"];
|
|
118
|
+
if (result === void 0) console.warn(`iStringParse: ${JSON.stringify(str)} has no '${lang}' or other locales`);
|
|
119
|
+
return result || "";
|
|
120
|
+
};
|
|
106
121
|
//#endregion
|
|
107
122
|
//#region ../../packages/busabase-contract/src/contract/node-agent-prompt-schemas.ts
|
|
108
123
|
/**
|
|
@@ -5614,6 +5629,26 @@ const TemplateManifestSchema = z.object({
|
|
|
5614
5629
|
secrets: z.array(TemplateSecretSchema).default([]),
|
|
5615
5630
|
requires: z.object({ airapp: z.boolean().optional() }).default({})
|
|
5616
5631
|
});
|
|
5632
|
+
const TemplateRiskLevelSchema = z.enum([
|
|
5633
|
+
"gated-write",
|
|
5634
|
+
"local-write",
|
|
5635
|
+
"read-only",
|
|
5636
|
+
"sandbox"
|
|
5637
|
+
]);
|
|
5638
|
+
/**
|
|
5639
|
+
* Normalize a declared `risk` string to a known level, or `undefined`.
|
|
5640
|
+
*
|
|
5641
|
+
* Deliberately never throws. The frontmatter field stays a free-form string
|
|
5642
|
+
* (see `SkillBusabaseMetadataSchema` below) so an author's typo or an
|
|
5643
|
+
* already-published package using a since-retired term (`"review-first"`, from
|
|
5644
|
+
* before this enum existed) fails soft — the card shows "undeclared" instead of
|
|
5645
|
+
* guessing, and the package still installs. Only `busabase-cli check` should
|
|
5646
|
+
* ever turn an unrecognized value into something the author sees and fixes.
|
|
5647
|
+
*/
|
|
5648
|
+
const parseTemplateRisk = (raw) => {
|
|
5649
|
+
const result = TemplateRiskLevelSchema.safeParse(raw);
|
|
5650
|
+
return result.success ? result.data : void 0;
|
|
5651
|
+
};
|
|
5617
5652
|
/**
|
|
5618
5653
|
* `metadata.busabase` inside the root `SKILL.md`'s YAML frontmatter.
|
|
5619
5654
|
*
|
|
@@ -5627,6 +5662,12 @@ const SkillBusabaseMetadataSchema = z.object({
|
|
|
5627
5662
|
folderSlug: z.string().optional(),
|
|
5628
5663
|
/** Resource keys the manual talks about; each must exist under `content/`. */
|
|
5629
5664
|
resources: z.array(z.string()).default([]),
|
|
5665
|
+
/**
|
|
5666
|
+
* Free-form on purpose — see `parseTemplateRisk`. Validating this to the enum
|
|
5667
|
+
* here would turn a stranger's typo or a retired term into a hard parse
|
|
5668
|
+
* failure for the whole frontmatter, which is a worse outcome than a card
|
|
5669
|
+
* that cannot show a risk badge.
|
|
5670
|
+
*/
|
|
5630
5671
|
risk: z.string().optional()
|
|
5631
5672
|
});
|
|
5632
5673
|
/**
|
|
@@ -5704,208 +5745,6 @@ z.object({
|
|
|
5704
5745
|
[TEMPLATE_SKILL_METADATA_KEY]: z.literal(true)
|
|
5705
5746
|
});
|
|
5706
5747
|
//#endregion
|
|
5707
|
-
//#region ../../packages/busabase-package/src/frontmatter.ts
|
|
5708
|
-
/**
|
|
5709
|
-
* YAML frontmatter for doc nodes (§6.3): `name`, `description?`, `position?` above
|
|
5710
|
-
* the markdown body.
|
|
5711
|
-
*
|
|
5712
|
-
* Writing is deterministic (§6.6): a fixed key order and LF endings, so re-exporting
|
|
5713
|
-
* an unchanged doc is byte-identical.
|
|
5714
|
-
*/
|
|
5715
|
-
const DELIMITER = "---";
|
|
5716
|
-
/**
|
|
5717
|
-
* The canonical on-disk body: LF endings, no leading or trailing blank lines. Applied
|
|
5718
|
-
* on both read and write so the round trip is a fixed point — without it, the blank
|
|
5719
|
-
* line the writer puts after the closing `---` and the file's trailing newline would
|
|
5720
|
-
* accumulate into the body on every export→install→export cycle.
|
|
5721
|
-
*
|
|
5722
|
-
* Leading/trailing blank lines around a doc body are therefore normalized away on the
|
|
5723
|
-
* first export; internal blank lines are untouched.
|
|
5724
|
-
*/
|
|
5725
|
-
const normalizeDocBody = (body) => body.replaceAll("\r\n", "\n").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
5726
|
-
/** Split `---\n<yaml>\n---\n<body>`. A doc with no frontmatter is all body. */
|
|
5727
|
-
const parseFrontmatter = (text, filePath) => {
|
|
5728
|
-
const normalized = text.replaceAll("\r\n", "\n");
|
|
5729
|
-
if (!normalized.startsWith(`${DELIMITER}\n`)) return {
|
|
5730
|
-
data: {},
|
|
5731
|
-
body: normalizeDocBody(normalized)
|
|
5732
|
-
};
|
|
5733
|
-
const end = normalized.indexOf(`\n${DELIMITER}`, 3);
|
|
5734
|
-
if (end === -1) throw new Error(`${filePath} opens YAML frontmatter with "---" but never closes it. Add a closing "---" line.`);
|
|
5735
|
-
const rawYaml = normalized.slice(4, end);
|
|
5736
|
-
const body = normalizeDocBody(normalized.slice(end + 1 + 3));
|
|
5737
|
-
let data;
|
|
5738
|
-
try {
|
|
5739
|
-
data = parse(rawYaml);
|
|
5740
|
-
} catch (error) {
|
|
5741
|
-
throw new Error(`${filePath} has invalid YAML frontmatter: ${error.message}`);
|
|
5742
|
-
}
|
|
5743
|
-
if (data === null || data === void 0) return {
|
|
5744
|
-
data: {},
|
|
5745
|
-
body
|
|
5746
|
-
};
|
|
5747
|
-
if (typeof data !== "object" || Array.isArray(data)) throw new Error(`${filePath} frontmatter must be a YAML mapping (key: value pairs).`);
|
|
5748
|
-
return {
|
|
5749
|
-
data,
|
|
5750
|
-
body
|
|
5751
|
-
};
|
|
5752
|
-
};
|
|
5753
|
-
/** Serialize a doc to `---\n<frontmatter>\n---\n<body>` with a trailing newline. */
|
|
5754
|
-
const serializeDoc = (fields, body) => {
|
|
5755
|
-
const data = { name: fields.name };
|
|
5756
|
-
if (fields.description) data.description = fields.description;
|
|
5757
|
-
if (fields.position !== void 0) data.position = fields.position;
|
|
5758
|
-
const yaml = stringify(data, { lineWidth: 0 }).replace(/\n$/, "");
|
|
5759
|
-
return `${DELIMITER}\n${yaml}\n${DELIMITER}\n\n${normalizeDocBody(body)}\n`;
|
|
5760
|
-
};
|
|
5761
|
-
//#endregion
|
|
5762
|
-
//#region ../../packages/busabase-package/src/audit.ts
|
|
5763
|
-
/**
|
|
5764
|
-
* Is this package **fit to publish**?
|
|
5765
|
-
*
|
|
5766
|
-
* Deliberately a different question from {@link validateTemplate}, and the split is
|
|
5767
|
-
* the point rather than an organisational preference:
|
|
5768
|
-
*
|
|
5769
|
-
* - `validateTemplate` **classifies**. Its verdict changes what install *does* — a
|
|
5770
|
-
* Skill node, ownership stamps and a sample merge, versus a plain package install.
|
|
5771
|
-
* Four consumers must agree on it or the catalog lies about what an install does,
|
|
5772
|
-
* so it stays small and stable.
|
|
5773
|
-
* - `auditPackage` / `auditSkill` **advise**. Nothing behaves differently based on
|
|
5774
|
-
* their verdict; they exist for the author and the reviewer, and are free to grow.
|
|
5775
|
-
*
|
|
5776
|
-
* The practical consequence: a declared screenshot that is not on disk belongs here,
|
|
5777
|
-
* never in `validateTemplate`. Promoting it there would stop a template installing
|
|
5778
|
-
* over a missing image, which is the wrong lever entirely.
|
|
5779
|
-
*
|
|
5780
|
-
* Nothing here re-checks what `validateTemplate` already decides (declared resources,
|
|
5781
|
-
* the sample-row ceiling, name agreement between manifest and manual, an unambiguous
|
|
5782
|
-
* primary AirApp). Run both; they compose.
|
|
5783
|
-
*
|
|
5784
|
-
* Purely static: no install, no network, and none of the package's own code is
|
|
5785
|
-
* executed — safe to run in CI over a pull request from someone you do not know.
|
|
5786
|
-
*/
|
|
5787
|
-
/**
|
|
5788
|
-
* A materialized workspace id: busabase-core's `id()` is a three-letter prefix, the
|
|
5789
|
-
* creation time in base36, then seven random characters.
|
|
5790
|
-
*
|
|
5791
|
-
* The lookahead requiring a digit is what keeps ordinary words out — `recommendation`
|
|
5792
|
-
* has the right prefix and the wrong shape. A real id always carries its timestamp.
|
|
5793
|
-
*/
|
|
5794
|
-
const WORKSPACE_ID = /(?<![0-9a-z])(?:nod|bse|viw|rec|crq|ast|com|fom|shr|fvl|opr|cmt|rev|whd|aud)(?=[0-9a-z]{15}(?![0-9a-z]))(?=[0-9a-z]*\d)[0-9a-z]{15}/;
|
|
5795
|
-
const TEXT_EXTENSIONS = /\.(?:js|mjs|cjs|ts|mts|cts|json|ndjson|md|html|css|ya?ml|txt)$|(?:^|\/)\.env(?:\.|$)/;
|
|
5796
|
-
/**
|
|
5797
|
-
* Files that exist to *forbid* a string necessarily contain it. An AirApp's own
|
|
5798
|
-
* `scripts/check.mjs` asserts the obsolete API prefix is absent, and a test fixture
|
|
5799
|
-
* spells out the credential it proves gets rejected — scanning those turns a
|
|
5800
|
-
* correctly-guarded package into a failure.
|
|
5801
|
-
*/
|
|
5802
|
-
const IS_ASSERTION_CODE = /(?:^|\/)(?:scripts\/check\.mjs|tests?\/|[^/]*\.test\.[cm]?[jt]s)$/;
|
|
5803
|
-
/** Lockfiles carry integrity hashes that are not ours to interpret. */
|
|
5804
|
-
const NOT_SOURCE = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/;
|
|
5805
|
-
const relativePaths = (files, root = "") => {
|
|
5806
|
-
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
5807
|
-
const out = [];
|
|
5808
|
-
for (const key of files.keys()) if (!prefix || key.startsWith(prefix)) out.push(key.slice(prefix.length));
|
|
5809
|
-
return out;
|
|
5810
|
-
};
|
|
5811
|
-
const readText = (files, root, relative) => {
|
|
5812
|
-
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
5813
|
-
return files.get(`${prefix}${relative}`)?.toString("utf8");
|
|
5814
|
-
};
|
|
5815
|
-
/**
|
|
5816
|
-
* Audit the package layer: identity, the assets the catalog promises, and anything
|
|
5817
|
-
* that must never be published.
|
|
5818
|
-
*
|
|
5819
|
-
* `files` is the flat map `readPackageTree` was given, not the tree. That is load-bearing:
|
|
5820
|
-
* `assets/screenshots/` holds no nodes and so never reaches the tree, and "the manifest
|
|
5821
|
-
* promises a screenshot that does not exist" is exactly a file-level question.
|
|
5822
|
-
*/
|
|
5823
|
-
const auditPackage = (tree, files, options = {}) => {
|
|
5824
|
-
const findings = [];
|
|
5825
|
-
const error = (rule, message) => findings.push({
|
|
5826
|
-
severity: "error",
|
|
5827
|
-
rule,
|
|
5828
|
-
message
|
|
5829
|
-
});
|
|
5830
|
-
const warn = (rule, message) => findings.push({
|
|
5831
|
-
severity: "warning",
|
|
5832
|
-
rule,
|
|
5833
|
-
message
|
|
5834
|
-
});
|
|
5835
|
-
const root = options.root ?? "";
|
|
5836
|
-
const paths = relativePaths(files, root);
|
|
5837
|
-
const present = new Set(paths);
|
|
5838
|
-
if (options.directoryName !== void 0 && options.directoryName !== tree.manifest.name) error("package/identity", `Directory is "${options.directoryName}" but busabase.json declares "${tree.manifest.name}". The directory name is the package's identity, not a label beside it.`);
|
|
5839
|
-
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.`);
|
|
5840
|
-
for (const relative of paths) {
|
|
5841
|
-
const base = relative.slice(relative.lastIndexOf("/") + 1);
|
|
5842
|
-
if (/^\.env(?!\.example$)(?:\.|$)/.test(base)) error("package/dotenv", `${relative} is part of the package. Credentials belong in Vault, never in something people install.`);
|
|
5843
|
-
if (/^(?:id_rsa|id_ed25519)$/.test(base) || /\.(?:pem|p12|pfx|keystore)$/.test(base)) error("package/key-material", `${relative} looks like key material and must not be published.`);
|
|
5844
|
-
if (/(?:^|\/)(?:node_modules|dist|build)\//.test(relative)) warn("package/build-output", `${relative} looks like installed or built output. Ship source; the installer builds nothing.`);
|
|
5845
|
-
}
|
|
5846
|
-
for (const relative of paths) {
|
|
5847
|
-
if (!TEXT_EXTENSIONS.test(relative)) continue;
|
|
5848
|
-
if (NOT_SOURCE.test(relative) || IS_ASSERTION_CODE.test(relative)) continue;
|
|
5849
|
-
const source = readText(files, root, relative);
|
|
5850
|
-
if (source === void 0) continue;
|
|
5851
|
-
const found = WORKSPACE_ID.exec(source);
|
|
5852
|
-
if (found) error("package/workspace-ids", `${relative} contains "${found[0]}", a materialized workspace id. Node, Base and Space ids are whoever last ran setup's runtime state — publishing them ships one person's workspace layout to everyone.`);
|
|
5853
|
-
}
|
|
5854
|
-
return findings;
|
|
5855
|
-
};
|
|
5856
|
-
/**
|
|
5857
|
-
* Audit the Skill layer: is the manual one an agent can actually act on?
|
|
5858
|
-
*
|
|
5859
|
-
* Runs for any directory carrying a root `SKILL.md` — package or not, template or not.
|
|
5860
|
-
* It reports nothing at all when there is no manual: a package without one is a
|
|
5861
|
-
* perfectly legitimate thing to publish, not a Skill that failed.
|
|
5862
|
-
*
|
|
5863
|
-
* The honest limit: a checker can tell you the frontmatter parses and the files the
|
|
5864
|
-
* manual points at exist. It cannot tell you the manual is *true*. Everything in
|
|
5865
|
-
* `SKILL.md` becomes instructions an agent acts on, and no static rule catches a
|
|
5866
|
-
* confident, wrong sentence about what a table means.
|
|
5867
|
-
*/
|
|
5868
|
-
const auditSkill = (files, options = {}) => {
|
|
5869
|
-
const findings = [];
|
|
5870
|
-
const error = (rule, message) => findings.push({
|
|
5871
|
-
severity: "error",
|
|
5872
|
-
rule,
|
|
5873
|
-
message
|
|
5874
|
-
});
|
|
5875
|
-
const warn = (rule, message) => findings.push({
|
|
5876
|
-
severity: "warning",
|
|
5877
|
-
rule,
|
|
5878
|
-
message
|
|
5879
|
-
});
|
|
5880
|
-
const root = options.root ?? "";
|
|
5881
|
-
const source = readText(files, root, PACKAGE_SKILL_ENTRY);
|
|
5882
|
-
if (source === void 0) return findings;
|
|
5883
|
-
let body = "";
|
|
5884
|
-
try {
|
|
5885
|
-
const parsed = parseFrontmatter(source, PACKAGE_SKILL_ENTRY);
|
|
5886
|
-
body = parsed.body;
|
|
5887
|
-
const frontmatter = SkillFrontmatterSchema$1.safeParse(parsed.data);
|
|
5888
|
-
if (!frontmatter.success) error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY} frontmatter is not a valid Skill declaration: ${frontmatter.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ")}`);
|
|
5889
|
-
else if (frontmatter.data.description.trim() === "") error("skill/description", `${PACKAGE_SKILL_ENTRY} has no description. An agent chooses whether to reach for a Skill by reading it, so an empty one is not cosmetic — it is a Skill that never gets picked.`);
|
|
5890
|
-
} catch (cause) {
|
|
5891
|
-
error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY}: ${cause.message}`);
|
|
5892
|
-
return findings;
|
|
5893
|
-
}
|
|
5894
|
-
const todos = (body.match(/^.*\bTODO\b.*$/gm) ?? []).length;
|
|
5895
|
-
if (todos > 0) error("skill/todo", `${PACKAGE_SKILL_ENTRY} still carries ${todos} TODO line(s). The export draft leaves them on purpose — an agent acts on what this file says, and a blank the author was meant to fill in is worse than nothing once it ships.`);
|
|
5896
|
-
const present = new Set(relativePaths(files, root));
|
|
5897
|
-
const sidecar = new RegExp(`\\]\\((\\.?/?(?:${PACKAGE_SIDECARS})/[^)\\s]+)\\)|\`((?:${PACKAGE_SIDECARS})/[^\`\\s]+)\``, "g");
|
|
5898
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5899
|
-
for (const match of body.matchAll(sidecar)) {
|
|
5900
|
-
const target = (match[1] ?? match[2]).replace(/^\.?\//, "");
|
|
5901
|
-
if (seen.has(target)) continue;
|
|
5902
|
-
seen.add(target);
|
|
5903
|
-
if (!present.has(target)) warn("skill/missing-reference", `${PACKAGE_SKILL_ENTRY} points at "${target}", which the package does not ship. An agent told to read it has an instruction it cannot satisfy.`);
|
|
5904
|
-
}
|
|
5905
|
-
return findings;
|
|
5906
|
-
};
|
|
5907
|
-
const PACKAGE_SIDECARS = PACKAGE_SKILL_SIDECAR_DIRS.join("|");
|
|
5908
|
-
//#endregion
|
|
5909
5748
|
//#region ../../packages/busabase-contract/src/contract/auto-merge.ts
|
|
5910
5749
|
/**
|
|
5911
5750
|
* `autoMerge` on a destructive-but-reversible operation.
|
|
@@ -6291,12 +6130,7 @@ z.object({
|
|
|
6291
6130
|
z.object({
|
|
6292
6131
|
parentId: z.string().nullable().optional().describe("Node to start from. Omit or null to start from the space root."),
|
|
6293
6132
|
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."),
|
|
6294
|
-
|
|
6295
|
-
* `active` (default) walks the live tree. `archived` returns the flat set of
|
|
6296
|
-
* soft-archived nodes for the Trash view — no `parentId`/`depth` walk, since
|
|
6297
|
-
* archived nodes are shown as a list, not a tree.
|
|
6298
|
-
*/
|
|
6299
|
-
status: z.enum(["active", "archived"]).optional().default("active"),
|
|
6133
|
+
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."),
|
|
6300
6134
|
/**
|
|
6301
6135
|
* Narrow to specific node types and return a FLAT list of lightweight node
|
|
6302
6136
|
* summaries (`children: []`) instead of walking the tree. This is what
|
|
@@ -6323,8 +6157,8 @@ z.object({
|
|
|
6323
6157
|
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}.")
|
|
6324
6158
|
}).optional();
|
|
6325
6159
|
z.object({
|
|
6326
|
-
nodeId: z.string(),
|
|
6327
|
-
potentialAncestorId: z.string()
|
|
6160
|
+
nodeId: z.string().describe("The node walked UPWARDS from — the possible descendant."),
|
|
6161
|
+
potentialAncestorId: z.string().describe("The node looked for on the way up. Answers \"is `nodeId` inside this one?\", not the reverse — swapping the two silently returns the wrong answer rather than an error.")
|
|
6328
6162
|
});
|
|
6329
6163
|
z.object({ isDescendant: z.boolean() });
|
|
6330
6164
|
z.object({
|
|
@@ -6379,8 +6213,8 @@ z.object({
|
|
|
6379
6213
|
agentPrompts: customAgentPromptsSchema.nullable()
|
|
6380
6214
|
});
|
|
6381
6215
|
z.object({
|
|
6382
|
-
query: z.string().min(1),
|
|
6383
|
-
limit: z.coerce.number().int().min(1).max(50).optional().default(20)
|
|
6216
|
+
query: z.string().min(1).describe("Matched against node NAMES only. Use `/api/v1/search` to search content."),
|
|
6217
|
+
limit: z.coerce.number().int().min(1).max(50).optional().default(20).describe("Results to return. Capped at 50 here, unlike most listings' 100.")
|
|
6384
6218
|
});
|
|
6385
6219
|
z.object({
|
|
6386
6220
|
id: z.string(),
|
|
@@ -6571,8 +6405,8 @@ z.object({
|
|
|
6571
6405
|
unreadCount: z.number().int()
|
|
6572
6406
|
});
|
|
6573
6407
|
z.object({
|
|
6574
|
-
page: z.number().int().min(1).optional().default(1),
|
|
6575
|
-
pageSize: z.number().int().min(1).max(100).optional().default(50)
|
|
6408
|
+
page: z.number().int().min(1).optional().default(1).describe("1-indexed, not 0-indexed."),
|
|
6409
|
+
pageSize: z.number().int().min(1).max(100).optional().default(50).describe("Mentions per page. Capped at 100.")
|
|
6576
6410
|
});
|
|
6577
6411
|
z.object({
|
|
6578
6412
|
/** Stamps every unread mention row this caller has on that comment. */
|
|
@@ -6810,8 +6644,8 @@ z.object({
|
|
|
6810
6644
|
reason: z.string().optional()
|
|
6811
6645
|
});
|
|
6812
6646
|
z.object({
|
|
6813
|
-
subjectType: commentSubjectTypeSchema,
|
|
6814
|
-
subjectId: z.string().min(1)
|
|
6647
|
+
subjectType: commentSubjectTypeSchema.describe("What the comment thread hangs off, which decides how `subjectId` is interpreted."),
|
|
6648
|
+
subjectId: z.string().min(1).describe("The subject's id, interpreted according to `subjectType`.")
|
|
6815
6649
|
}).extend({
|
|
6816
6650
|
authorId: z.string().optional().default("local-admin"),
|
|
6817
6651
|
body: z.string().trim().min(1),
|
|
@@ -6822,29 +6656,28 @@ z.object({
|
|
|
6822
6656
|
*/
|
|
6823
6657
|
mentions: z.array(commentMentionInputSchema).optional().default([])
|
|
6824
6658
|
});
|
|
6825
|
-
z.object({ limit: z.coerce.number().int().min(1).max(100).optional().default(50) }).optional().default({ limit: 50 });
|
|
6826
|
-
z.object({ status: z.enum(["active", "archived"]).optional().default("active") });
|
|
6659
|
+
z.object({ limit: z.coerce.number().int().min(1).max(100).optional().default(50).describe("Rows to return, most recent first. Capped at 100; this listing has no cursor.") }).optional().default({ limit: 50 });
|
|
6660
|
+
z.object({ status: z.enum(["active", "archived"]).optional().default("active").describe("`active` (default) or the soft-archived set. Archived rows have the SAME shape as live ones — this is a predicate, not a different resource.") });
|
|
6827
6661
|
z.object({
|
|
6828
|
-
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
affectsNodeId: z.string().min(1).optional()
|
|
6662
|
+
limit: z.coerce.number().int().min(1).max(100).optional().default(50).describe("Change requests per page. Capped at 100; ask for the next page with `cursor`."),
|
|
6663
|
+
cursor: z.string().optional().describe("Opaque page cursor: pass back the `nextCursor` from the previous response. Do not construct or parse it."),
|
|
6664
|
+
status: z.array(changeRequestStatusSchema).optional().describe("Keep only these statuses. Omitting it returns every status, not just open ones."),
|
|
6665
|
+
mine: z.boolean().optional().describe("Only change requests CREATED by the acting user — not ones awaiting their review."),
|
|
6666
|
+
affectsNodeId: z.string().min(1).optional().describe("Only change requests whose target, or any of their operations, touches this node — including Base-backed nodes. To ask whether a resource already has an unfinished change request, pass this with `limit: 1` rather than paging the space: an empty result is conclusive.")
|
|
6834
6667
|
}).optional().default({ limit: 50 });
|
|
6835
6668
|
z.object({
|
|
6836
6669
|
changeRequests: z.array(changeRequestSchema),
|
|
6837
6670
|
nextCursor: z.string().nullable()
|
|
6838
6671
|
});
|
|
6839
6672
|
const changeRequestPageInputShape = {
|
|
6840
|
-
page: z.coerce.number().int().min(1).optional().default(1),
|
|
6841
|
-
pageSize: z.coerce.number().int().min(1).max(100).optional().default(50),
|
|
6842
|
-
status: z.array(changeRequestStatusSchema).optional(),
|
|
6843
|
-
mine: z.boolean().optional()
|
|
6673
|
+
page: z.coerce.number().int().min(1).optional().default(1).describe("1-indexed, not 0-indexed."),
|
|
6674
|
+
pageSize: z.coerce.number().int().min(1).max(100).optional().default(50).describe("Change requests per page. Capped at 100."),
|
|
6675
|
+
status: z.array(changeRequestStatusSchema).optional().describe("Keep only these statuses. Omitting it returns every status, not just open ones."),
|
|
6676
|
+
mine: z.boolean().optional().describe("Only change requests CREATED by the acting user — not ones awaiting their review.")
|
|
6844
6677
|
};
|
|
6845
6678
|
z.object({
|
|
6846
6679
|
...changeRequestPageInputShape,
|
|
6847
|
-
affectsNodeId: z.string().min(1).optional()
|
|
6680
|
+
affectsNodeId: z.string().min(1).optional().describe("Only change requests whose target, or any of their operations, touches this node — including Base-backed nodes.")
|
|
6848
6681
|
}).optional().default({
|
|
6849
6682
|
page: 1,
|
|
6850
6683
|
pageSize: 50
|
|
@@ -6876,9 +6709,9 @@ const SEARCH_SOURCES = [
|
|
|
6876
6709
|
"nodes"
|
|
6877
6710
|
];
|
|
6878
6711
|
z.object({
|
|
6879
|
-
query: z.string().default(""),
|
|
6880
|
-
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
|
6881
|
-
offset: z.coerce.number().int().min(0).optional().default(0),
|
|
6712
|
+
query: z.string().default("").describe("Full-text query. An empty string matches nothing."),
|
|
6713
|
+
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."),
|
|
6714
|
+
offset: z.coerce.number().int().min(0).optional().default(0).describe("0-indexed skip count. This endpoint pages by offset, not by cursor."),
|
|
6882
6715
|
/**
|
|
6883
6716
|
* Restrict which content this call searches. Omitted means all three
|
|
6884
6717
|
* (unchanged behavior for every caller before this parameter existed).
|
|
@@ -6888,7 +6721,7 @@ z.object({
|
|
|
6888
6721
|
* (`?sources=records&sources=files`) becomes an array. Accept both shapes
|
|
6889
6722
|
* and normalize to an array.
|
|
6890
6723
|
*/
|
|
6891
|
-
sources: z.union([z.array(z.enum(SEARCH_SOURCES)), z.enum(SEARCH_SOURCES)]).transform((value) => Array.isArray(value) ? value : [value]).optional()
|
|
6724
|
+
sources: z.union([z.array(z.enum(SEARCH_SOURCES)), z.enum(SEARCH_SOURCES)]).transform((value) => Array.isArray(value) ? value : [value]).optional().describe("Restrict which content is searched. Omitting it searches ALL sources. Repeat the parameter to pass several (`?sources=records&sources=files`); a single occurrence is accepted as a bare value.")
|
|
6892
6725
|
});
|
|
6893
6726
|
const authSpaceSchema = z.object({
|
|
6894
6727
|
id: z.string(),
|
|
@@ -7140,9 +6973,27 @@ const PACKAGE_MAX_FILE_COUNT = 5e3;
|
|
|
7140
6973
|
const PACKAGE_MAX_RECORDS_PER_BASE = 5e4;
|
|
7141
6974
|
const PackageManifestSchema = z.object({
|
|
7142
6975
|
format: z.literal(PACKAGE_FORMAT),
|
|
7143
|
-
/**
|
|
6976
|
+
/**
|
|
6977
|
+
* Default `--into-folder` name. Not the repo name — a repo can host many
|
|
6978
|
+
* packages. This is the package's IDENTITY, not a label: it must equal the
|
|
6979
|
+
* on-disk directory name and the SKILL.md frontmatter name (enforced by
|
|
6980
|
+
* `audit.ts`'s "one identity" rule), and it is used as a slug — CLI sort,
|
|
6981
|
+
* `suggestSlug()`, the Dashboard's `/templates/:name` route, and the
|
|
6982
|
+
* install-target folder name all key off it verbatim. Deliberately NOT an
|
|
6983
|
+
* iString: none of those call sites expect a locale-keyed object, and
|
|
6984
|
+
* "the directory name is the package's identity, not a label beside it"
|
|
6985
|
+
* (audit.ts) is exactly the argument against ever making it one. A
|
|
6986
|
+
* human-facing translated title belongs in `description` below, not here.
|
|
6987
|
+
*/
|
|
7144
6988
|
name: z.string().min(1),
|
|
7145
|
-
|
|
6989
|
+
/**
|
|
6990
|
+
* The one-line blurb shown on a template's catalog card, and (via
|
|
6991
|
+
* `layout-read.ts`) the description on the Folder node created at install
|
|
6992
|
+
* time — Folder/node descriptions are already iString-capable text
|
|
6993
|
+
* columns, so this single field legitimately serves both. Plain string
|
|
6994
|
+
* stays valid forever; nothing that already writes a bare string breaks.
|
|
6995
|
+
*/
|
|
6996
|
+
description: iStringSchema.default(""),
|
|
7146
6997
|
/**
|
|
7147
6998
|
* Informational / reserved for a future marketplace index. The real version pin
|
|
7148
6999
|
* is always the git ref in the install URL.
|
|
@@ -7161,19 +7012,40 @@ const PackageManifestSchema = z.object({
|
|
|
7161
7012
|
*/
|
|
7162
7013
|
template: TemplateManifestSchema.optional()
|
|
7163
7014
|
});
|
|
7015
|
+
/**
|
|
7016
|
+
* Scenario Agent prompts for the node this sidecar describes — what a person can
|
|
7017
|
+
* ask an agent to DO with it, carried so it survives export → install.
|
|
7018
|
+
*
|
|
7019
|
+
* Same schema the node's own column and `busabase-cli nodes set-agent-prompts`
|
|
7020
|
+
* validate against (50 per node, 80 chars per label, 8 KiB per body per locale),
|
|
7021
|
+
* so a package cannot smuggle in a list the API would reject.
|
|
7022
|
+
*
|
|
7023
|
+
* Optional and absent by default: a package written before this field, or one
|
|
7024
|
+
* whose author wrote no prompts, is unchanged on disk and installs exactly as
|
|
7025
|
+
* before — the nodes then show their node type's default prompts, which is the
|
|
7026
|
+
* behaviour every package had until now.
|
|
7027
|
+
*
|
|
7028
|
+
* Distinct from `busabase.json`'s template-level `agentPrompts`: that one is a
|
|
7029
|
+
* flat list of strings for the Template Center card and the install-time "Ask
|
|
7030
|
+
* agent" action, about the template as a whole. This one is per node, structured,
|
|
7031
|
+
* and becomes that node's prompts in the dialog after install.
|
|
7032
|
+
*/
|
|
7033
|
+
const packageNodeAgentPromptsSchema = customAgentPromptsSchema.optional();
|
|
7164
7034
|
/** `_node.json` — marks a directory as a verbatim-content file-tree node. */
|
|
7165
7035
|
const PackageFileTreeNodeMetaSchema = z.object({
|
|
7166
7036
|
type: z.enum(PACKAGE_FILE_TREE_NODE_TYPES),
|
|
7167
7037
|
name: z.string().min(1),
|
|
7168
7038
|
description: z.string().default(""),
|
|
7169
|
-
position: z.number().int().optional()
|
|
7039
|
+
position: z.number().int().optional(),
|
|
7040
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7170
7041
|
});
|
|
7171
7042
|
/** `_folder.json` — optional; also how an empty folder survives git. */
|
|
7172
7043
|
const PackageFolderMetaSchema = z.object({
|
|
7173
7044
|
/** Defaults to the humanized slug when absent. */
|
|
7174
7045
|
name: z.string().optional(),
|
|
7175
7046
|
description: z.string().default(""),
|
|
7176
|
-
position: z.number().int().optional()
|
|
7047
|
+
position: z.number().int().optional(),
|
|
7048
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7177
7049
|
});
|
|
7178
7050
|
/** `<filename>.node.json` — optional display metadata for a `file` node. */
|
|
7179
7051
|
const PackageFileNodeMetaSchema = PackageFolderMetaSchema;
|
|
@@ -7198,7 +7070,8 @@ const PackageDocAssetMetaSchema = z.object({
|
|
|
7198
7070
|
const PackageDocFrontmatterSchema = z.object({
|
|
7199
7071
|
name: z.string().min(1),
|
|
7200
7072
|
description: z.string().default(""),
|
|
7201
|
-
position: z.number().int().optional()
|
|
7073
|
+
position: z.number().int().optional(),
|
|
7074
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7202
7075
|
});
|
|
7203
7076
|
/**
|
|
7204
7077
|
* Field options as carried by the package. Mirrors the API's `fieldOptionsSchema`
|
|
@@ -7318,7 +7191,8 @@ const PackageBaseSchema = z.object({
|
|
|
7318
7191
|
requiredApprovals: z.number()
|
|
7319
7192
|
}).optional(),
|
|
7320
7193
|
fields: z.array(PackageBaseFieldSchema).default([]),
|
|
7321
|
-
views: z.array(PackageViewSchema).default([])
|
|
7194
|
+
views: z.array(PackageViewSchema).default([]),
|
|
7195
|
+
agentPrompts: packageNodeAgentPromptsSchema
|
|
7322
7196
|
});
|
|
7323
7197
|
/**
|
|
7324
7198
|
* One record per line. `key` is a package-local stable identifier (export uses the
|
|
@@ -7334,84 +7208,30 @@ const PackageRecordLineSchema = z.object({
|
|
|
7334
7208
|
fields: z.record(z.string(), z.unknown()).default({})
|
|
7335
7209
|
});
|
|
7336
7210
|
//#endregion
|
|
7337
|
-
//#region ../../packages/busabase-package/src/
|
|
7338
|
-
const escapeLiteral = (value) => value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7211
|
+
//#region ../../packages/busabase-package/src/tree.ts
|
|
7339
7212
|
/**
|
|
7340
|
-
*
|
|
7213
|
+
* The in-memory `busabase-package@1` tree — the shape both `layout-read` (repo dir
|
|
7214
|
+
* → tree) and `layout-write` (space subtree → tree → repo dir) speak, and that
|
|
7215
|
+
* `plan`/`apply` consume. Plus the format's shared validation rules (§6.3).
|
|
7216
|
+
*/
|
|
7217
|
+
/**
|
|
7218
|
+
* A node's scenario prompts in a fixed key order — determinism (§6.6).
|
|
7341
7219
|
*
|
|
7342
|
-
*
|
|
7343
|
-
*
|
|
7344
|
-
*
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
if (char === "*") {
|
|
7351
|
-
if (pattern[index + 1] === "*") {
|
|
7352
|
-
index += 1;
|
|
7353
|
-
if (pattern[index + 1] === "/") {
|
|
7354
|
-
index += 1;
|
|
7355
|
-
source += "(?:.*/)?";
|
|
7356
|
-
} else source += ".*";
|
|
7357
|
-
} else source += "[^/]*";
|
|
7358
|
-
continue;
|
|
7359
|
-
}
|
|
7360
|
-
if (char === "?") {
|
|
7361
|
-
source += "[^/]";
|
|
7362
|
-
continue;
|
|
7363
|
-
}
|
|
7364
|
-
if (char === "/") {
|
|
7365
|
-
source += "/";
|
|
7366
|
-
continue;
|
|
7367
|
-
}
|
|
7368
|
-
source += escapeLiteral(char);
|
|
7369
|
-
}
|
|
7370
|
-
return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${source}(?:/.*)?$`);
|
|
7371
|
-
};
|
|
7372
|
-
const parseRule = (line) => {
|
|
7373
|
-
let pattern = line.trim();
|
|
7374
|
-
if (!pattern || pattern.startsWith("#")) return void 0;
|
|
7375
|
-
const negated = pattern.startsWith("!");
|
|
7376
|
-
if (negated) pattern = pattern.slice(1);
|
|
7377
|
-
const directoryOnly = pattern.endsWith("/");
|
|
7378
|
-
if (directoryOnly) pattern = pattern.slice(0, -1);
|
|
7379
|
-
const anchored = pattern.startsWith("/");
|
|
7380
|
-
if (anchored) pattern = pattern.slice(1);
|
|
7381
|
-
if (!pattern) return void 0;
|
|
7382
|
-
return {
|
|
7383
|
-
regex: patternToRegex(pattern, anchored),
|
|
7384
|
-
negated,
|
|
7385
|
-
directoryOnly
|
|
7386
|
-
};
|
|
7387
|
-
};
|
|
7388
|
-
const parseIgnoreFile = (contents) => {
|
|
7389
|
-
const rules = contents.split(/\r?\n/).map(parseRule).filter((rule) => rule !== void 0);
|
|
7390
|
-
return {
|
|
7391
|
-
isEmpty: rules.length === 0,
|
|
7392
|
-
ignores: (relativePath) => {
|
|
7393
|
-
let ignored = false;
|
|
7394
|
-
for (const rule of rules) {
|
|
7395
|
-
if (rule.directoryOnly && !rule.regex.test(`${relativePath}/`)) continue;
|
|
7396
|
-
if (!rule.regex.test(relativePath)) continue;
|
|
7397
|
-
ignored = !rule.negated;
|
|
7398
|
-
}
|
|
7399
|
-
return ignored;
|
|
7400
|
-
}
|
|
7401
|
-
};
|
|
7402
|
-
};
|
|
7403
|
-
/** A matcher that installs everything — used when no ignore file is present. */
|
|
7404
|
-
const IGNORE_NOTHING = {
|
|
7405
|
-
ignores: () => false,
|
|
7406
|
-
isEmpty: true
|
|
7407
|
-
};
|
|
7408
|
-
//#endregion
|
|
7409
|
-
//#region ../../packages/busabase-package/src/tree.ts
|
|
7410
|
-
/**
|
|
7411
|
-
* The in-memory `busabase-package@1` tree — the shape both `layout-read` (repo dir
|
|
7412
|
-
* → tree) and `layout-write` (space subtree → tree → repo dir) speak, and that
|
|
7413
|
-
* `plan`/`apply` consume. Plus the format's shared validation rules (§6.3).
|
|
7220
|
+
* Written through instead of spread verbatim because the list reaches the writer
|
|
7221
|
+
* from two directions: hand-authored on disk (whatever order the author typed)
|
|
7222
|
+
* and via a zod parse (schema order). Without this, re-exporting an unchanged
|
|
7223
|
+
* package would diff on key order alone.
|
|
7224
|
+
*
|
|
7225
|
+
* `intent` is omitted when absent rather than defaulted to `"change"` here — the
|
|
7226
|
+
* schema owns that default, and writing it out would silently rewrite every
|
|
7227
|
+
* author's file on the first round trip.
|
|
7414
7228
|
*/
|
|
7229
|
+
const serializeAgentPrompts = (prompts) => prompts?.length ? prompts.map((prompt) => ({
|
|
7230
|
+
key: prompt.key,
|
|
7231
|
+
...prompt.intent ? { intent: prompt.intent } : {},
|
|
7232
|
+
label: prompt.label,
|
|
7233
|
+
body: prompt.body
|
|
7234
|
+
})) : void 0;
|
|
7415
7235
|
/**
|
|
7416
7236
|
* Every creatable node type's slug is `/^[a-z0-9-]+$/` across the whole API
|
|
7417
7237
|
* (`docs.create`, `files.create`, `createFileTreeInputSchema`, `createBaseInputSchema`,
|
|
@@ -7528,27 +7348,298 @@ const walkNodes = function* (nodes) {
|
|
|
7528
7348
|
};
|
|
7529
7349
|
const collectBaseNodes = (nodes) => [...walkNodes(nodes)].filter((node) => node.type === "base");
|
|
7530
7350
|
//#endregion
|
|
7531
|
-
//#region ../../packages/busabase-package/src/
|
|
7351
|
+
//#region ../../packages/busabase-package/src/frontmatter.ts
|
|
7352
|
+
const DELIMITER = "---";
|
|
7353
|
+
/**
|
|
7354
|
+
* The canonical on-disk body: LF endings, no leading or trailing blank lines. Applied
|
|
7355
|
+
* on both read and write so the round trip is a fixed point — without it, the blank
|
|
7356
|
+
* line the writer puts after the closing `---` and the file's trailing newline would
|
|
7357
|
+
* accumulate into the body on every export→install→export cycle.
|
|
7358
|
+
*
|
|
7359
|
+
* Leading/trailing blank lines around a doc body are therefore normalized away on the
|
|
7360
|
+
* first export; internal blank lines are untouched.
|
|
7361
|
+
*/
|
|
7362
|
+
const normalizeDocBody = (body) => body.replaceAll("\r\n", "\n").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
7363
|
+
/** Split `---\n<yaml>\n---\n<body>`. A doc with no frontmatter is all body. */
|
|
7364
|
+
const parseFrontmatter = (text, filePath) => {
|
|
7365
|
+
const normalized = text.replaceAll("\r\n", "\n");
|
|
7366
|
+
if (!normalized.startsWith(`${DELIMITER}\n`)) return {
|
|
7367
|
+
data: {},
|
|
7368
|
+
body: normalizeDocBody(normalized)
|
|
7369
|
+
};
|
|
7370
|
+
const end = normalized.indexOf(`\n${DELIMITER}`, 3);
|
|
7371
|
+
if (end === -1) throw new Error(`${filePath} opens YAML frontmatter with "---" but never closes it. Add a closing "---" line.`);
|
|
7372
|
+
const rawYaml = normalized.slice(4, end);
|
|
7373
|
+
const body = normalizeDocBody(normalized.slice(end + 1 + 3));
|
|
7374
|
+
let data;
|
|
7375
|
+
try {
|
|
7376
|
+
data = parse(rawYaml);
|
|
7377
|
+
} catch (error) {
|
|
7378
|
+
throw new Error(`${filePath} has invalid YAML frontmatter: ${error.message}`);
|
|
7379
|
+
}
|
|
7380
|
+
if (data === null || data === void 0) return {
|
|
7381
|
+
data: {},
|
|
7382
|
+
body
|
|
7383
|
+
};
|
|
7384
|
+
if (typeof data !== "object" || Array.isArray(data)) throw new Error(`${filePath} frontmatter must be a YAML mapping (key: value pairs).`);
|
|
7385
|
+
return {
|
|
7386
|
+
data,
|
|
7387
|
+
body
|
|
7388
|
+
};
|
|
7389
|
+
};
|
|
7390
|
+
/** Serialize a doc to `---\n<frontmatter>\n---\n<body>` with a trailing newline. */
|
|
7391
|
+
const serializeDoc = (fields, body) => {
|
|
7392
|
+
const data = { name: fields.name };
|
|
7393
|
+
if (fields.description) data.description = fields.description;
|
|
7394
|
+
if (fields.position !== void 0) data.position = fields.position;
|
|
7395
|
+
const agentPrompts = serializeAgentPrompts(fields.agentPrompts);
|
|
7396
|
+
if (agentPrompts) data.agentPrompts = agentPrompts;
|
|
7397
|
+
const yaml = stringify(data, { lineWidth: 0 }).replace(/\n$/, "");
|
|
7398
|
+
return `${DELIMITER}\n${yaml}\n${DELIMITER}\n\n${normalizeDocBody(body)}\n`;
|
|
7399
|
+
};
|
|
7400
|
+
//#endregion
|
|
7401
|
+
//#region ../../packages/busabase-package/src/audit.ts
|
|
7402
|
+
/**
|
|
7403
|
+
* Is this package **fit to publish**?
|
|
7404
|
+
*
|
|
7405
|
+
* Deliberately a different question from {@link validateTemplate}, and the split is
|
|
7406
|
+
* the point rather than an organisational preference:
|
|
7407
|
+
*
|
|
7408
|
+
* - `validateTemplate` **classifies**. Its verdict changes what install *does* — a
|
|
7409
|
+
* Skill node, ownership stamps and a sample merge, versus a plain package install.
|
|
7410
|
+
* Four consumers must agree on it or the catalog lies about what an install does,
|
|
7411
|
+
* so it stays small and stable.
|
|
7412
|
+
* - `auditPackage` / `auditSkill` **advise**. Nothing behaves differently based on
|
|
7413
|
+
* their verdict; they exist for the author and the reviewer, and are free to grow.
|
|
7414
|
+
*
|
|
7415
|
+
* The practical consequence: a declared screenshot that is not on disk belongs here,
|
|
7416
|
+
* never in `validateTemplate`. Promoting it there would stop a template installing
|
|
7417
|
+
* over a missing image, which is the wrong lever entirely.
|
|
7418
|
+
*
|
|
7419
|
+
* Nothing here re-checks what `validateTemplate` already decides (declared resources,
|
|
7420
|
+
* the sample-row ceiling, name agreement between manifest and manual, an unambiguous
|
|
7421
|
+
* primary AirApp). Run both; they compose.
|
|
7422
|
+
*
|
|
7423
|
+
* Purely static: no install, no network, and none of the package's own code is
|
|
7424
|
+
* executed — safe to run in CI over a pull request from someone you do not know.
|
|
7425
|
+
*/
|
|
7426
|
+
/**
|
|
7427
|
+
* A materialized workspace id: busabase-core's `id()` is a three-letter prefix, the
|
|
7428
|
+
* creation time in base36, then seven random characters.
|
|
7429
|
+
*
|
|
7430
|
+
* The lookahead requiring a digit is what keeps ordinary words out — `recommendation`
|
|
7431
|
+
* has the right prefix and the wrong shape. A real id always carries its timestamp.
|
|
7432
|
+
*/
|
|
7433
|
+
const WORKSPACE_ID = /(?<![0-9a-z])(?:nod|bse|viw|rec|crq|ast|com|fom|shr|fvl|opr|cmt|rev|whd|aud)(?=[0-9a-z]{15}(?![0-9a-z]))(?=[0-9a-z]*\d)[0-9a-z]{15}/;
|
|
7434
|
+
const TEXT_EXTENSIONS = /\.(?:js|mjs|cjs|ts|mts|cts|json|ndjson|md|html|css|ya?ml|txt)$|(?:^|\/)\.env(?:\.|$)/;
|
|
7435
|
+
/**
|
|
7436
|
+
* Files that exist to *forbid* a string necessarily contain it. An AirApp's own
|
|
7437
|
+
* `scripts/check.mjs` asserts the obsolete API prefix is absent, and a test fixture
|
|
7438
|
+
* spells out the credential it proves gets rejected — scanning those turns a
|
|
7439
|
+
* correctly-guarded package into a failure.
|
|
7440
|
+
*/
|
|
7441
|
+
const IS_ASSERTION_CODE = /(?:^|\/)(?:scripts\/check\.mjs|tests?\/|[^/]*\.test\.[cm]?[jt]s)$/;
|
|
7442
|
+
/** Lockfiles carry integrity hashes that are not ours to interpret. */
|
|
7443
|
+
const NOT_SOURCE = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/;
|
|
7444
|
+
const relativePaths = (files, root = "") => {
|
|
7445
|
+
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
7446
|
+
const out = [];
|
|
7447
|
+
for (const key of files.keys()) if (!prefix || key.startsWith(prefix)) out.push(key.slice(prefix.length));
|
|
7448
|
+
return out;
|
|
7449
|
+
};
|
|
7450
|
+
const readText = (files, root, relative) => {
|
|
7451
|
+
const prefix = root ? `${root.replace(/\/+$/, "")}/` : "";
|
|
7452
|
+
return files.get(`${prefix}${relative}`)?.toString("utf8");
|
|
7453
|
+
};
|
|
7454
|
+
/**
|
|
7455
|
+
* Audit the package layer: identity, the assets the catalog promises, and anything
|
|
7456
|
+
* that must never be published.
|
|
7457
|
+
*
|
|
7458
|
+
* `files` is the flat map `readPackageTree` was given, not the tree. That is load-bearing:
|
|
7459
|
+
* `assets/screenshots/` holds no nodes and so never reaches the tree, and "the manifest
|
|
7460
|
+
* promises a screenshot that does not exist" is exactly a file-level question.
|
|
7461
|
+
*/
|
|
7462
|
+
const auditPackage = (tree, files, options = {}) => {
|
|
7463
|
+
const findings = [];
|
|
7464
|
+
const error = (rule, message) => findings.push({
|
|
7465
|
+
severity: "error",
|
|
7466
|
+
rule,
|
|
7467
|
+
message
|
|
7468
|
+
});
|
|
7469
|
+
const warn = (rule, message) => findings.push({
|
|
7470
|
+
severity: "warning",
|
|
7471
|
+
rule,
|
|
7472
|
+
message
|
|
7473
|
+
});
|
|
7474
|
+
const root = options.root ?? "";
|
|
7475
|
+
const paths = relativePaths(files, root);
|
|
7476
|
+
const present = new Set(paths);
|
|
7477
|
+
if (options.directoryName !== void 0 && options.directoryName !== tree.manifest.name) error("package/identity", `Directory is "${options.directoryName}" but busabase.json declares "${tree.manifest.name}". The directory name is the package's identity, not a label beside it.`);
|
|
7478
|
+
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.`);
|
|
7479
|
+
for (const relative of paths) {
|
|
7480
|
+
const base = relative.slice(relative.lastIndexOf("/") + 1);
|
|
7481
|
+
if (/^\.env(?!\.example$)(?:\.|$)/.test(base)) error("package/dotenv", `${relative} is part of the package. Credentials belong in Vault, never in something people install.`);
|
|
7482
|
+
if (/^(?:id_rsa|id_ed25519)$/.test(base) || /\.(?:pem|p12|pfx|keystore)$/.test(base)) error("package/key-material", `${relative} looks like key material and must not be published.`);
|
|
7483
|
+
if (/(?:^|\/)(?:node_modules|dist|build)\//.test(relative)) warn("package/build-output", `${relative} looks like installed or built output. Ship source; the installer builds nothing.`);
|
|
7484
|
+
}
|
|
7485
|
+
for (const relative of paths) {
|
|
7486
|
+
if (!TEXT_EXTENSIONS.test(relative)) continue;
|
|
7487
|
+
if (NOT_SOURCE.test(relative) || IS_ASSERTION_CODE.test(relative)) continue;
|
|
7488
|
+
const source = readText(files, root, relative);
|
|
7489
|
+
if (source === void 0) continue;
|
|
7490
|
+
const found = WORKSPACE_ID.exec(source);
|
|
7491
|
+
if (found) error("package/workspace-ids", `${relative} contains "${found[0]}", a materialized workspace id. Node, Base and Space ids are whoever last ran setup's runtime state — publishing them ships one person's workspace layout to everyone.`);
|
|
7492
|
+
}
|
|
7493
|
+
if (!tree.rootSkill) warn("template/no-skill-node", `This package carries no ${PACKAGE_SKILL_ENTRY}, so nothing installs a manual beside its resources. An agent working in the installed folder has nothing to read, and the person who installed it has nothing to point one at.`);
|
|
7494
|
+
const promptless = nodesWithoutAgentPrompts(tree.nodes);
|
|
7495
|
+
if (promptless.length > 0) warn("template/node-without-prompts", `${promptless.length} installed node(s) carry no agentPrompts (${promptless.slice(0, 5).join(", ")}${promptless.length > 5 ? ", …" : ""}). They will show their node type's generic prompts, so nothing tells the user what THIS app is for. Write scenario prompts per node — see \`busabase-cli nodes set-agent-prompts\`.`);
|
|
7496
|
+
return findings;
|
|
7497
|
+
};
|
|
7532
7498
|
/**
|
|
7533
|
-
*
|
|
7534
|
-
* memory) → the in-memory tree. Used by `install`, and by the round-trip tests.
|
|
7499
|
+
* Node slugs (dotted by folder) with no scenario prompts of their own.
|
|
7535
7500
|
*
|
|
7536
|
-
*
|
|
7537
|
-
*
|
|
7538
|
-
*
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7501
|
+
* Folders are exempt: a folder is a container, and the useful prompts hang off the
|
|
7502
|
+
* things inside it. Everything else is something a person opens and then has to
|
|
7503
|
+
* work out what to ask for.
|
|
7504
|
+
*/
|
|
7505
|
+
const nodesWithoutAgentPrompts = (nodes, prefix = "") => {
|
|
7506
|
+
const out = [];
|
|
7507
|
+
for (const node of nodes) {
|
|
7508
|
+
const path = `${prefix}${node.slug}`;
|
|
7509
|
+
if (node.type === "folder") {
|
|
7510
|
+
out.push(...nodesWithoutAgentPrompts(node.children, `${path}/`));
|
|
7511
|
+
continue;
|
|
7512
|
+
}
|
|
7513
|
+
if (!node.agentPrompts?.length) out.push(path);
|
|
7514
|
+
}
|
|
7515
|
+
return out;
|
|
7516
|
+
};
|
|
7517
|
+
/**
|
|
7518
|
+
* Audit the Skill layer: is the manual one an agent can actually act on?
|
|
7542
7519
|
*
|
|
7543
|
-
*
|
|
7544
|
-
*
|
|
7520
|
+
* Runs for any directory carrying a root `SKILL.md` — package or not, template or not.
|
|
7521
|
+
* It reports nothing at all when there is no manual: a package without one is a
|
|
7522
|
+
* perfectly legitimate thing to publish, not a Skill that failed.
|
|
7545
7523
|
*
|
|
7546
|
-
*
|
|
7547
|
-
*
|
|
7548
|
-
*
|
|
7549
|
-
*
|
|
7550
|
-
* what keeps a template byte-identical to a plain package below the root.
|
|
7524
|
+
* The honest limit: a checker can tell you the frontmatter parses and the files the
|
|
7525
|
+
* manual points at exist. It cannot tell you the manual is *true*. Everything in
|
|
7526
|
+
* `SKILL.md` becomes instructions an agent acts on, and no static rule catches a
|
|
7527
|
+
* confident, wrong sentence about what a table means.
|
|
7551
7528
|
*/
|
|
7529
|
+
const auditSkill = (files, options = {}) => {
|
|
7530
|
+
const findings = [];
|
|
7531
|
+
const error = (rule, message) => findings.push({
|
|
7532
|
+
severity: "error",
|
|
7533
|
+
rule,
|
|
7534
|
+
message
|
|
7535
|
+
});
|
|
7536
|
+
const warn = (rule, message) => findings.push({
|
|
7537
|
+
severity: "warning",
|
|
7538
|
+
rule,
|
|
7539
|
+
message
|
|
7540
|
+
});
|
|
7541
|
+
const root = options.root ?? "";
|
|
7542
|
+
const source = readText(files, root, PACKAGE_SKILL_ENTRY);
|
|
7543
|
+
if (source === void 0) return findings;
|
|
7544
|
+
let body = "";
|
|
7545
|
+
try {
|
|
7546
|
+
const parsed = parseFrontmatter(source, PACKAGE_SKILL_ENTRY);
|
|
7547
|
+
body = parsed.body;
|
|
7548
|
+
const frontmatter = SkillFrontmatterSchema$1.safeParse(parsed.data);
|
|
7549
|
+
if (!frontmatter.success) error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY} frontmatter is not a valid Skill declaration: ${frontmatter.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ")}`);
|
|
7550
|
+
else if (frontmatter.data.description.trim() === "") error("skill/description", `${PACKAGE_SKILL_ENTRY} has no description. An agent chooses whether to reach for a Skill by reading it, so an empty one is not cosmetic — it is a Skill that never gets picked.`);
|
|
7551
|
+
} catch (cause) {
|
|
7552
|
+
error("skill/frontmatter", `${PACKAGE_SKILL_ENTRY}: ${cause.message}`);
|
|
7553
|
+
return findings;
|
|
7554
|
+
}
|
|
7555
|
+
const todos = (body.match(/^.*\bTODO\b.*$/gm) ?? []).length;
|
|
7556
|
+
if (todos > 0) error("skill/todo", `${PACKAGE_SKILL_ENTRY} still carries ${todos} TODO line(s). The export draft leaves them on purpose — an agent acts on what this file says, and a blank the author was meant to fill in is worse than nothing once it ships.`);
|
|
7557
|
+
const present = new Set(relativePaths(files, root));
|
|
7558
|
+
const sidecar = new RegExp(`\\]\\((\\.?/?(?:${PACKAGE_SIDECARS})/[^)\\s]+)\\)|\`((?:${PACKAGE_SIDECARS})/[^\`\\s]+)\``, "g");
|
|
7559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7560
|
+
for (const match of body.matchAll(sidecar)) {
|
|
7561
|
+
const target = (match[1] ?? match[2]).replace(/^\.?\//, "");
|
|
7562
|
+
if (seen.has(target)) continue;
|
|
7563
|
+
seen.add(target);
|
|
7564
|
+
if (!present.has(target)) warn("skill/missing-reference", `${PACKAGE_SKILL_ENTRY} points at "${target}", which the package does not ship. An agent told to read it has an instruction it cannot satisfy.`);
|
|
7565
|
+
}
|
|
7566
|
+
return findings;
|
|
7567
|
+
};
|
|
7568
|
+
const PACKAGE_SIDECARS = PACKAGE_SKILL_SIDECAR_DIRS.join("|");
|
|
7569
|
+
//#endregion
|
|
7570
|
+
//#region ../../packages/busabase-package/src/ignore.ts
|
|
7571
|
+
const escapeLiteral = (value) => value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7572
|
+
/**
|
|
7573
|
+
* Translate one gitignore-ish pattern into a regex over a POSIX relative path.
|
|
7574
|
+
*
|
|
7575
|
+
* The three globs are handled in one pass so that `**` is recognised before the
|
|
7576
|
+
* `*` it starts with — splitting on `*` first would turn `**` into two
|
|
7577
|
+
* segment-local wildcards and quietly stop matching across directories.
|
|
7578
|
+
*/
|
|
7579
|
+
const patternToRegex = (pattern, anchored) => {
|
|
7580
|
+
let source = "";
|
|
7581
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
7582
|
+
const char = pattern[index];
|
|
7583
|
+
if (char === "*") {
|
|
7584
|
+
if (pattern[index + 1] === "*") {
|
|
7585
|
+
index += 1;
|
|
7586
|
+
if (pattern[index + 1] === "/") {
|
|
7587
|
+
index += 1;
|
|
7588
|
+
source += "(?:.*/)?";
|
|
7589
|
+
} else source += ".*";
|
|
7590
|
+
} else source += "[^/]*";
|
|
7591
|
+
continue;
|
|
7592
|
+
}
|
|
7593
|
+
if (char === "?") {
|
|
7594
|
+
source += "[^/]";
|
|
7595
|
+
continue;
|
|
7596
|
+
}
|
|
7597
|
+
if (char === "/") {
|
|
7598
|
+
source += "/";
|
|
7599
|
+
continue;
|
|
7600
|
+
}
|
|
7601
|
+
source += escapeLiteral(char);
|
|
7602
|
+
}
|
|
7603
|
+
return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${source}(?:/.*)?$`);
|
|
7604
|
+
};
|
|
7605
|
+
const parseRule = (line) => {
|
|
7606
|
+
let pattern = line.trim();
|
|
7607
|
+
if (!pattern || pattern.startsWith("#")) return void 0;
|
|
7608
|
+
const negated = pattern.startsWith("!");
|
|
7609
|
+
if (negated) pattern = pattern.slice(1);
|
|
7610
|
+
const directoryOnly = pattern.endsWith("/");
|
|
7611
|
+
if (directoryOnly) pattern = pattern.slice(0, -1);
|
|
7612
|
+
const anchored = pattern.startsWith("/");
|
|
7613
|
+
if (anchored) pattern = pattern.slice(1);
|
|
7614
|
+
if (!pattern) return void 0;
|
|
7615
|
+
return {
|
|
7616
|
+
regex: patternToRegex(pattern, anchored),
|
|
7617
|
+
negated,
|
|
7618
|
+
directoryOnly
|
|
7619
|
+
};
|
|
7620
|
+
};
|
|
7621
|
+
const parseIgnoreFile = (contents) => {
|
|
7622
|
+
const rules = contents.split(/\r?\n/).map(parseRule).filter((rule) => rule !== void 0);
|
|
7623
|
+
return {
|
|
7624
|
+
isEmpty: rules.length === 0,
|
|
7625
|
+
ignores: (relativePath) => {
|
|
7626
|
+
let ignored = false;
|
|
7627
|
+
for (const rule of rules) {
|
|
7628
|
+
if (rule.directoryOnly && !rule.regex.test(`${relativePath}/`)) continue;
|
|
7629
|
+
if (!rule.regex.test(relativePath)) continue;
|
|
7630
|
+
ignored = !rule.negated;
|
|
7631
|
+
}
|
|
7632
|
+
return ignored;
|
|
7633
|
+
}
|
|
7634
|
+
};
|
|
7635
|
+
};
|
|
7636
|
+
/** A matcher that installs everything — used when no ignore file is present. */
|
|
7637
|
+
const IGNORE_NOTHING = {
|
|
7638
|
+
ignores: () => false,
|
|
7639
|
+
isEmpty: true
|
|
7640
|
+
};
|
|
7641
|
+
//#endregion
|
|
7642
|
+
//#region ../../packages/busabase-package/src/layout-read.ts
|
|
7552
7643
|
const decodeText = (bytes, filePath) => {
|
|
7553
7644
|
const text = bytes.toString("utf8");
|
|
7554
7645
|
if (text.includes("�")) throw new Error(`${filePath} is not valid UTF-8 text.`);
|
|
@@ -7603,6 +7694,20 @@ const zodParse = (schema, value, filePath) => {
|
|
|
7603
7694
|
}
|
|
7604
7695
|
return result.data;
|
|
7605
7696
|
};
|
|
7697
|
+
/**
|
|
7698
|
+
* A sidecar's `agentPrompts` as the tree carries it — as a spread, so a node that
|
|
7699
|
+
* has none carries no `agentPrompts` KEY at all rather than one set to `undefined`.
|
|
7700
|
+
*
|
|
7701
|
+
* `[]` and "absent" mean the same thing on disk (the node falls back to its type's
|
|
7702
|
+
* default prompts either way), so an empty list is dropped rather than
|
|
7703
|
+
* round-tripping as a sidecar key that changes nothing.
|
|
7704
|
+
*/
|
|
7705
|
+
/** `base.json` minus the node-level key, so the list lives in exactly one place. */
|
|
7706
|
+
const baseWithoutAgentPrompts = (base) => {
|
|
7707
|
+
const { agentPrompts: _agentPrompts, ...rest } = base;
|
|
7708
|
+
return rest;
|
|
7709
|
+
};
|
|
7710
|
+
const agentPromptsField = (prompts) => prompts?.length ? { agentPrompts: prompts } : {};
|
|
7606
7711
|
const listDir = (files, dir) => {
|
|
7607
7712
|
const fileNames = /* @__PURE__ */ new Set();
|
|
7608
7713
|
const dirNames = /* @__PURE__ */ new Set();
|
|
@@ -7646,7 +7751,7 @@ const readPackageTree = (files, options = {}) => {
|
|
|
7646
7751
|
const manifest = zodParse(PackageManifestSchema, rawManifest, manifestPath);
|
|
7647
7752
|
const contentDir = `${root}${PACKAGE_CONTENT_DIRNAME}/`;
|
|
7648
7753
|
const nodes = readNodes(files, contentDir);
|
|
7649
|
-
const rootSkill = readRootSkill(files, root, manifest.name, manifest.description);
|
|
7754
|
+
const rootSkill = readRootSkill(files, root, manifest.name, iStringParse(manifest.description, "en"));
|
|
7650
7755
|
return {
|
|
7651
7756
|
manifest,
|
|
7652
7757
|
nodes,
|
|
@@ -7775,6 +7880,7 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7775
7880
|
name: meta.name,
|
|
7776
7881
|
description: meta.description,
|
|
7777
7882
|
position: meta.position,
|
|
7883
|
+
...agentPromptsField(meta.agentPrompts),
|
|
7778
7884
|
files: entries
|
|
7779
7885
|
};
|
|
7780
7886
|
}
|
|
@@ -7788,7 +7894,8 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7788
7894
|
name: base.name,
|
|
7789
7895
|
description: base.description,
|
|
7790
7896
|
position: base.position,
|
|
7791
|
-
base,
|
|
7897
|
+
...agentPromptsField(base.agentPrompts),
|
|
7898
|
+
base: baseWithoutAgentPrompts(base),
|
|
7792
7899
|
records
|
|
7793
7900
|
};
|
|
7794
7901
|
}
|
|
@@ -7796,7 +7903,8 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7796
7903
|
const meta = files.has(metaPath) ? zodParse(PackageFolderMetaSchema, parseJsonFile(files, metaPath), metaPath) : {
|
|
7797
7904
|
name: void 0,
|
|
7798
7905
|
description: "",
|
|
7799
|
-
position: void 0
|
|
7906
|
+
position: void 0,
|
|
7907
|
+
agentPrompts: void 0
|
|
7800
7908
|
};
|
|
7801
7909
|
return {
|
|
7802
7910
|
type: "folder",
|
|
@@ -7804,6 +7912,7 @@ const readDirNode = (files, dir, slug) => {
|
|
|
7804
7912
|
name: meta.name ?? humanizeSlug(slug),
|
|
7805
7913
|
description: meta.description,
|
|
7806
7914
|
position: meta.position,
|
|
7915
|
+
...agentPromptsField(meta.agentPrompts),
|
|
7807
7916
|
children: readNodes(files, dir)
|
|
7808
7917
|
};
|
|
7809
7918
|
};
|
|
@@ -7854,7 +7963,8 @@ const readFileNode = (files, dir, fileName) => {
|
|
|
7854
7963
|
const meta = files.has(sidecarPath) ? zodParse(PackageFileNodeMetaSchema, parseJsonFile(files, sidecarPath), sidecarPath) : {
|
|
7855
7964
|
name: void 0,
|
|
7856
7965
|
description: "",
|
|
7857
|
-
position: void 0
|
|
7966
|
+
position: void 0,
|
|
7967
|
+
agentPrompts: void 0
|
|
7858
7968
|
};
|
|
7859
7969
|
return {
|
|
7860
7970
|
type: "file",
|
|
@@ -7862,6 +7972,7 @@ const readFileNode = (files, dir, fileName) => {
|
|
|
7862
7972
|
name: meta.name ?? fileName,
|
|
7863
7973
|
description: meta.description,
|
|
7864
7974
|
position: meta.position,
|
|
7975
|
+
...agentPromptsField(meta.agentPrompts),
|
|
7865
7976
|
fileName,
|
|
7866
7977
|
mimeType: guessMimeType$1(fileName),
|
|
7867
7978
|
bytes
|
|
@@ -7882,6 +7993,7 @@ const readDocNode = (bytes, dir, fileName) => {
|
|
|
7882
7993
|
name: frontmatter.name,
|
|
7883
7994
|
description: frontmatter.description,
|
|
7884
7995
|
position: frontmatter.position,
|
|
7996
|
+
...agentPromptsField(frontmatter.agentPrompts),
|
|
7885
7997
|
body
|
|
7886
7998
|
};
|
|
7887
7999
|
};
|
|
@@ -7958,6 +8070,8 @@ const validateTemplate = (tree) => {
|
|
|
7958
8070
|
if (skill && skill.name !== tree.manifest.name) errors.push(`Name mismatch: ${PACKAGE_SKILL_ENTRY} declares "${skill.name}" but busabase.json declares "${tree.manifest.name}". They are the same app and must agree.`);
|
|
7959
8071
|
const bases = baseSlugs(tree);
|
|
7960
8072
|
for (const key of busabaseMeta?.resources ?? []) if (!bases.has(key)) errors.push(`${PACKAGE_SKILL_ENTRY} lists resource "${key}", but content/ has no Base with that slug. An agent told about a table that does not exist will write to the wrong one.`);
|
|
8073
|
+
const risk = parseTemplateRisk(busabaseMeta?.risk);
|
|
8074
|
+
if (busabaseMeta?.risk !== void 0 && risk === void 0) warnings.push(`metadata.busabase.risk is "${busabaseMeta.risk}", which is not a recognized level (gated-write, local-write, read-only, sandbox). The Template Center will show it as undeclared.`);
|
|
7961
8075
|
const airapps = airAppSlugs(tree);
|
|
7962
8076
|
const resolved = resolvePrimaryAirApp(template, airapps);
|
|
7963
8077
|
let primaryAirApp;
|
|
@@ -7994,7 +8108,8 @@ const validateTemplate = (tree) => {
|
|
|
7994
8108
|
errors,
|
|
7995
8109
|
warnings,
|
|
7996
8110
|
...primaryAirApp ? { primaryAirApp } : {},
|
|
7997
|
-
...template ? { template } : {}
|
|
8111
|
+
...template ? { template } : {},
|
|
8112
|
+
...risk ? { risk } : {}
|
|
7998
8113
|
};
|
|
7999
8114
|
};
|
|
8000
8115
|
/**
|
|
@@ -8014,7 +8129,7 @@ const deriveSkillDraft = (tree) => {
|
|
|
8014
8129
|
const bases = nodes.filter((node) => node.type === "base");
|
|
8015
8130
|
const airapps = nodes.filter(isAirApp);
|
|
8016
8131
|
const name = tree.manifest.name;
|
|
8017
|
-
const description = tree.manifest.description || `TODO: one line on what ${name} does.`;
|
|
8132
|
+
const description = iStringParse(tree.manifest.description, "en") || `TODO: one line on what ${name} does.`;
|
|
8018
8133
|
const yamlString = (value) => JSON.stringify(value);
|
|
8019
8134
|
const resourceLines = bases.map((node) => ` - ${yamlString(node.slug)}`).join("\n");
|
|
8020
8135
|
const resourceDocs = bases.map((node) => {
|
|
@@ -8496,7 +8611,7 @@ const applyInstall = async (client, plan, options) => {
|
|
|
8496
8611
|
parentNodeId: void 0,
|
|
8497
8612
|
slug: plan.targetFolderSlug,
|
|
8498
8613
|
name: plan.tree.manifest.name,
|
|
8499
|
-
description: plan.tree.manifest.description,
|
|
8614
|
+
description: iStringParse(plan.tree.manifest.description, "en"),
|
|
8500
8615
|
submittedBy: options.submittedBy,
|
|
8501
8616
|
metadata: app?.rootMetadata
|
|
8502
8617
|
});
|
|
@@ -8728,6 +8843,37 @@ const stampResource = async (client, app, nodeId, installedSlug, state, extra =
|
|
|
8728
8843
|
}
|
|
8729
8844
|
};
|
|
8730
8845
|
/**
|
|
8846
|
+
* Write a node's carried scenario prompts onto the node install just created.
|
|
8847
|
+
*
|
|
8848
|
+
* After creation rather than inline, because the prompts live in their own
|
|
8849
|
+
* column with their own route — no create endpoint accepts them. That has one
|
|
8850
|
+
* visible consequence, and it is the reason for the warning below: on the
|
|
8851
|
+
* review-first path a Doc or File node is a PENDING change request with no node
|
|
8852
|
+
* id yet, so there is nothing to write them to. Structure (folders, Bases,
|
|
8853
|
+
* Skills/AirApps/Drives) is always materialized (see {@link createBase}), so the
|
|
8854
|
+
* nodes that carry most prompts are unaffected.
|
|
8855
|
+
*
|
|
8856
|
+
* Never fails the install, for the same reason {@link stampResource} does not:
|
|
8857
|
+
* the resources are already there and useful, and prompts can be re-applied with
|
|
8858
|
+
* `busabase-cli nodes set-agent-prompts` at any time.
|
|
8859
|
+
*/
|
|
8860
|
+
const applyAgentPrompts = async (client, nodeId, node, state) => {
|
|
8861
|
+
const agentPrompts = node.agentPrompts;
|
|
8862
|
+
if (!agentPrompts?.length) return;
|
|
8863
|
+
if (!nodeId) {
|
|
8864
|
+
state.warnings.push(`"${node.slug}" installs as a change request, so its ${agentPrompts.length} agent prompt(s) were not applied. After merging it, run \`busabase-cli nodes set-agent-prompts\` for that node, or re-install with --auto-merge.`);
|
|
8865
|
+
return;
|
|
8866
|
+
}
|
|
8867
|
+
try {
|
|
8868
|
+
await client.nodes.updateAgentPrompts({
|
|
8869
|
+
nodeId,
|
|
8870
|
+
agentPrompts
|
|
8871
|
+
});
|
|
8872
|
+
} catch (error) {
|
|
8873
|
+
state.warnings.push(`Could not set agent prompts on "${node.slug}": ${error.message}. The node installed fine; it will show its node type's default prompts.`);
|
|
8874
|
+
}
|
|
8875
|
+
};
|
|
8876
|
+
/**
|
|
8731
8877
|
* Re-create the package's carried Doc images on THIS host and return the
|
|
8732
8878
|
* old id → new id map the Doc bodies are rewritten through.
|
|
8733
8879
|
*
|
|
@@ -8761,21 +8907,24 @@ const createStructure = async (client, nodes, parentNodeId, options, state, base
|
|
|
8761
8907
|
submittedBy: options.submittedBy
|
|
8762
8908
|
});
|
|
8763
8909
|
state.created.folders++;
|
|
8910
|
+
await applyAgentPrompts(client, nodeId, node, state);
|
|
8764
8911
|
await createStructure(client, node.children, nodeId, options, state, bases, fieldIds, docAssetIds, app);
|
|
8765
8912
|
break;
|
|
8766
8913
|
}
|
|
8767
8914
|
case "doc": {
|
|
8768
8915
|
const warning = reportUnresolvableDocAssets(node.slug, node.body, new Set(docAssetIds.keys()));
|
|
8769
8916
|
if (warning) state.warnings.push(warning);
|
|
8770
|
-
|
|
8917
|
+
const result = await client.docs.create({
|
|
8771
8918
|
parentNodeId,
|
|
8772
8919
|
slug: node.slug,
|
|
8773
8920
|
name: node.name,
|
|
8774
8921
|
description: node.description,
|
|
8775
8922
|
body: rewriteDocAssetIds(node.body, docAssetIds),
|
|
8776
8923
|
autoMerge: options.autoMerge
|
|
8777
|
-
})
|
|
8924
|
+
});
|
|
8925
|
+
if (result.materialized) state.created.docs++;
|
|
8778
8926
|
else state.pendingChangeRequests++;
|
|
8927
|
+
await applyAgentPrompts(client, result.materialized ? result.node.id : void 0, node, state);
|
|
8779
8928
|
break;
|
|
8780
8929
|
}
|
|
8781
8930
|
case "base":
|
|
@@ -8794,15 +8943,17 @@ const createStructure = async (client, nodes, parentNodeId, options, state, base
|
|
|
8794
8943
|
state.warnings.push(asSkippableUpload(error, node.fileName));
|
|
8795
8944
|
break;
|
|
8796
8945
|
}
|
|
8797
|
-
|
|
8946
|
+
const result = await client.files.create({
|
|
8798
8947
|
parentNodeId,
|
|
8799
8948
|
slug: node.slug,
|
|
8800
8949
|
name: node.name,
|
|
8801
8950
|
description: node.description,
|
|
8802
8951
|
assetId: asset.assetId,
|
|
8803
8952
|
autoMerge: options.autoMerge
|
|
8804
|
-
})
|
|
8953
|
+
});
|
|
8954
|
+
if (result.materialized) state.created.files++;
|
|
8805
8955
|
else state.pendingChangeRequests++;
|
|
8956
|
+
await applyAgentPrompts(client, result.materialized ? result.node.id : void 0, node, state);
|
|
8806
8957
|
break;
|
|
8807
8958
|
}
|
|
8808
8959
|
}
|
|
@@ -8854,6 +9005,7 @@ const createBase = async (client, node, parentNodeId, options, state, bases, fie
|
|
|
8854
9005
|
});
|
|
8855
9006
|
indexFields(fieldIds, node.slug, result.fields);
|
|
8856
9007
|
await stampResource(client, app, result.nodeId, node.slug, state);
|
|
9008
|
+
await applyAgentPrompts(client, result.nodeId, node, state);
|
|
8857
9009
|
for (const view of node.base.views) {
|
|
8858
9010
|
if (!(await client.views.changeRequest({
|
|
8859
9011
|
operation: "create",
|
|
@@ -8888,7 +9040,7 @@ const createFileTreeNode = async (client, node, parentNodeId, options, state, ap
|
|
|
8888
9040
|
return;
|
|
8889
9041
|
}
|
|
8890
9042
|
}))).filter((entry) => entry !== void 0);
|
|
8891
|
-
|
|
9043
|
+
const result = await client.fileTrees.create({
|
|
8892
9044
|
type: node.type,
|
|
8893
9045
|
parentNodeId,
|
|
8894
9046
|
slug: node.slug,
|
|
@@ -8898,10 +9050,12 @@ const createFileTreeNode = async (client, node, parentNodeId, options, state, ap
|
|
|
8898
9050
|
...app ? { metadata: resourceStamp(app, node.slug, extraMetadata) } : {},
|
|
8899
9051
|
mergeMode: "replace",
|
|
8900
9052
|
autoMerge: options.autoMerge
|
|
8901
|
-
})
|
|
9053
|
+
});
|
|
9054
|
+
if (result.materialized) {
|
|
8902
9055
|
state.created.fileTreeNodes++;
|
|
8903
9056
|
state.created.files += node.files.length;
|
|
8904
9057
|
} else state.pendingChangeRequests++;
|
|
9058
|
+
await applyAgentPrompts(client, result.materialized ? result.node.id : void 0, node, state);
|
|
8905
9059
|
};
|
|
8906
9060
|
/**
|
|
8907
9061
|
* Thrown when the host can't accept an upload the normal way — see
|
|
@@ -9143,15 +9297,6 @@ const approveAndMerge = async (client, changeRequestId) => {
|
|
|
9143
9297
|
};
|
|
9144
9298
|
//#endregion
|
|
9145
9299
|
//#region ../../packages/busabase-package/src/collect.ts
|
|
9146
|
-
/**
|
|
9147
|
-
* `export`'s read half: a space subtree → the in-memory package tree, entirely via
|
|
9148
|
-
* the public read API. The inverse of `apply`.
|
|
9149
|
-
*
|
|
9150
|
-
* Rewrites the three id-bearing option keys back to slug references, drops everything
|
|
9151
|
-
* the format has no slot for (ids, history, permissions), carries the bytes behind
|
|
9152
|
-
* every Doc's inline images (see {@link collectDocAssets}), and enforces the format's
|
|
9153
|
-
* self-containment rule: a relation may not leave the package.
|
|
9154
|
-
*/
|
|
9155
9300
|
/** Find the subtree to export by node slug or id. */
|
|
9156
9301
|
const findSourceNode = (nodes, slugOrId) => {
|
|
9157
9302
|
const stack = [...nodes];
|
|
@@ -9168,7 +9313,8 @@ const collectPackageTree = async (client, root, options) => {
|
|
|
9168
9313
|
const { skillSource, rest } = partitionTemplateSkill(children);
|
|
9169
9314
|
const collectOptions = {
|
|
9170
9315
|
...options,
|
|
9171
|
-
baseResourceKeys: collectBaseResourceKeys(children)
|
|
9316
|
+
baseResourceKeys: collectBaseResourceKeys(children),
|
|
9317
|
+
agentPromptsUnavailable: options.agentPromptsUnavailable ?? { value: false }
|
|
9172
9318
|
};
|
|
9173
9319
|
const nodes = await collectNodes(client, rest, collectOptions);
|
|
9174
9320
|
const tree = {
|
|
@@ -9312,12 +9458,36 @@ const collectNodes = async (client, sources, options) => {
|
|
|
9312
9458
|
}
|
|
9313
9459
|
return sortNodes(nodes);
|
|
9314
9460
|
};
|
|
9461
|
+
/**
|
|
9462
|
+
* This node's scenario Agent prompts, read from the column they live in.
|
|
9463
|
+
*
|
|
9464
|
+
* A separate read per node because that is the only way to get them: they are
|
|
9465
|
+
* deliberately absent from `nodes.list` (50 prompts x 8 KiB per locale is why
|
|
9466
|
+
* they have their own column and their own route).
|
|
9467
|
+
*
|
|
9468
|
+
* Never fatal. A server that predates the route, or a node this key may read but
|
|
9469
|
+
* whose prompts it may not, costs the export those prompts and nothing else —
|
|
9470
|
+
* the package still installs, and its nodes fall back to their type's defaults,
|
|
9471
|
+
* exactly as every package did before the field existed.
|
|
9472
|
+
*/
|
|
9473
|
+
const collectAgentPrompts = async (client, source, options) => {
|
|
9474
|
+
if (options.agentPromptsUnavailable?.value) return void 0;
|
|
9475
|
+
try {
|
|
9476
|
+
const result = await client.nodes.getAgentPrompts({ nodeId: source.id });
|
|
9477
|
+
return result.agentPrompts?.length ? result.agentPrompts : void 0;
|
|
9478
|
+
} catch (error) {
|
|
9479
|
+
if (options.agentPromptsUnavailable) options.agentPromptsUnavailable.value = true;
|
|
9480
|
+
options.warn(`Could not read agent prompts for "${source.slug}": ${error.message}. Exported without them for this node and every node after it; the installed nodes will show their node type's default prompts.`);
|
|
9481
|
+
return;
|
|
9482
|
+
}
|
|
9483
|
+
};
|
|
9315
9484
|
const collectNode = async (client, source, options) => {
|
|
9316
9485
|
const common = {
|
|
9317
9486
|
slug: restoredNodeSlug(source),
|
|
9318
9487
|
name: source.name,
|
|
9319
9488
|
description: source.description ?? "",
|
|
9320
|
-
position: source.position
|
|
9489
|
+
position: source.position,
|
|
9490
|
+
agentPrompts: await collectAgentPrompts(client, source, options)
|
|
9321
9491
|
};
|
|
9322
9492
|
switch (source.type) {
|
|
9323
9493
|
case "folder": return {
|
|
@@ -9919,7 +10089,8 @@ const countTree = (tree) => {
|
|
|
9919
10089
|
const renderPlan = (plan) => {
|
|
9920
10090
|
const lines = [];
|
|
9921
10091
|
lines.push(`Package: ${plan.tree.manifest.name}`);
|
|
9922
|
-
|
|
10092
|
+
const description = iStringParse(plan.tree.manifest.description, "en");
|
|
10093
|
+
if (description) lines.push(` ${description}`);
|
|
9923
10094
|
lines.push("");
|
|
9924
10095
|
lines.push(`Target folder: ${plan.targetFolderSlug}`);
|
|
9925
10096
|
lines.push("");
|
|
@@ -10006,25 +10177,6 @@ const assertPlanIsApplicable = (plan, autoMerge) => {
|
|
|
10006
10177
|
//#endregion
|
|
10007
10178
|
//#region ../../packages/busabase-package/src/discover.ts
|
|
10008
10179
|
/**
|
|
10009
|
-
* What is at the end of this URL — one package, one template, or a repository
|
|
10010
|
-
* full of them?
|
|
10011
|
-
*
|
|
10012
|
-
* A user pasting a GitHub URL does not think in terms of "package roots". They
|
|
10013
|
-
* paste the repo they were looking at. Sometimes that repo IS the package;
|
|
10014
|
-
* sometimes it is `busabase/skills`, which holds dozens under `skills/*`. Before
|
|
10015
|
-
* this, the second case was only ever an error message with a list of
|
|
10016
|
-
* subdirectories to paste instead — correct, and a dead end for anyone who did
|
|
10017
|
-
* not want to edit a URL by hand.
|
|
10018
|
-
*
|
|
10019
|
-
* Discovery answers it once for every caller: the CLI turns the result into a
|
|
10020
|
-
* pick-one list, the dashboard's install dialog into a chooser, the Template
|
|
10021
|
-
* Center's index CI into the set of cards to publish. All three must agree on
|
|
10022
|
-
* what counts as a template, which is why the verdict comes from the same
|
|
10023
|
-
* `validateTemplate` they would each otherwise re-implement.
|
|
10024
|
-
*
|
|
10025
|
-
* Spec: `apps/busabase/content/spec/template-center.md` §6.2a.
|
|
10026
|
-
*/
|
|
10027
|
-
/**
|
|
10028
10180
|
* Only one level deep, matching the `skills/<name>/` layout the skills CLI
|
|
10029
10181
|
* already established and the Template Center reads. Walking arbitrarily deep
|
|
10030
10182
|
* would turn one paste into an unbounded scan of a monorepo and surface vendored
|
|
@@ -10074,6 +10226,7 @@ const discoverPackages = (files) => {
|
|
|
10074
10226
|
counts: countTree(tree),
|
|
10075
10227
|
...validation.primaryAirApp ? { primaryAirApp: validation.primaryAirApp } : {},
|
|
10076
10228
|
...tree.manifest.template?.category ? { category: tree.manifest.template.category } : {},
|
|
10229
|
+
...validation.risk ? { risk: validation.risk } : {},
|
|
10077
10230
|
screenshots: tree.manifest.template?.screenshots ?? [],
|
|
10078
10231
|
tags: [.../* @__PURE__ */ new Set([...tree.manifest.tags, ...tree.manifest.template?.tags ?? []])],
|
|
10079
10232
|
agentPrompts: tree.manifest.template?.agentPrompts ?? [],
|
|
@@ -10290,22 +10443,6 @@ const fetchGithubPackageFiles = async (repoUrl, options = {}) => {
|
|
|
10290
10443
|
};
|
|
10291
10444
|
//#endregion
|
|
10292
10445
|
//#region ../../packages/busabase-package/src/index-build.ts
|
|
10293
|
-
/**
|
|
10294
|
-
* Build the Template Center's catalog from a directory of skills.
|
|
10295
|
-
*
|
|
10296
|
-
* The catalog is not hand-maintained. A card that says "6 tables" for a package
|
|
10297
|
-
* that installs five is a trust bug, not a typo — so every field here is read
|
|
10298
|
-
* off the package itself, and whether an entry appears at all is decided by the
|
|
10299
|
-
* SAME `validateTemplate` the installer uses. A skill that would install as a
|
|
10300
|
-
* plain package can never be listed as a template, because one function answers
|
|
10301
|
-
* that question for both.
|
|
10302
|
-
*
|
|
10303
|
-
* Deliberately a plain function over a file map rather than something that
|
|
10304
|
-
* walks a disk: the CLI hands it a checkout, and a future CI job could hand it
|
|
10305
|
-
* a fetched zipball, without either needing its own notion of what counts.
|
|
10306
|
-
*
|
|
10307
|
-
* Spec: `apps/busabase/content/spec/template-center.md` §6.4.
|
|
10308
|
-
*/
|
|
10309
10446
|
/** Bumped when a consumer would misread an older file. */
|
|
10310
10447
|
const TEMPLATE_INDEX_FORMAT = "busabase-template-index@1";
|
|
10311
10448
|
const toEntry = (found, airapps) => ({
|
|
@@ -10313,6 +10450,7 @@ const toEntry = (found, airapps) => ({
|
|
|
10313
10450
|
name: found.name,
|
|
10314
10451
|
description: found.description,
|
|
10315
10452
|
category: found.category ?? "uncategorized",
|
|
10453
|
+
...found.risk ? { risk: found.risk } : {},
|
|
10316
10454
|
tags: found.tags ?? [],
|
|
10317
10455
|
screenshots: found.screenshots,
|
|
10318
10456
|
agentPrompts: found.agentPrompts ?? [],
|
|
@@ -10420,11 +10558,12 @@ const serializeView = (view) => compact({
|
|
|
10420
10558
|
fieldWidths: view.config.fieldWidths
|
|
10421
10559
|
})
|
|
10422
10560
|
});
|
|
10423
|
-
const serializeBase = (base, position) => toJsonFile(compact({
|
|
10561
|
+
const serializeBase = (base, position, agentPrompts) => toJsonFile(compact({
|
|
10424
10562
|
name: base.name,
|
|
10425
10563
|
description: base.description || void 0,
|
|
10426
10564
|
position,
|
|
10427
10565
|
reviewPolicy: base.reviewPolicy,
|
|
10566
|
+
agentPrompts: serializeAgentPrompts(agentPrompts),
|
|
10428
10567
|
fields: base.fields.map(serializeField),
|
|
10429
10568
|
views: base.views.map(serializeView)
|
|
10430
10569
|
}));
|
|
@@ -10498,10 +10637,11 @@ const renderNodes = (nodes, dir, files) => {
|
|
|
10498
10637
|
const renderNode = (node, dir, files) => {
|
|
10499
10638
|
switch (node.type) {
|
|
10500
10639
|
case "folder":
|
|
10501
|
-
if (node.children.length === 0 || node.description !== "" || node.position !== void 0 || node.name !== void 0) files.set(`${dir}${node.slug}/${PACKAGE_FOLDER_META_FILENAME}`, toJsonFile(compact({
|
|
10640
|
+
if (node.children.length === 0 || node.description !== "" || node.position !== void 0 || node.agentPrompts !== void 0 || node.name !== void 0) files.set(`${dir}${node.slug}/${PACKAGE_FOLDER_META_FILENAME}`, toJsonFile(compact({
|
|
10502
10641
|
name: node.name,
|
|
10503
10642
|
description: node.description || void 0,
|
|
10504
|
-
position: node.position
|
|
10643
|
+
position: node.position,
|
|
10644
|
+
agentPrompts: serializeAgentPrompts(node.agentPrompts)
|
|
10505
10645
|
})));
|
|
10506
10646
|
renderNodes(node.children, `${dir}${node.slug}/`, files);
|
|
10507
10647
|
return;
|
|
@@ -10509,11 +10649,12 @@ const renderNode = (node, dir, files) => {
|
|
|
10509
10649
|
files.set(`${dir}${node.slug}.md`, Buffer.from(serializeDoc({
|
|
10510
10650
|
name: node.name,
|
|
10511
10651
|
description: node.description,
|
|
10512
|
-
position: node.position
|
|
10652
|
+
position: node.position,
|
|
10653
|
+
agentPrompts: node.agentPrompts
|
|
10513
10654
|
}, node.body), "utf8"));
|
|
10514
10655
|
return;
|
|
10515
10656
|
case "base": {
|
|
10516
|
-
files.set(`${dir}${node.slug}/${PACKAGE_BASE_FILENAME}`, serializeBase(node.base, node.position));
|
|
10657
|
+
files.set(`${dir}${node.slug}/${PACKAGE_BASE_FILENAME}`, serializeBase(node.base, node.position, node.agentPrompts));
|
|
10517
10658
|
const records = serializeRecords(node.records);
|
|
10518
10659
|
if (records.byteLength > 0) files.set(`${dir}${node.slug}/${PACKAGE_RECORDS_FILENAME}`, records);
|
|
10519
10660
|
return;
|
|
@@ -10527,7 +10668,8 @@ const renderNode = (node, dir, files) => {
|
|
|
10527
10668
|
type: node.type,
|
|
10528
10669
|
name: node.name,
|
|
10529
10670
|
description: node.description || void 0,
|
|
10530
|
-
position: node.position
|
|
10671
|
+
position: node.position,
|
|
10672
|
+
agentPrompts: serializeAgentPrompts(node.agentPrompts)
|
|
10531
10673
|
})));
|
|
10532
10674
|
for (const entry of [...node.files].sort((a, b) => a.path.localeCompare(b.path, "en"))) {
|
|
10533
10675
|
assertSafeFilePath(entry.path, `${node.type} "${node.slug}"`);
|
|
@@ -10538,10 +10680,11 @@ const renderNode = (node, dir, files) => {
|
|
|
10538
10680
|
case "file":
|
|
10539
10681
|
assertSafeFilePath(node.fileName, `file node "${node.slug}"`);
|
|
10540
10682
|
files.set(`${dir}${node.fileName}`, node.bytes);
|
|
10541
|
-
if (node.name !== node.fileName || node.description !== "" || node.position !== void 0) files.set(`${dir}${node.fileName}${PACKAGE_NODE_META_SUFFIX}`, toJsonFile(compact({
|
|
10683
|
+
if (node.name !== node.fileName || node.description !== "" || node.position !== void 0 || node.agentPrompts !== void 0) files.set(`${dir}${node.fileName}${PACKAGE_NODE_META_SUFFIX}`, toJsonFile(compact({
|
|
10542
10684
|
name: node.name,
|
|
10543
10685
|
description: node.description || void 0,
|
|
10544
|
-
position: node.position
|
|
10686
|
+
position: node.position,
|
|
10687
|
+
agentPrompts: serializeAgentPrompts(node.agentPrompts)
|
|
10545
10688
|
})));
|
|
10546
10689
|
return;
|
|
10547
10690
|
}
|
|
@@ -10701,7 +10844,8 @@ const renderPackageChoices = (repoUrl, candidates, wanted) => {
|
|
|
10701
10844
|
candidate.primaryAirApp ? "app" : void 0
|
|
10702
10845
|
].filter(Boolean).join(" · ");
|
|
10703
10846
|
lines.push(` ${candidate.name} (${shape})`);
|
|
10704
|
-
|
|
10847
|
+
const description = iStringParse(candidate.description, "en");
|
|
10848
|
+
if (description) lines.push(` ${description}`);
|
|
10705
10849
|
for (const error of candidate.templateErrors) lines.push(` not a template: ${error}`);
|
|
10706
10850
|
}
|
|
10707
10851
|
lines.push("");
|
|
@@ -10752,7 +10896,7 @@ const runExport = async (client, nodeSlugOrId, options) => {
|
|
|
10752
10896
|
tree.rootSkill = {
|
|
10753
10897
|
slug: manifest.name,
|
|
10754
10898
|
name: manifest.name,
|
|
10755
|
-
description: manifest.description,
|
|
10899
|
+
description: iStringParse(manifest.description, "en"),
|
|
10756
10900
|
files: [{
|
|
10757
10901
|
path: PACKAGE_SKILL_ENTRY,
|
|
10758
10902
|
bytes: Buffer.from(deriveSkillDraft(tree), "utf8")
|